我使用的是mysql数据库,在我的系统运行很长实际都没有出现问题,但最近却出现了一个致命错误。用了很长时间才得以搞清楚。我的程序大致描述如下:
程序大致是,我们把仓库中的所有货品进行唯一编号,比如有100件商品就编100个号,每个号都有唯一的出厂码什么的,当订单请求到来是,程序按照出货逻辑取货,并标示被取货的为已出货,同时在这个货品纪录中写入一个订单号。(注意,是多个终端程序同时操作数据库)1 事务开始
2 读取数据库A中符合条件的纪录,并在这些符合条件的纪录中抽取1条纪录
3 对这条纪录进行处理,并更改这个纪录的状态,以免别的程序对其进行处理(具体到我的程序是把这个货品出货)
4 继续对这个纪录进行处理 .... ,并在这个纪录上添加处理的订单号。
5 事务结束运行都正常,但最近却出现了少量,极少量一个货品本被出掉了,但下一个单又取到了,相当于一个货品归属了两个单,。我无论如何测试都测试不出来。最后我假设服务器特别繁忙甚至宕机的情形,即在4后面加一个 sleep 的函数 ,以加大程序的执行时间。
当两个任务快速提交的时候,就会出现上述情形。它的过程是:
一个请求(A)按照一定的逻辑获取了一个货品(比如,123号货品),虽然状态改变了,但因为事务尚未提交,数据库中这个商品尚未更改状态。
另外一个请求 (B)在(A)事务尚未提交时到达了,按同样的逻辑取货会取到123号货品,.....
(A)提交事务,这时123号货品更改了状态和订单号,这时标示123号的订单号为(A)
(B)B事务提交,同样是123号货品,也改变状态和订单号。这样,这个123号货品就属于了(B),(A)订单就没有货品了。出现了致命错误。
我们说(B)实际上读取了脏数据。
现在的解决办法是,在事务开始加入了一个变量锁(设置一个超级变量作为进入的程序标示),在事务结束时开锁。这样就禁止了多个进程同时取订单。虽然效率低点,但正常情况下这个不费时间,可以忍受,在服务器繁忙或宕机时最多系统罢工但不至于出错。
但php没有跨越用户的全局变量,我考量使用文件作为变量锁,但我怕文件在读取与存储时也会出现延迟,于是就使事务中使用本数据库的一个表作为锁标记,具体办法是创建一个表,两个字段
id  关键字,自增长
name 锁定名词,唯一索引
事务开始时,插入表一个纪录,insert tablename (name) values ("A")
如果又有一个事务来了,它在插入这个标时会报错,这时退出事务并告诉系统繁忙,或系统自动1秒后再试。
事务结束后删除这个纪录即可。
在事务中插入系统的纪录,即使事务没有提交,另外进程在插入是,其索引的唯一性依然要接受检查,并考虑尚未提交的纪录。这样就可以做单万无一失。
请高人帮忙分析,有没有更好的处理办法。这个问题花了我很长时间,主要是对mysql的事务机制不够了解。

解决方案 »

  1.   

    你是用mysql/innodb吗, 假如用mysql/myisam是没有事务的
      

  2.   

    假如是innodb引擎,你的问题是不存在的.(当然语句写得不好,也有可能存在).
    假如是myisam引擎,是不支持事务. 可能会发生你所说的问题
    你的补救方法是可以的, 但有个缺陷. 
    "事务结束后删除这个纪录即可。", 
    假如事务结束后进程刚好中断(如网络问题等,可能性很小,但存在),这个记录永远存在,结果其他的进程无法进行新的事务.
    你可以改成表锁 lock table xxx write, 事务结束后手工再释放, 假如执行到一半进程中断, 这个锁也会释放.
    应用锁也可以.
      

  3.   

    谢谢回复。
    我是用的innodb,但上述清空确实存在,我也很纳闷。
    这个系统处理了40多万个订单了,一共出现8个这样的情形。但最近有天就出现了3个,这是非常严重的,我查过服务器记录,那天确实存在服务器负担过重而宕机的情形。
    后来我测试是干脆在事务里面加入sleep直接延长事务处理时间,那样的情况就测试出来了。
    我知道如果是写数据,数据库不会出问题,如果上一个事务锁定表时间确实太长,第二个事务无法获得资源最多就是报死锁错误。
    我的问题是,多个进程在一个表中“捞”记录,“捞”时是读取数据库,捞成功后做了出来才修改。这里应该涉及到系统所谓的“读”锁的问题。假如事务结束后进程刚好中断(如网络问题等,可能性很小,但存在)///我考虑过这种情形,我设定了一个插入时间,在任何进程删除锁定表时,连带15分钟前的所有锁定记录一起删除,这样很少发生的事件最多使系统停止服务15分钟,但不会出现错误。但显然,这样的处理很蹩脚的。呵呵。
      

  4.   

    你说的  lock table xxx read
    是不是架上这个语句后,xxx表就不能再被查询,直到unlock。我需要不被查询。因为我的上一个进程取(读)到一个货品时,还没有改变这个货品的状态,就不能允许另外的进程再去取(读)这个货品,因为一个货品只能属于一个订单,即只能被一个进程读取。
      

  5.   

    如果是innodb, 那就不要用lock table xxx write, 这个命令大部分是用在myisam不支持事务的流程上
    改一下你的流程吧1 事务开始
    2 读取数据库A中符合条件的纪录,并在这些符合条件的纪录中抽取1条纪录
    3 对这条纪录进行处理,并更改这个纪录的状态,以免别的程序对其进行处理(具体到我的程序是把这个货品出货)
    4 继续对这个纪录进行处理 .... ,并在这个纪录上添加处理的订单号。
    5 事务结束第一办法:
    问题是出在第3条语句, 可能出现更新丢失, 你要判定是否真正更新假设你更新的语句为update xxx set flag=1 where id=xxx 
    flag为状态, 1为更改, 0未更改请把第3个流程改为
    update xxx set flag=1 where id=xxx and flag=0
    if rowcount =0 
    -- 如果没更新, 说明这个记录在另一个进程已被其他进程读取
      rollback
    endif第二办法:
    MYSQL/INNODB 不管什么哪个级别的事务, SELECT 都不加锁,除非后面加for update/share
    你也可以在第2个步骤加锁, 改为select .. for update  或第2个步骤前加个排他锁lock table .. write
    但这样做并发性降低.推荐用第一办法. 这种方法叫乐观性锁定.