在实际工做中常常遇到对帐户的操做(帐户充值和帐户消费),处理的逻辑以下:html
// 1 查询帐户当前的金额 // 2 根据操做,计算操做后的金额 // 3 更新帐户的金额
然而,在实际中常常会有并发操做的问题,下面经过在数据中执行SQL的方式,模拟下不作并发处理的状况:mysql
数据库是MySQL,隔离级别采用默认的可重复读,表为t_money,只有两列:id、money,只有一条记录id=1, money=1000。分别起两个客户端,模拟并发操做的行为:redis
序号 | 事务1 | 事务2 |
---|---|---|
1 | start transaction; | |
2 | start transaction; | |
3 | select * from t_money where id=1; | |
4 | select * from t_money wehre id=1; | |
5 | update t_money set money=900 where id=1; | |
6 | update t_money set money=1200 where id=1; (不能执行,被阻塞) | |
7 | select * from t_money where id=1; | |
8 | commit; | (事务1执行commit后,被阻塞的update执行) |
9 | select * from t_money where id=1; | select * from t_money where id=1; |
10 | commit; | |
11 | select * from t_money where id=1; | select * from t_money where id=1; |
按照上面的步骤执行完成后,11步查出来帐户id=1的money=1200。sql
按照业务的逻辑,消费和充值后,帐户的金额应该为1100,而系统中id=1的帐户金额竟然为1200,这是绝对不能接受的!数据库
将更新金额的语句,使用:并发
update t_money set money=money-100 where id=1;
update会使用“当前读”,能够读取到其它事物未提交的数据。当前读遇到其它事务的写操做时,会被阻塞,引发当前读的语句:分布式
select ... for update; select ... lock in share mode; update delete insert
也就是,操做前要得到锁,操做完成释放锁;没有得到锁,不容许进行操做,直接返回并发错误。code
在实际系统中,每每是分布式部署的,那么就须要加分布式锁。最容易想到(本人)的就是使用redis,在redis中使用setnx,伪代码以下:htm
if(redis.setnx(id)){ // 加锁成功 // 帐户操做 } else { // 返回并发错误,由调用者处理后续逻辑(重试等) }
在方案1中,在加锁失败后,直接返回并发异常,调用方须要重试。实际上,第一次请求时,虽然不能得到锁,可是可能在1s以后就能够得到锁了,咱们何不如稍微等待下再重试呢?事务
更加优雅的加锁,伪代码:
if (redis.setnx(id)) { // 加锁成功 // 帐户操做 } else { // 第一次加锁失败 Thread.sleep(1000); // 等待1s,也能够等待并指定屡次重试 if (redis.setnx(id)) { // 帐户操做 } else { // 返回并发错误 } }
对于redis实现并发锁,有不少能够研究的细节,好比:setnx成功后,系统挂了,后续加锁就永远不能成功了,该如何处理?更多细节,能够看看他人是如何用redis实现分布式并发锁的。