使用mysql+php开发中如果涉及到金钱或者稀有的资源,在处理时我们要保证数据的唯一性,因此要启用事务功能。在使用事务的时候,一般都是创建innodb的表,这个可以通过设置表的type为innodb搞定。下面说说mysql对事务的处理方式,开发的时候要注意:
1.切记innodb是行锁,只是锁住某条记录。因此如果出现对同一条记录的操作,你们后面的会被阻塞。而插入记录不受影响。
2.如果先insert了数据,然后update了已经被锁定的行,会等待行锁解除或者超时。如果超时,insert的数据不会回滚,update的内容会回滚。(这条是别人分析出来的,后来自己做测试的时候,发现只要没有commit就不会提交insert。原意只是说明insert在当前事务中执行成功(php返回true),而update执行失败(,因行锁死而超时,php返回flase),而由于最后没有commit,因此都会回滚。)
3.如果出现了死锁,那么检测到死锁的进程会完全回滚。
4.两个会话如果同时开启事务,会话1做了inset操作并提交。会话2在事务中是无法看到新增加的insert记录的(这个要注意)。
目前我从实验得到的结论就是上面这么多,下面用一个实例来说明:
系统是一个购物系统,上线了一个超低价抢购的商品,数量有限。很多用户会同时抢购,这就要求正确处理产品数量,不能超卖,而且还要准确扣除用户钱包的余额并记录交易流水。
原系统是这么设计的:
商品表会记录商品总数量及已销售数量,如果用户购买产品,会先select产品表查看是否有剩余商品,如果有剩余,则执行交易函数。
交易函数接收三个变量,分别是用户id,商品id及金额,交易函数的流程是,首先对用户是否有足够的钱支付进行判断,如果有则开启事务,事务开启后先insert记录交易,然后更新已销售数量(先取出已销售数值,加一再赋值给已销售数量),此时判断两次操作是否有效,有效则提交。之后对客户资金进行扣除,如果扣除失败则编写相应的sql语句取消第一次事务的操作。
上线运营,还没多久就出现问题了,商品表显示商品还有剩余,而用户的交易流水却已经超过了总商品额。
首先认为是同时访问用户太多造成的,根据第二条删除部分认为先insert,而update时超时,造成有交易记录而没有更新已销售数量(实际上这并不是造成这个问题的原因)。如果发生这样的情况,后面的交易资金就会存在没有扣除的情况。这里就出现了第一个潜在的bug,因为对商品的处理和对用户资金的处理不在一个事务里面,而恰巧php的执行在两个事务中间中断,就会出现用户下了订单却没有扣钱。
而后来自己测试,不会发生一个失败的事务里面,做了insert而不做update操作的情况。此时发问题的原因关注在同一个事务里面的update里面,发现原开发是先取出已销售数量,然后做+操作后,又赋值回去了。这个是典型的bug,也是造成这个问题的基本原因,正确的用法应该是value=value+num,而不是value=$value.
分析得知,mysql的事务功能还是很靠谱的,真正需要注意的地方不多,因为只要不commit就不会提交修改,这个是很安全的。所以总结下经验:
1.开启事务后,如果同样的事务里面有insert操作,先进行insert,然后再进行update操作。根据insert和update的结果,判定提交还是回滚。
2.资源有限的量,应该通过update table set value=value+/-num来处理。这样做的有两个原因,一是避免高并发时value取值错误(说的是把value赋给php变量,而之后又把php变量赋值给value字段的情况),二是避免当前资源数量计算错误(比如在同样的事务中,用select sum计算多条交易记录,来判断已使用资源的量是不可取的,因为开启事务后,其他会话的insert记录,不会被当前会话select到)。
3.页面超时造成的update锁死的影响。update的行如果锁死,大概需要50秒返回错误,而php一般是30秒就响应超时了,虽然这不影响结果的准确性,但是如果想捕获update的响应超时,记得设置php超时时间。