分布式系统敏感操作的并发处理(并发锁)

在实际工作中经常遇到对账户的操作(账户充值和账户消费),处理的逻辑如下:

// 1 查询账户当前的金额
// 2 根据操作,计算操作后的金额
// 3 更新账户的金额

然而,在实际中经常会有并发操作的问题,下面通过在数据中执行SQL的方式,模拟下不做并发处理的情况:

数据库是MySQL,隔离级别采用默认的可重复读,表为t_money,只有两列:id、money,只有一条记录id=1, money=1000。分别起两个客户端,模拟并发操作的行为:

  • 事务1,账户消费100元
  • 事务2,账户充值200元
序号 事务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。

按照业务的逻辑,消费和充值后,账户的金额应该为1100,而系统中id=1的账户金额居然为1200,这是绝对不能接受的!

解决方案

1. 利用MySQL的当前读

将更新金额的语句,使用:

update t_money set money=money-100 where id=1;

update会使用“当前读”,可以读取到其它事物未提交的数据。当前读遇到其它事务的写操作时,会被阻塞,引起当前读的语句:

select ... for update;
select ... lock in share mode;
update
delete
insert

2. redis并发锁

也就是,操作前要获得锁,操作完成释放锁;没有获得锁,不允许进行操作,直接返回并发错误。

在实际系统中,往往是分布式部署的,那么就需要加分布式锁。最容易想到(本人)的就是使用redis,在redis中使用setnx,伪代码如下:

if(redis.setnx(id)){
    // 加锁成功
    // 账户操作
} else {
    // 返回并发错误,由调用者处理后续逻辑(重试等)
}

2. 优雅的redis并发锁

在方案1中,在加锁失败后,直接返回并发异常,调用方需要重试。实际上,第一次请求时,虽然不能获得锁,但是可能在1s之后就可以获得锁了,我们何不如稍微等待下再重试呢?

更加优雅的加锁,伪代码:

if (redis.setnx(id)) {
    // 加锁成功
    // 账户操作
} else {
    // 第一次加锁失败
    Thread.sleep(1000); // 等待1s,也可以等待并指定多次重试
    if (redis.setnx(id)) {
        // 账户操作
    } else {
        // 返回并发错误
    }
}

对于redis实现并发锁,有很多可以研究的细节,比如:setnx成功后,系统挂了,后续加锁就永远不能成功了,该如何处理?更多细节,可以看看他人是如何用redis实现分布式并发锁的。

posted @ 2018-04-19 14:40  Acode  阅读(1648)  评论(0编辑  收藏  举报
您是本站第访问量位访问者!