Redis分布式锁的简单实现

Posted 风在哪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis分布式锁的简单实现相关的知识,希望对你有一定的参考价值。

Redis分布式锁

随着业务发展的需要,原来单机部署的系统演化成分布式集群系统之后,由于分布式系统多线程、多进程并且分布在不同的机器上,使得原来单机部署情况下的并发控制策略失效,单纯的Java API并不能提供分布式锁的能力,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

也就是说,当我们使用分布式锁时,该锁多分布式系统内的所有应用都有效。

分布式锁的主流实现方案有多种,包括基于数据库实现的分布式锁、基于缓存(如redis)实现(性能最高)、基于zookeeper实现(可靠性最高)。本节主要讲基于redis实现的分布式锁。

setnx命令实现分布式锁

setnx key value

setnx只有当key不存在的情况下,才将key的值设置为value,若key已经存在,那么setnx命令不做任何操作。

例如:

image-20210503212302934

当我们初次调用setnx命令设置lock的值时,lock还不存在,所以可以设置成功,后续再调用setnx设置相同的key,也就是lock时,却无法设置成功,这就是setnx的作用。

其实setnx就是[SET if Not eXists(如果不存在则SET)]的简写。

那么加锁之后我们如何释放锁呢,此时就需要调用del命令删除key,以此来释放锁。

image-20210503212618173

也就是将对应的key删除掉就释放了相应的锁了。

所以,redis实现分布式锁需要setnx和del这两个指令来完成加锁和解锁。

相关问题

1、当我们加锁以后忘记释放锁,那么其他应用将一直无法获取到锁,这是就产生了问题。那么这个问题怎么解决呢?

我们可以在加锁之后设置锁的存活时间,存活时间到达之后,锁会自动失效,我们无需手动删除key。

例如:

image-20210503212953516

此时我们就可以放心使用分布式锁了。

2、上面虽然实现了加锁以及设置过期时间,但是如果我们加锁以后,设置过期时间之前,redis突然宕机了,没有执行expire指令,当redis重启以后,该锁还是会一直存在,所以我们需要使用原子操作来加锁并且设置过期时间,那么可以通过一条指令搞定:

set lock yes nx ex 20

image-20210503213206888

这样我们就可以放心使用redis分布式锁啦!

redis分布式锁优化-UUID防误删

首先我们来看看redis分布式锁的流程:

  1. 加锁并设置过期时间
  2. 进行业务处理
  3. del操作释放锁

假如此时我们的A服务在第2步由于机器的原因卡住了,并且卡顿的时间超过了我们设置的过期时间。此时,B服务获得了相同的分布式锁。

此时A服务从卡顿状态恢复,并且处理完业务,继续执行了del操作,那么此时del操作删除的是B服务的分布式锁,这会导致一系列的问题。

这种问题如何解决呢,其实解决的方式很简单:

  1. 加锁并设置过期时间,加锁时需要设置锁的值为UUID(其实不是UUID也可以,只要不同的服务产生的值不同即可,主要作用就是标识不同的加锁对象)
  2. 进行业务处理
  3. 首先获取锁的值,判断是否与自身的UUID值相等,如果相等则证明这是我们加的分布式锁,可以进行删除,否则就不能删除,因为这是其他服务加的锁。

具体代码很简单,这里不再展示啦!

redis分布式锁优化-原子性操作实现锁的删除操作

其实我们使用UUID删除分布式锁的话还是存在一定的问题:

当A服务判断了分布式锁的UUID与自身的UUID值相等,那就准备进行删除操作,但是此时,分布式锁正好过期,B服务获得了这个分布式锁,A服务还是会进行del操作删除分布式锁,那么又成了删除B服务的分布式锁。这里又产生了问题,那么我们如何解决呢?

我们可以通过原子性操作来解决这个问题。

通过如下代码可以使用Lua脚本实现原子操作:

@GetMapping("testLockLua")
public void testLockLua() {
    //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
    String uuid = UUID.randomUUID().toString();
    //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
    String locKey = "lock"; // 锁住的是每个商品的数据

    // 3 获取锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);

    // 第一种: lock 与过期时间中间不写任何的代码。
    // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
    // 如果true
    if (lock) {
        // 执行的业务逻辑开始
        // 获取缓存中的num 数据
        Object value = redisTemplate.opsForValue().get("num");
        // 如果是空直接返回
        if (StringUtils.isEmpty(value)) {
            return;
        }
        // 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
        int num = Integer.parseInt(value + "");
        // 使num 每次+1 放入缓存
        redisTemplate.opsForValue().set("num", String.valueOf(++num));
        /*使用lua脚本来锁*/
        // 定义lua 脚本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 使用redis执行lua执行
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        // 设置一下返回值类型 为Long
        // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
        // 那么返回字符串与0 会有发生错误。
        redisScript.setResultType(Long.class);
        // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
        redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
    } else {
        // 其他线程等待
        try {
            // 睡眠
            Thread.sleep(1000);
            // 睡醒了之后,调用方法。
            testLockLua();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

总结

综上所述,要想完美的使用redis分布式锁,我们需要如下几步:

  1. 加锁并设置过期时间,设置锁的值为UUID,防止误删
  2. 进行业务的处理
  3. 删除时使用Lua脚本进行事务的原子性操作,防止误删其他服务的锁

参考:

【尚硅谷】2021 最新 Redis 6 入门到精通 超详细 教程

以上是关于Redis分布式锁的简单实现的主要内容,如果未能解决你的问题,请参考以下文章

单实例redis分布式锁的简单实现

基于Redis的分布式锁的简单实现

分布式锁的两种实现方式(基于redis和基于zookeeper)

分布式锁的两种实现方式(基于redis和基于zookeeper)

redis 分布式锁的简单使用

redis 分布式锁的简单使用