近日,因人员调整接手了一个其他部门负责的项目。随后发现其中的很多关键环节是没有考虑mysql并发操作的,现列出存在的一例问题 并分享如何解决的。
问题描述:
用户账户余额转移赠送 (用户A将自己的账户剩余金额赠送给用户B),同一时刻还可能存在用户A消费操作(例如赠送操作在app,消费操作在手机站)。
PHP 代码:
$sql_select = 'select amount from user where id = 1';
/***
* 从mysql查询出的用户A余额,放入php 变量$amount
* 做一些逻辑处理,例如A用户是否有权限操作,此处代码省略
*
* 用户A账户清零
* $sql_deduct = ' update user set amount = 0 where id = 1';
*/
//将 用户A金额转移给用户B
$sql_transfer = sprintf (' update user set amount = amount+%d where id = 2', $amount );
此种写法存在问题,因:
1.$sql_select执行查询后,有可能存在并发操作,例如刚好此时 用户A有其他消费,mysql 账户余额amount 被扣减。
此时php 变量$amount 与 mysql amount 内容已经不一致。
2. $sql_transfer 使用了php变量:$amount(值已经不是最新的) 进行账户增加,会导致增加与扣除的金额不一致。
解决方案:
处理并发修改一般是要进行加锁防止其他mysql session 修改同一条内容,mysql 也提供了独占锁机制解决并发更新问题。
方案1:
InnoDB 引擎可以使用select … for update , 对要修改的表中的某一行加锁。另外:此方案对MyISAM引擎无效。MyISAM可以考虑使用方案2.
SET autocommit = 0;
$sql_select = 'select amount from user where id = 1 for update ';
//这里可以写修改db 的业务逻辑
COMMIT;
1.执行$sql_select后,user id = 1 的这一行会被mysql 锁定,其他mysql session 只能读取锁定前的数据,其他mysql session要加锁或者修改涉及此行,都会被阻塞(例如修改,删除此行,修改表结构等),直到锁定释放 (commit提交事务 或 mysql session 结束)
2.必须将mysql 自动提交关闭(SET autocommit = 0;),否则锁定无效
3.特定的行进行加锁仅针对“特定的索引 ” 有效,例如id = 1 的select查询,是行锁因id 是主键索引。
3.1 如果查询条件是 id>1,那么mysql 会锁定整个表.
3.2 如果查询条件没有使用索引,那么mysql 也会锁定整个表.
建议大家尽可能使用行锁,以提高mysql并发性能。
另外, 需要注意的是 ,此方案可能会造成死锁。这个还要从业务方面尽可能避免,以后会继续讨论如何避免死锁。
方案2:
2. 使用LOCK TABLES … READ 锁表
LOCK TABLES user READ
//这里可以写修改db 的业务逻辑
UNLOCK TABLES;
此方案比较粗暴,会锁定整个表的写操作,但如果在MyISAM 引擎下,也就只能选择这种方式了。
结论:
mysql 加锁本质上都是通过牺牲并发性能换取数据的一致性,所以在业务需求分析设计时,就要考虑哪些可能存在并发写入,进行规划尽可能减少锁的次数、时间。
作者: 白金马桶