普通场景
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执行完,请求2才执行,此时两个请求都被正常处理,最终余额20元
graph TD
请求1获取到当前金额100元 --> 请求1扣除30元,更新为70元 --> 请求2获取到当前金额70元 --> 请求2扣除50元,更新为20元 --> 最终余额20元
- 请求1和请求2同时获取到当前为100元,然后1先执行扣除,2再执行扣除,最终余额50元
graph TD
请求1获取到当前金额100元 --> 请求2获取到当前金额100元 --> 请求1扣除30元,更新为70元 --> 请求2扣除50元,更新为50元 --> 最终余额50元
如果在并行执行时出现了这种顺序,那最终数据库中的剩余金额是有误的,会造成严重的问题。
如何保证并发完全
需求的处理包含两个Mysql操作,第一步查询,第二步更新,在并发场景下,多个请求间不能保证顺序执行,所以导致了错误。以下以如何安全扣除30元来举例:
1. 更新时指定条件
- 首先查询出当前余额,如100
- 更新时除了指定uid外还要指定money=100时才更新
- 如果更新失败则重复此步骤
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. 附加版本号
- 查询当前数据和版本号
- 更新时指定版本号
- 如果失败则重试
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. 使用分布式锁
- lock(uid)
- 获取当前余额
- 更新余额
- unlock(uid)
分布式锁一般基于redis的set ex nx原子命令来实现 建议使用1、2依靠数据库保障并发安全,3会引入额外的中间件。