redis分布式锁

redis分布式锁

参考:https://blog.csdn.net/fengyuyeguirenenen/article/details/123752418

redis分布式锁由来:

单机版的服务使用synchronize和lock是没问题的

如果一上集群,每个服务各自加锁的对象是不一样的,这时我们加的锁就不在各自的服务里,需要借助外力,redis进场。

秒杀下单啥的,抢红包都得用分布式锁

redis分布式锁_第1张图片

####方案一:SETNX+EXPIRE实现

缺点:将setnx和expire两个命令分开了,不是原子操作。

如果执行完setnx加锁之后,expire之前,进程崩了,那么这个锁就一直存在了,别的进程会永远获取不到锁

if(jedis.setnx(key_resource_id,lock_value) == 1{ //加锁
    expire(key_resource_id,100; //设置过期时间
    try {
        do something  //业务请求
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}
方案二:SETNX+VALUE(value是系统时间+过期时间)实现

为了解决上面的setnx后,expire前的锁释放不了问题,可以利用上面的setnx时候的value值

将value值赋值为当前系统时间+过期时间

当产生了上面的锁永久存在情况后,后面的进程获取不到锁,这时就在上面的setnx步骤后面加个判断,获取当前锁的内容get lock,然后手动判断这个锁是否已经过期(即校验value).

  • 下面的代码还考虑了多线程同时进行新的赋值操作,最后还进行了一步对比,可以好好理解一下
  • 仅当前线程设置的时间与当前redis获取到的时间完全相同,才能保证这段时间里,没有其他线程影响该锁

缺点:

1.过期时间是由客户端自己生成的,在分布式的情况下,每个客户端的时间必须同步

2.如果锁过期的情况下,并发的多个客户端同事请求过来,都执行getSet方法,最后只能有一个客户端加锁成功(这一点代码其实也特意处理过了)

3.该锁没有保存持有者的唯一标识,可能会被别的客户端释放

long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);
 
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
        return true;
} 
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);
 
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
 
     // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
    String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
    
    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
         // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
         return true;
    }
}
        
//其他情况,均返回加锁失败
return false;
}
方案三:使用Lue脚本(即方案一的原子版本)

这个就是方案一的一摸一样,只不过用了脚本,是两个变成了原子操作

Lua是redis2.6版本最大的亮点,解决了redis之前只能分别操作的缺点,原子化多个操作

原谅我确实看不懂。。。。

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;
 String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            " redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";   
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);
方案四:SET的拓展命令实现(SET KEY VALUE EX PX NX XX)

这个也相当于完善了方案一,用set的拓展命令完成了原子性,因为这个set的命令也是能满足原子性的。

SET key value [EX seconds] [PX milliseconds] [NX |XX]

  • NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
  • EX seconds :设定key的过期时间,时间单位是秒。
  • PX milliseconds: 设定key的过期时间,单位为毫秒
  • XX: 仅当key存在时设置值

缺点:

  1. 锁过期了,但是业务还没有执行完(假设现在是线程a获取到了锁,锁的过期时间是100,但是100s后线程a的任务还没有完成,这时线程b来获取这个锁,就能获取到了。。。)
  2. 锁被别的线程误删(假设线程a执行完后,去释放锁,但是这里会产生一种情况,这个锁的过期时间是50s,线程a需要100s才完成,所以在50s后其实就是线程b拥有了这个锁,然后到了100s的时候,线程a不知道锁已经过期了,进行释放锁的操作,delete了key,就相当于释放了线程b的锁)
if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1{ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}
方案五:SET KEY VALUE (EX PX NX XX)+校验唯一随机值,再释放锁

为了完善上一条的锁可能会被其他线程删除(上一条的缺点2),我们给value设置一个标记当前线程的唯一随机数,删除的时候进行校验呗

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1{ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       //判断是不是当前线程加的锁,是才释放
       if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
        }
    }
}

但是上面的finally里面的判断操作和删除操作也不是原子操作,所以还得用lua脚本让他变成原子操作。

redis分布式锁_第2张图片

方案六:Redisson开源框架实现(看门狗)

上一个的操作还会有【锁过期释放,业务没有执行完】的问题(方案4的缺点一)

我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每个一点时间检查锁是否还在,存在就对锁的过期时间延长,防止锁过期提前释放

核心就是只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,每隔10s检查一下,如果线程1还持有锁,就对锁的过期时间延长,防止锁过期提前释放。

redis分布式锁_第3张图片

  • 具体实现可见:https://blog.csdn.net/qq_36602071/article/details/126039824
方案七:多机实现的分布式锁RedLock

前面六种都是基于单机版的讨论,但是redis一般都是集群部署

所以当面对集群部署的时候,假如现在主节点有锁后,这时还没有往slave节点同步锁前,主节点挂了,这里重新选举了新master,丢失了锁,这样其他线程就能获取到该锁,显然是不行的

  • 没尝试过。

你可能感兴趣的:(redis,redis,分布式,数据库)