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

在实际工做中常常遇到对帐户的操做(帐户充值和帐户消费),处理的逻辑以下:html

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

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

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

  • 事务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。sql

按照业务的逻辑,消费和充值后,帐户的金额应该为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并发锁

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

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

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实现分布式并发锁的。

相关文章
相关标签/搜索