Redis分布式锁案例

我的博客文章https://affzl.xyz

分布式锁

所有服务抢占同一个公共锁,抢到后,执行业务,执行完毕释放业务。
去中心化分布式系统越来越普及,有一种不可避免的场景就是多个进程互斥的对其资源的使用,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,且保证在多进程下的数据安全,分布式锁就十分重要了。
通过执行setnx,若成功再执行expire添加过期时间的方式加锁,解锁执行delete命令。

Redis分布式锁需要满足的条件

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁也能保证后续其他客户端能加锁。
  • 同一性。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
  • 容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。

Redis实现分布式锁的几种常见方案

SETNX + EXPIRE
SETNX key value:将键名为 key、值为 value 的数据存储到 Redis 数据库中,但只有在该键不存在时才进行设置。如果该键已经存在,则该操作不会执行任何动作。
EXPIRE key seconds:为键名为 key 的数据设置过期时间,单位为秒。在指定的 seconds 秒数之后,Redis 会自动删除该键及其对应的值。
例如:
SETNX session:1234 some_value	# 若session:1234 不存在就创建
EXPIRE session:1234 60	# session:1234 60s后过期

先用setnx来抢锁,如果抢到锁,再用expire给锁设置一个过期时间,这样持有锁超时时释放锁,防止锁忘记释放。但此时setnx和expire两个命令无法保证原子性

  • 如果枪锁成功,但是设置过期时间前异常或挂掉,那么过期时间设置失败,会有死锁问题。
if(jedis.setnx(key_resource_id,lock_value) == 1{ //加锁
    expire(key_resource_id,100; //设置过期时间
    try {
        //业务代码块
    }catch() {
    }finally {
       jedis.del(key_resource_id); //释放锁
    }
}
SETNX + value(系统时间+过期时间)

把过期时间放到setnx的value值里面,解决了原子性问题。如果加锁失败,再拿出value值校验一下即可。

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()) {
     // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
    String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
         // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
         return true;
    }
}
//其他情况均返回加锁失败
return false;
通过开源框架-Redisson

Redisson就是当一个线程获得锁以后,给该线程开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
假设两个线程争夺统一公共资源:线程A获取锁,并通过哈希算法选择节点,执行Lua脚本加锁,同时其看门狗机制会启动一个watch dog(后台线程),每隔10秒检查线程,如果线程A还持有锁,那么就会不断的延长锁key的生存时间。线程B获得锁失败,就会订阅解锁消息,当获取锁到剩余过期时间后,调用信号量方法阻塞住,直到被唤醒或等待超时。一旦线程A释放了锁,就会广播解锁消息。于是,解锁消息的监听器会释放信号量,获取锁被阻塞的线程B就会被唤醒,并重新尝试获取锁。
Redisson 支持单点模式、主从模式、哨兵模式、集群模式,假设现为单点模式:

//构造Config
Config config = new Config();
config.useSingleServer().setAddress("redis://ip:port").setPassword("Password.~#").setDatabase(0);
//构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
//获取锁实例
RLock rLock = redissonClient.getLock(lockKey);
try {
    //获取锁,waitTimeout为最大等待时间,超过这个值,则认为获取锁失败。leaseTime为锁的持有时间
    boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
    if (res) {
        //业务块
    }
} catch (Exception e) {
}finally{
    //解锁
    rLock.unlock();
}

Redis的简单使用问题

set lock lock1 NX 将键名为 “lock”,值为 “lock1” 的数据存储到 Redis 数据库中,并且只有在该键不存在时才进行设置,即使用 “NX”(Not Exists)选项
直接加锁

//1、占分布式锁。去redis占坑、加锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
if (lock){
    //加锁成功  执行业务
    Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
    redisTemplate.delete("lock");
    return dataFromDb;
}

问题:如果因为一些原因没有解锁,会造成死锁
解决:设置一个过期时间,避免死锁
直接加锁+过期时间
EXPIRE lock 30

//1、占分布式锁。去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
if (lock){
    //设置过期时间
    redisTemplate.expire("lock",30, TimeUnit.SECONDS);
    //加锁成功  执行业务
    Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
    redisTemplate.delete("lock");
    return dataFromDb;
}

问题:原子性问题,如果在设置过期时间时挂了,即过期时间设置失败会造成死锁
解决:在加锁时设置过期时间,解决了原子性问题
加锁时设置过期时间
set lock lock1 EX 30 NX

//1、占分布式锁。去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1",30,TimeUnit.SECONDS);
if (lock){
    //加锁成功  执行业务
    Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
    redisTemplate.delete("lock");
    return dataFromDb;
} 

问题:在执行完毕删锁时,该线程的lock可能已经过期,其它线程又获取了lock。可能删除别的线程中的lock
解决:获取lock对比删除这几步必须是原子操作,使用lua脚本解决
使用lua脚本

//1、占分布式锁。去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS);
if (lock){
    Map<String, List<Catelog2Vo>> dataFromDb = null;
    try {
        //加锁成功  执行业务
        dataFromDb = getDataFromDb();
    }finally {
        //原子删锁
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] \n" +
                "then\n" +
                "\treturn redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end;\n";
        redisTemplate.execute(new DefaultRedisScript<Integer>(script,Integer.class)
                ,Arrays.asList("lock"),uuid);

    }
    return dataFromDb;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码不会敲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值