Redis实现分布式锁

已经有了实现redis分布式锁的第三方包,比如redisson就有各种锁,这篇主要研究下实现redis分布式锁的思路。
当我们的系统是单机时,可以使用synchronized/CAS来控制数据共享问题,但是当系统做大做强了,单机顶不住了,则需要加机器,这时候还用synchronized/CAS来处理数据共享问题就不行了,这时候就可以借助redis来实现。
系统变迁
我们知道redis有个setnx命令:set一个值,如果存在则返回false,并且不set进去,如果不存在则set进去,并且返回true;根据这特性我们可以用该命令来设置lock。

  • 样例1:
@RestController
@RequestMapping("/redis")
public class RedisController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/redisTest")
    public String redisTest(){

        // 模拟 统计接口访问次数
        String lock = "clickTimeLock";
        // 尝试去获取锁
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock, "true");
        if (!isLock){
            return "正在被其他系统使用中,稍后再试";
        }
        // 获取到锁了,执行业务逻辑
        try{
            //访问次数
            String clickTimes = stringRedisTemplate.opsForValue().get("clickTimes");
            int times = Integer.parseInt(clickTimes) + 1;
            stringRedisTemplate.opsForValue().set("clickTimes", String.valueOf(times));
            System.out.println(times);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 释放锁
            stringRedisTemplate.delete(lock);
        }
        return "统计成功";
    }
}

从这段代码里会发现一个问题:
系统a获取了锁,突然间挂掉宕机了,则锁一直保留在redis里没有被释放掉(走不到finally块里)。
那我们就可以根据业务情况设置一个超时时间。

  • 样例2
// 模拟 统计接口访问次数
String lock = "clickTimeLock";
// 尝试去获取锁, 设置一个锁过时的超时时间,超时了就释放掉
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock, "true", 5, TimeUnit.SECONDS);
if (!isLock){
    return "正在被其他系统使用中,稍后再试";
}

又有一个问题:超时时间设置多少比较好?

  • 场景一:业务执行需要5s,我们设置6s的超时时间,当系统线程卡顿时超过了6s,则会超时锁释放,被其他线程加锁或删锁,但是实际上原来的线程还没有结束业务操作,这会造成数据安全问题。
  • 场景二:业务执行需要5s,我们设置60s的超时时间,当系统宕机时,其他的系统线程要想操作数据,得等待50多秒的时间,对一个高并发的系统来说是不可接受的。

这样一看,我们只能接受场景一的情况,那就会出现下面的问题:
场景问题
要解决这样的问题,就需要不能让其他线程删了自己的线程,给自己的锁加个uuid。

  • 样例3
@RequestMapping("/redisTest")
public String redisTest(){

    // 模拟 统计接口访问次数
    String lock = "clickTimeLock";
    // 给自己的锁加个uuid的值,这样就防止被修改了
    String uuid = UUID.randomUUID().toString();
    // 尝试去获取锁, 设置一个锁过时的超时时间,超时了就释放掉
    Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 6, TimeUnit.SECONDS);
    if (!isLock){
        return "正在被其他系统使用中,稍后再试";
    }
    // 执行业务逻辑
    try{
        //访问次数
        String clickTimes = stringRedisTemplate.opsForValue().get("clickTimes");
        int times = Integer.parseInt(clickTimes) + 1;
        stringRedisTemplate.opsForValue().set("clickTimes", String.valueOf(times));
        System.out.println(times);
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        // 释放锁,判断是不是自己的uuid, 但是这块代码不是原子性的
        if (uuid.equals(stringRedisTemplate.opsForValue().get(lock))){
            stringRedisTemplate.delete(lock);
        }
    }
    return "统计成功";
}

这样的情况下,我们还是需要处理超时导致的锁失效问题

  • 样例4
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 6, TimeUnit.SECONDS);
if (!isLock){
    return "正在被其他系统使用中,稍后再试";
}
// 加锁成功了,起个子线程,定时查看主线程状态,还在使用中则延迟过时时间
// 代码省略,可直接查看redisson的源码

// 执行业务逻辑

到这里,应该就能满足并发不是很高的需求应用了。

redisson里的实现

来看下redisson里是如何实现分布式锁的,代码改造如下:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.17.1</version>
</dependency>
@Autowired
private Redisson redisson;

@RequestMapping("/redisTest")
public String redisTest(){

    // 模拟 统计接口访问次数
    String lockName = "clickTimeLock";
    RLock lock = redisson.getLock(lockName);
    lock.lock();
    // 执行业务逻辑
    try{
        //访问次数
        String clickTimes = stringRedisTemplate.opsForValue().get("clickTimes");
        int times = Integer.parseInt(clickTimes) + 1;
        stringRedisTemplate.opsForValue().set("clickTimes", String.valueOf(times));
        System.out.println(times);
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        // 释放锁
        lock.unlock();
    }
    return "统计成功";
}

先看一下lock获取锁的逻辑代码:
y1y2
y3
y4
y5
到此可以发现redisson加锁是通过lua脚步实现的原子性,里面也有超时的设置。
当你查看unlock逻辑时,也是使用的lua脚本实现的原子性,而且会发现通过线程id来解锁的,而锁值是通过uuid来实现的。
y6
y7
上面有个加锁成功后开启子线程去续命锁,省略了代码,来看看redisson的代码:

在这里插入图片描述

加锁成功后异步开个schedule
在这里插入图片描述
在这里插入图片描述
可以看到里边弄了个定时任务来处理超时时间。
里面还有一些其他线程请求获取锁,通过信号量来等待唤醒等操作,有兴趣可以自行查看下。
redisson里还有很多锁的实现:
redisson锁
比如搭建高可用redis集群时,可用红锁来处理,记录就到此了,菜鸟一个,继续努力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值