这次彻底读透 Redis !( 五 )

  • 安全性:锁只能被持久的客户端删除,不能被其他人删除
  • 高可用,高性能:加锁和解锁消耗的性能少,时间短
  • 锁超时:当客户端获取锁后出现故障,没有立即释放锁,该锁要能够在一定时间内释放,否则其他客户端无法获取到锁
  • 可重入性:客户端获取到锁后,在持久锁期间可以再次获取到该锁
  • 解决方案 
    方案一:SETNX 命令
    Redis 提供了一个获取分布式锁的命令 SETNX
    setnx key value
    如果获取锁成功,redis 返回 1,获取锁失败 redis 返回 0
    这次彻底读透 Redis !

    文章插图
     
    客户端使用伪代码
    if (setnx(k1,v1) == 1) { try{ // 执行逻辑 .... }catch() { }finally{ // 执行完成后释放锁 del k1; } }
    这个命令看似可以达到我们的目的,但是不符合分布式锁的特性,如果客户端在执行业务逻辑过程中,服务器宕机了,finally 中代码还没来得及执行,锁没有释放,也就意味其他客户端永远无法获取到这个锁
    方案二:SETNX + EXPIRE
    该方案获取锁之后,立即给锁加上一个过期时间,这样即使客户端没有手动释放锁,锁到期后也会自动释放
    这次彻底读透 Redis !

    文章插图
     
    我们来看下伪代码
    if (setnx(k1, v1) == 1){ expire(key, 10); try { //.... 你的业务逻辑 } finally { del(key); } }
    这个方案很完美,既可以获取到,又不用担心客户端宕机 。等等,这里面真的没有问题吗?再仔细瞅瞅,一瞅就瞅出问题来了
    if (setnx(k1, v1) == 1){ // 再刚获取锁之后,想要给锁设置过期时间,此时服务器挂了 expire(key, 10); // 这条命令没有执行 try { //.... 你的业务逻辑 } finally { del(key); } }
    这里的 setnx 命令和 expire 命令不是原子性的,他们之间执行需要一定的等等时间,虽然这个时间很短,但是依然有极小概率出现问题
    使用 Lua 脚本
    既然 setnx 和 expire 两个命令非原子性,那么我们让其符合原子性即可,通过 Lua 脚本即可实现 。Redis 使用单个 Lua 解释器去运行所有脚本,并且,Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行
    具体实现如下:
    if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) else return 0 end;
    这样应该没问题了吧,看似上面的几个问题都很好解决了 。不对,再想想,肯定还有没考虑到的
    我们再来看一段伪代码
    // 执行 lua 脚本 // 获取 k1 锁,过期时间 10 s if (execlua()==1){ try { buyGoods(); } finally { del(key); } }
    这次彻底读透 Redis !

    文章插图
     
    从图中我们可以很清晰发现问题所在
     
    1. 客户端 A 还未执行完毕,客户端 B 就获取到了锁,这样就可能导致并发问题
    2. 客户端 A 执行完毕,开始删除锁 。但此时的锁为 B 所有,相当于删除了属于客户端 B 的锁,这样肯定会发生问题
     
    方案四:SET EX PX NX + 校验唯一随机值,再删除
    既然锁有可能被别的客户端删除,那么在删除锁的时候我们加上一层校验,判断释放锁是当前客户端持有的,如果是当前客户端,则允许删除,否则不允许删除 。
     
    • EX second :设置键的过期时间为 second 秒 。SET key value EX second 效果等同于 SETEX key second value。
    • PX millisecond :设置键的过期时间为 millisecond 毫秒 。SET key value PX millisecond 效果等同于 PSETEX key millisecond value。
    • NX :只在键不存在时,才对键进行设置操作 。SET key value NX 效果等同于 SETNX key value。
    • XX :只在键已经存在时,才对键进行设置操作 。
    • 嵌入式物联网需要学的东西真的非常多,千万不要学错了路线和内容,导致工资要不上去!
     
    使用示例:
    if(jedis.set(resource_name, random_value, "NX", "EX", 100s) == 1){ //加锁, value 传入一个随机数 try { do something //业务处理 }catch(){ } finally { // 判断 value 是否相等, 相等才释放锁, 这里判断和删除是非原子性, 真实场景下可以将这两步放入 Lua 脚本中执行 if (random_value.equals(jedis.get(resource_name))) { jedis.del(lockKey); //释放锁 } } }


    推荐阅读