MySQL并发安全

166 阅读2分钟

普通场景

mysql> DESCRIBE users;
+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| uid   | int  | NO   | PRI | NULL    |       |
| money | int  | NO   |     | NULL    |       |
+-------+------+------+-----+---------+-------+
mysql> SELECT * FROM users;
+------+-------+
| uid  | money |
+------+-------+
| 1001 |   100 |
+------+-------+

现在有一个数据表users,里面有一条数据uid是1001,money是100,表示账户中的余额。 目前有一个需求是要扣除30元,那么我们可以这么写

select money FROM users WHERE uid=1001;       // 得到uid为1001的用户有100元
UPDATE users SET money=100-30 WHERE uid=1001; // 扣除30剩余70  

并发场景

在大流量场景下,经常采用微服务架构,请求会被分派到多实例上并行执行。 如请求1扣除30元,请求2扣除50元。两个请求分别在实例1和实例2中并行执行,考虑以下几种情况:

  1. 请求1执行完,请求2才执行,此时两个请求都被正常处理,最终余额20元
graph TD
请求1获取到当前金额100元 --> 请求1扣除30元,更新为70元 --> 请求2获取到当前金额70元 --> 请求2扣除50元,更新为20元 --> 最终余额20元
  1. 请求1和请求2同时获取到当前为100元,然后1先执行扣除,2再执行扣除,最终余额50元
graph TD
请求1获取到当前金额100元 --> 请求2获取到当前金额100元 --> 请求1扣除30元,更新为70元 --> 请求2扣除50元,更新为50元 --> 最终余额50元

如果在并行执行时出现了这种顺序,那最终数据库中的剩余金额是有误的,会造成严重的问题。

如何保证并发完全

需求的处理包含两个Mysql操作,第一步查询,第二步更新,在并发场景下,多个请求间不能保证顺序执行,所以导致了错误。以下以如何安全扣除30元来举例:

1. 更新时指定条件

  1. 首先查询出当前余额,如100
  2. 更新时除了指定uid外还要指定money=100时才更新
  3. 如果更新失败则重复此步骤
UPDATE users SET money=100-30 WHERE uid=1001 AND money=100;

多个update语句的执行顺序由Mysql事务来保证,不会出现并发安全问题

SQL可以进一步优化来提高成功率,这样只要剩余金额大于等于要扣除的值,就可以成功

UPDATE users SET money=money-30 WHERE uid=1001 AND money>=30;

2. 附加版本号

  1. 查询当前数据和版本号
  2. 更新时指定版本号
  3. 如果失败则重试
SELECT money, version FROM users WHERE uid=1001;   // money为100, version为1
UPDATE users SET money=70,version=2 WHERE uid=1001 AND version=1;

3. 使用分布式锁

  1. lock(uid)
  2. 获取当前余额
  3. 更新余额
  4. unlock(uid)

分布式锁一般基于redis的set ex nx原子命令来实现 建议使用1、2依靠数据库保障并发安全,3会引入额外的中间件。