中间件 | Redis - [分布式锁 & 事务]

§1 分布式锁

redis 是线程安全的

  • redis 是单线程的
    因为作为内存数据库,CPU 很难成为它的性能瓶颈
    这里说的单线程是它执行指令的线程,IO 部分是支持多线程的
  • redis-server 是线程安全的
    因为 redis 是单线程执行指令的,所以线程安全
  • 但线程安全不等于业务上线程安全
    这是因为可能出现多个客户端对 redis 的同一个 key 进行并发竞争
    这可能导致业务上应该先执行的操作延后,或者并发修改同一个 key 但其中一个结果被另一个覆盖
    上述场景与 ConurrentHashMap 线程不安全原理一致
  • 可以使用分布式锁解决

redis 主从复制的集群是 AP 的

  • redis 单节点是 CP 的
  • 但是当 ridis 组成了主从复制的集群时,就是 AP 的了
    这是因为主从复制存在数据的异步复制,复制过程中主节点挂了,会重新选主,从节点升级为 master,但这个新 master 可能不带有分布式锁信息

分布式锁思路梳理

  • 检查业务处理中缓存,如果存在说明是过期的,业务可能还在处理,会生成自动或人工工单,当确认成功或失败后重启业务
    为防止主从复制时宕机导致的数据丢失,可以使用其他的持久化工具
  • 查询业务完成缓存,只有缓存中没有,才尝试创建锁
    防止多消费时,刚刚消费完成解锁,就又消费一次
  • 根据业务信息生成锁的 bizLockKey
    示例:String bizLockKey = "RLK:" + biz_prefix + biz_id
    biz_prefix 是业务前缀,比如 so:wms: 这是一个销售单流转到 wms 系统的前缀
    biz_id 是业务号,对应上面例子,可以是销售单的单号,也可以是 wms 系统的工单号
  • 生成带有主机线程信息的随机值 bizLockKey
    示例:String bizLockValue = ip_short + UUID + thread
  • try
  • 尝试通过 setnx 获取分布式锁,需要携带值、超时时间
    超时时间是防止业务出错或宕机导致的不能释放锁
    携带值是为了保证以后解的是现在加的锁
    redisTemplate.opsForValue.setIfAbsent(bizLockKey,bizLockValue,time,timeUnit);
  • 未抢到锁返回,抢到了处理业务
  • finally 释放锁,释放时需要判断是否是自己的锁
    防止 A 加锁、A 超时释放锁、B 加锁、A 释放 B 的锁
    同时,可能出现判断通过后,锁突然变更了(A 的锁过期了,现在是 B 的锁了)
    官网推荐使用 LUA 脚本,使判断和删除原子性
    Jedis jedis = RedisUtils.getJedis();
    String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
            "then\n" +
            "    return redis.call(\"del\",KEYS[1])\n" +
            "else\n" +
            "    return 0\n" +
            "end";
    
    try{
        if("1".equals(jedis.eval(lua, Arrays.asList(key),Arrays.asList(vlue)))){
            // do sth
        }
    }finally {
        if(null != jedis) jedis.close();
    }
    
    也可以使 bizLockKey 携带随机信息(保证不会误删除)
    也可以使用 redis 的事务解决
    while(true){
        redis.watch(key);
        if(Value.equals(redis.opsForValue().get(key))){
            redis.setEnableTransactionSupport(true);
            redis.multi();
            if(!CollectionUtils.isEmpty(redis.delete(Key))){
                redis.unwatch();
                break;
            }
        }
    }
    
    也可以通过锁续期解决,见下面
  • 锁续期
    方案1:是直接给一个很大的实际,比如 300 秒,实际等于没有续期
    方案2:加锁成功后,启动守护线程,每隔一定时间刷新超时时间,若业务处理线程存活,则自动进行续期否则超时时间开始真实的流逝
    方案3:推荐,直接通过 Redisson 实现,此方式其实就是方案2
    RLock lock = redisson.getLock(key,value);
    try {
       lock.lock();
       // ...
    }finally {
       if(lock.isLocked() && lock.isHeldByCurrentThread())
           lock.unlock();
    }
    

分布式锁坑总结

  • 业务处理到一半卡死,没走到放锁的逻辑,ridis 里始终有锁
    增加过期时间
  • 上锁后,指定过期时间前宕机,导致 ridis 里始终有锁
    通过 setnx ,实际是 set key value nx px 毫秒数 上锁,使上锁和超时设置原子化
  • 业务没有完成但超时了,锁会消失,若另一个线程加锁,此时原线程可能误删新锁
    设置锁的时候添加一个随机信息作为值,删除前核对这个值,相当于乐观锁
  • 在上面的比较中,判断和删锁不是原子的,所以可能导致先进判断,但锁失效了并变成新锁,被误删
    可以使用 lua 脚本、key 中添加随机值、redis 事务、redisson
  • redis 主从复制造成的所丢失
    增加业务处理缓存,记录一定周期内正在处理的业务,比如三天
    加锁前判断业务是否在缓存,如果在即处理中、丢失锁等情况,需要等待其完成或失败,然后结合自动、人工工单处理
  • 重复消费时,线程刚刚处理完释放锁,另一个线程就加上锁l ,刚刚处理完的业务又处理一次
    通过幂等解决,或添加业务完成缓存,存放一定周期内处理完的业务

红锁

  • 5+ 单独部署的 redis 实例(不是一个集群)
  • 全局加锁成功条件
    • 半数以上实例加锁成功即全局加锁成功
    • 从开始加锁到全局加锁成功的时间 < 锁过期时间

实际工作中红锁使用场景较少

  • 部署成本较高
  • 服务器时间可能不同步

§2 redis 事务

特性

  • 有序隔离
    事务可以串联多个指令,与其他指令隔离,执行过程中不允许其他指令打断
  • 无隔离级别
    redis 是单线程的,没有 exec 之前事务中的指令都未被执行, exec 之后各个指令都不会被打断
    因此 redis 的事务无所谓隔离级别,或可将其视为 串行化
  • 不保证原子性
    事务中某指令执行失败后,不会回滚,其他命令依然执行

流程
redis 事务由下面流程组成

  • MULTI
    标记事务开始
    将后面的指令放入指令队列,当 EXEC 时执行指令序列
  • EXEC
    执行
  • DISCARD
    清除指令队列中的指令并回复正常的连接状态
  • WATCH / UNWATCH
    事务需要按条件执行时,此命令可以为设定 key 的受监控状态,相当于给 key 加了一个监控
    若被监控的 key 的值发生变化,则后面的事务不会执行
    WATCH key1 key2

事务示例

# 组织指令并执行
multi
set k1 v1
set k2 v2
exec
# 组织指令并撤销
multi
set k1 v1
set k2 v2
set k3 v3
discard

事务中的指令错误
指令错误分为 MULTI 阶段错误EXEC 阶段错误

  • MULTI 阶段错误,导致整个 MULTI 无法执行(不能 exec
  • EXEC 阶段错误,只导致出错误的指令执行失败

指令错误示例

multi
set k1 v1
set k2
# 下面的指令会拒绝执行,因为 multi 中 set k2 指令错误
exec

multi
set k1 v1
incr k1
set k2 v2
# exec 成功执行,上面命令中只有第二条执行失败,因为 k1 的值 v1 无法自增
exec

事务冲突
成因说明

  • redis 没有严格意义上的事务冲突
    即,redis 是单线程的,redis 的事务不能并行进行
  • redis 在现象上存在事务冲突
    • 这是因为 redis 不支持条件指令,即不支持 if(condition) then... 逻辑
    • 因此,当业务逻辑中出现上述情况时,不能将这样的逻辑片段加入事务
    • 故,只能通过其他逻辑补充,比如 java
    • 在这个过程中
      • 不同的 java 线程可能同时获取了同一个 key
      • 不同的 java 线程可能同时对 key 的当前值进行判断
      • 不同的 java 线程可能同时调用 redis 事务,并在 redis-server 端先后执行
      • 但一条 java 线程的事务成功后,另一台 java 线程先前做的判断其实已经失效了,但依然去执行了自己的事务
      • 因此出现事务冲突现象

解决方式
redis 提供了 基于乐观锁 的监视指令 watch,使用流程如下

  • 执行事务之前,对事务中需要操作的 key 加监视,可以同时监视多个 key
    WATCH key...
  • 正常执行 redis 事务
  • 释放被监视的 key
    UNWATCH key...
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值