数据库并发问题详述.docx
- 文档编号:5205551
- 上传时间:2022-12-13
- 格式:DOCX
- 页数:15
- 大小:32.49KB
数据库并发问题详述.docx
《数据库并发问题详述.docx》由会员分享,可在线阅读,更多相关《数据库并发问题详述.docx(15页珍藏版)》请在冰豆网上搜索。
数据库并发问题详述
数据库并发问题详述
数据库并发问题详述问题背景及特点:
我们在使用多用户数据库时常常会碰到数据更新失败、删除失等情况,如果有多个用户且同时访问一个数据库则当他们的事务同时使用相同的数据时可能会发生并发问题。
并发问题包括:
1.丢失或覆盖更新。
(幻像读)
2.未确认的相关性(脏读)。
3.不一致的分析(非重复读)。
详细描述:
1.丢失更新
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。
每个事务都不知道其它事务的存在。
最后的更新将重写由其它事务所做的更新,这将导致数据丢失。
例如,两个编辑人员制作了同一文档的电子复本。
每个编辑人员独立地更改其复本,然后保存更改后的复本,这样就覆盖了原始文档。
最后保存其更改复本的编辑人员覆盖了第一个编辑人员所做的更改。
如果在第一个编辑人员完成之后第二个编辑人员才能进行更改,则可以避免该问题。
2.未确认的相关性(脏读)
当第二个事务选择其它事务正在更新的行时,会发生未确认的相关性问题。
第二个事务正在读取的数据还没有确认并且可能由更新此行的事务所更改。
例如,一个编辑人员正在更改电子文档。
在更改过程中,另一个编辑人员复制了该文档(该复本包含到目前为止所做的全部更改)并将其分发给预期的用户。
此后,第一个编辑人员认为目前所做的更改是错误的,于是删除了所做的编辑并保存了文档。
分发给用户的文档包含不再存在的编辑内容,并且这些编辑内容应认为从未存在过。
如果在第一个编辑人员确定最终更改前任何人都不能读取更改的文档,则可以避免该问题。
3.不一致的分析(非重复读)
当第二个事务多次访问同一行而且每次读取不同的数据时,会发生不一致的分析问题。
不一致的分析与未确认的相关性类似,因为其它事务也是正在更改第二个事务正在读取的数据。
然而,在不一致的分析中,第二个事务读取的数据是由已进行了更改的事务提交的。
而且,不一致的分析涉及多次(两次或更多)读取同一行,而且每次信息都由其它事务更改;因而该行被非重复读取。
例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。
当编辑人员第二次读取文档时,文档已更改。
原始读取不可重复。
如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
4.幻像读
当对某行执行插入或删除操作,而该行属于某个事务正在读取的行的范围时,会发生幻像读问题。
事务第一次读的行范围显示出其中一行已不复存在于第二次读或后续读中,因为该行已被其它事务删除。
同样,由于其它事务的插入操作,事务的第二次或后续读显示有一行已不存在于原始读中。
例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。
如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。
--------------------------------------------------------------------------------------------------------
从上面可以看到,解决并发主要是用到了锁和事务。
锁:
给记录或表加上锁是为了对当前操作对象加上一个状态表示位,
让其它用户在获取编辑权限时有了判断。
事务:
是为了保证一组操作的完整性。
(要么就全部成功,要么就全部失败)
--------------------------------------------------------------------------------------------------------
一般处理并发问题时我这样做:
1.开启事务。
2.申请写权限,也就是给对象(表或记录)加锁。
3.如果失败,则结束事务,过一会重试。
4.如果成功,也就是给对象加锁成功,防止其它用户再用同样的方式打开。
5.进行编辑操作。
6.写入所进行的编辑结果。
7.如果写入成功,则提交事务,完成操作。
8.如果写入失败,则回滚事务,取消提交。
9.(7.8)两步操作已释放了锁定的对象,恢复到操作前的状态。
(
遭遇数据库并发问题
文章分类:
Java编程
最近碰到一个数据库并发的问题,觉得是一个很有意义的经验教训,备忘一下.
大致过程是这样的:
在一次迭代发布之后,用户反馈了一个问题:
虚拟商品交易成功,但是买家没有收到卡密.而在日志中跟踪该笔交易,正好有一个超时情况,日志中打印出了程序执行的完整路径,也就是说整个交易完全没有问题.当时百思不得其解.怀疑数据库出了问题.又过了一天,用户反映了同样的问题,对比一下本次发布的代码修改,里面有一个是关于sql的优化,比较修改前后的代码,终于找到了问题的罪魁祸首:
原来是在sql上出现了问题.
最开始正确的sql:
Sql代码
1.update table set status =#newStatus#, trade_id = #tradeId#, buyer_id = #buyer_id# where id in (
2. select * from (
3. select id from table
4. where item_id = #itemId#
5. and status = #initStatus#
6. order by id
7. ) where rownum <= #count#
8.) and status = #initStatus#
updatetablesetstatus=#newStatus#,trade_id=#tradeId#,buyer_id=#buyer_id#whereidin(
select*from(
selectidfromtable
whereitem_id=#itemId#
andstatus=#initStatus#
orderbyid
)whererownum<=#count#
)andstatus=#initStatus#
这句sql的意思是,一个商品下面有多个虚拟物品(比如游戏币,充值卡),一个买家购买了该商品的多个虚拟物品,此时应该将虚拟物品的状态设置为已购买状态,因为这些虚拟物品会有多个人购买,为了避免同一个虚拟物品被不同的人同时购买,需要对要购买的虚拟物品排序,然后从中获取要购买的数量,最后更新状态.
修改后有问题的sql是这样:
Sql代码
1.update table set status =#soldStatus#, trade_id = #tradeId#, buyer_id = #buyer_id# where id in (
2. select * from (
3. select id from table
4. where item_id = #itemId#
5. and status = #initStatus#
6. order by id
7. ) where rownum <= #count#
8.)
updatetablesetstatus=#soldStatus#,trade_id=#tradeId#,buyer_id=#buyer_id#whereidin(
select*from(
selectidfromtable
whereitem_id=#itemId#
andstatus=#initStatus#
orderbyid
)whererownum<=#count#
)
当时咨询dba,认为在里面已经判断了状态,外层sql的状态判断显得多余,建议去掉.
虽然整个sql操作过程也是在事务中执行.但是依然无法避免并发的出现,经过分析,基本上可以还原出问题的场景:
因为更新状态实际上分为两步,首先要查出数据,接着再更新.当两个人同时购买同一个商品时.如果两人同时查出同一批数据,一人先更新,退出事务(此时这批数据的状态已经发生改变),接着另外一个人再次更新,如果此时更新没有判断这批数据当前的状态(也就是做乐观锁),那么就会覆盖前更新者的数据,导致前一个没有获得最终购买的虚拟物品.
而外层的状态判断相当于对执行的sql加了一个乐观锁,如果查询得到的那批数据被另一个请求更新,则在更新状态的数量将与请求的数量不一致,导致该次更新失败.
java并发问题
现正着手做一个考勤系统,要用到并发,框架是用的mina,但是后台的一些业务处理要用到并发,比如说手机短信发送,日志记录,记录数据库等等,
我是这样想的:
因为日志情况特殊,必须按时间来记录,所以日志我觉得用到队列,先进先出的规则,手机短信和数据库就不用按队列了。
我不知道这样做是不是正确的。
想问一下大家的意见,还有这种项目应该考虑哪些?
并发我用的是jdk里面的java.util.concurrent包。
谢谢!
Java并发编程-问题
从一开始Java就被设计成支持并发编程的语言,java语言及其核心类库都有对并发编程的支持。
从5.0开始,Java平台引入了一些高层的并发接口。
本系列文章将尽可能的概括这些内容。
进程与线程
并发编程模式中,有两个基本执行单元:
进程与线程。
进程和线程是现代操作系统的基本概念。
一个进程拥有独立完备的执行环境,进程拥有私有的计算机资源,比如独立的内存空间、代码段、指令寄存器等等。
进程在操作系统中基本等同于应用程序。
最终用户看到的独立程序有可能是多个互相协作的进程,为了方便进程之间的通信,大多数操作系统支持进程间通信(InterProcessCommunication,IPC)资源,比如管道和端口。
IPC往往不仅仅在同一台计算机系统上使用,也往往运用于不同计算机系统之间的通信。
线程通常也称轻量级进程,线程拥有的资源比进程的要少。
线程只存在于进程中,一个进程可以包含多个线程。
比如一个Java程序中可以有多个线程存在。
线程不拥有独立的内存空间,而是和同进程内的其他线程共享进程的内存空间。
由于线程共享进程的资源(内存或者打开的文件),同进程的线程之间往往需要大量的互斥和同步,保证资源使用的可确定性。
这在前面文章中已经说过,资源共享是并发编程中同步和互斥的根源。
由于进程的特性,使得它们之间资源共享的冲突比较少,因此并发编程主要是针对线程的,多线程编程是Java程序的基本特征。
因此这儿讲的并发编程主要是针对Java线程编程的。
Java线程
简单来说,每个Java线程都有一个Thread实例与之对应。
创建线程对象的方法通常有两种:
1.直接创建和管理,也就是每当程序需要异步执行任务时就实例化一个Thread对象,然后自己管理其生命周期。
2.抽象线程管理,使其从程序代码中分离开来。
这些接口在java5之后的java.util.concurrency包中提供。
我们先讨论第一种方式,java.util.concurrency包提供的高层工具我们在后面的文章再讨论。
第一种方式创建线程非常直接,有两种方法可以创建一个线程实例:
1.生成一个Runnable对象,并将它传递给Thread对象。
publicclassHelloWorldimplementsRunnable{
publicvoidrun(){
System.out.println("HelloWorld");
}
publicstaticvoidmain(Stringargs[]){
(newThread(newHelloWorld())).start();
}
}
2.继承Thread并实现其run方法:
publicclassHelloThreadextendsThread{
publicvoidrun(){
System.out.println("HelloWorld!
");
}
publicstaticvoidmain(Stringargs[]){
(newHelloThread()).start();
}
}
如何选择使用哪种方式呢?
第一种模式更为通用,实现一个Runnable接口允许你继承并复用某类。
第二种更简单,缺点是必须继承Thread。
你可以根据具体情况选择。
Thread对象中定义了一些有用的方法来管理线程的生命周期:
1.publicstaticvoidsleep(longmillis)throwsInterruptedException方法,该方法挂起当前线程一段时间,主动让出处理器让其他线程使用。
sleep方法还可以用来控制执行的速度。
注意这个方法在挂起线程时,有可能被其他线程中断挂起,因此不能依赖其时间参数来定时。
2.publicvoidinterrupt()方法,该方法中断某线程正在做的事情,告诉它某些事情发生了需要处理。
程序员需要捕捉这个中断异常,并在异常处理中执行应该做的动作。
接受中断的方式有两种,一种是被中断线程目前正在执行一个能抛出InterruptedException的方法,比如sleep或者Object.wait等方法,还比如一些可以被interrupted的SeverSocket.accept方法等等。
下面是示例代码:
for(inti=0;i //Pausefor4seconds try{ Thread.sleep(4000); }catch(InterruptedExceptione){ //We'vebeeninterrupted: nomoremessages. return; } //Printamessage System.out.println(importantInfo[i]); } 另一种是程序正在执行一些不抛出InterruptedException的动作,这时该线程要负责定期使用interrupted()方法检查当前线程是否受到中断信号,然后做相应处理。 下面是示例代码: for(inti=0;i heavyCrunch(inputs[i]); if(Thread.interrupted()()){ //We'vebeeninterrupted: nomorecrunching. return; } } SwingWorker的cancel方法就是采用的第二种模式实现的,SwingWorker的cancel方法调用其任务线程的interrupt()方法,而doInBackground方法应该定期调用SwingWorker的isCanceled方法判断当前任务是否被取消。 isCanceled()最终调用了Thread.interrupted()方法检测当前线程是否接受到中断信号。 3.publicstaticThreadcurrentThread()获得当前线程对象。 4.publicbooleanisInterrupted()该线程是否受到中断信号。 5.publicstaticbooleaninterrupted(),检测当前线程是否接受到中断信号。 它实际上是调用了Thread.currentThread().isInterrupted()实现的。 6.publicfinalvoidjoin()该方法允许一个线程等待另一个线程的结束。 比如t是一个目前正在执行的线程,那么t.join()将目前线程挂起直至线程t结束。 线程示例 下面举例总结本节所说明的一些概念。 SimpleThreads由两个线程组成,第一个是主线程,主线程使用一个Runnable对象MessageLoop创建一个新的线程,并等待该线程结束。 如果MessageLoop线程耗时太长,那么主线程将强制中断该线程。 MessageLoop线程打印出一系列的消息,如果被中断,则打印消息并退出: publicclassSimpleThreads{ //Displayamessage,precededbythenameofthecurrentthread staticvoidthreadMessage(Stringmessage){ StringthreadName=Thread.currentThread().getName(); System.out.format("%s: %s%n",threadName,message); } privatestaticclassMessageLoopimplementsRunnable{ publicvoidrun(){ StringimportantInfo[]={ "Mareseatoats", "Doeseatoats", "Littlelambseativy", "Akidwilleativytoo" }; try{ for(inti=0;i //Pausefor4seconds Thread.sleep(4000); //Printamessage threadMessage(importantInfo[i]); } }catch(InterruptedExceptione){ threadMessage("Iwasn'tdone! "); } } } publicstaticvoidmain(Stringargs[])throwsInterruptedException{ //Delay,inmillisecondsbeforeweinterruptMessageLoop //thread(defaultonehour). longpatience=1000*60*60; //Ifcommandlineargumentpresent,givespatienceinseconds. if(args.length>0){ try{ patience=Long.parseLong(args[0])*1000; }catch(NumberFormatExceptione){ System.err.println("Argumentmustbeaninteger."); System.exit (1); } } threadMessage("StartingMessageLoopthread"); longstartTime=System.currentTimeMillis(); Threadt=newThread(newMessageLoop()); t.start(); threadMessage("WaitingforMessageLoopthreadtofinish"); //loopuntilMessageLoopthreadexits while(t.isAlive()){ threadMessage("Stillwaiting..."); //Waitmaximumof1secondforMessageLoopthreadto //finish. t.join(1000); if(((System.currentTimeMillis()-startTime)>patience)&&t.isAlive()){ threadMessage("Tiredofwaiting! "); t.interrupt(); //Shouldn'tbelongnow--waitindefinitely t.join(); } } threadMessage("Finally! "); } } 线程之间共享数据引起了并发执行程序中的同步问题。 那些数据是可能需要同步访问的呢? 很简单,线程之间能够共享的数据,也就是对多个线程可见的数据。 Java的数据有两种基本类型内存分配模式(不算虚拟机内部类型,详细内容参见虚拟机规范): 运行时栈和堆两种。 由于运行时栈是线程所私有的,它主要用来保存局部变量和中间运算结果,因此它们的数据是不可能被线程之间所共享的。 内存堆是创建类对象和数组地方,它们是被虚拟机内各个线程所共享的,因此如果一个线程能获得某个堆对象的引用,那么就称这个对象是对该线程可见的。 线程之间通信基本上通过共享对象引用来达到共享对象的简单类型字段和引用字段。 由于不涉及I/O操作,这种模式的共享比IPC共享要高效的多。 但也使得两类错误成为可能: 线程干扰和内存一致性错误。 防止此类问题发生的线程编程技术称作同步。 我们详述一下这两个错误的概念。 线程干扰 考察下面的一段代码: classCounter{ privateintc=0; publicvoidincrement(){ c++; } publicvoiddecrement(){ c--; } publicintvalue(){ returnc; } } Counter的目的是对increment方法的调用将c增加1,对decrement的调用将c减去1。 然而如果一个Counter对象被多个线程所引用,那么这些线程之间的干扰就让我们经常得不到期望的结果。 当运行在不同线程中对同一对象进行访问的两个操作发生时,干扰就会产生。 这是因为这两个操作往往是由多步组成的,而且它们的执行顺序是可以互相交织的。 表面看来,Counter对象的increment和decrement操作是不可能交织的,每个操作都是一个简单的Java语句。 实际上这些语句都已经被虚拟机翻译成了好几步的指令。 我们不再详细描述虚拟机所采用的指令步骤,只需知道一个c++操作可能被虚拟机分为三步: 1.获取c的当前值 2.将该值加上1 3.将加的结果保存回变量c c--也是同样三步,除了第二步进行减操作外。 假设A线程调用increment方法的同时B线程调用decrement方法,而c的初始值为0,那么它们的交织的指令动作可能依着下面的顺序进行: 1.线程A: 获取c. 2.线程B: 获取c. 3.线程A:
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 数据库 并发 问题 详述