Java架构直通车——基于Redis的Set NX实现分布式锁

文章目录

  • 实现原理
    • SetNx的缺陷
      • 超时问题
      • 单机/多机问题
    • 实践:基于Redis的Set NX实现分布式锁
  • 基于Redisson实现分布式锁

实现原理


我们先来看获取redis锁的set命令:

SET resource_name random_value NX PX 30000

不用想得过于复杂,这里简单解释下上面这个命令:

  • SET resource_name random_value直接可以理解为SET key value,不过这里的key和value有它自己的含义。
    key:resource_name是保存的业务名称,与上文数据库中使用的bussiness_code是同样的意思,这是区分不同业务的锁。
    value:random_value则是随机的值,用于释放锁时候的校验
  • NX:表示key不存在的时候设置成功,key存在的时候设置不成功。这相当于就是锁的操作,并且是一个原子性的操作。
    我们知道Redis是一个单线程的操作,而在应用中的并发请求中,应用的多个线程打在redis上,由于redis是单线程的,并行的请求编程了串行,只会有一个线程能够设置成功,相当于请求到锁。
  • PX:给Redis里面的key设置一个过期的时间,如果出现异常,锁可以过期失效。

有上面的解释可以知道,我们主要是利用了NX的特性,多个线程并发的时候,只有一个线程设置成功,设置成功后即获得了锁,可以进行后续的业务处理。并且利用PX,如果过程中出现了异常,过了锁的有效期,锁就会自动释放。


当然有上锁,就会有释放锁。

在释放锁的时候会校验之前设置的redis的随机数,相同才能释放:这才能证明这个key的value是当前线程设置的。 这里释放锁,我们采用的是Lua脚本,因为redis的delete命令没有提供值校验的功能。

Lua脚本如下:

if redis.call("get",KEYS[1])==ARGV[1] then
	return redis.call("del",KEYS[1])
else
	return 0
end

理解起来很简单,KEYS[1]就是你的resource_name,而ARGV[1]就是程序传递过来的值,如果get到的这个值等于传递过来的值,那么就删除这个键值对。

lua脚本还具有以下优点:

  • 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
  • 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
  • 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。

SetNx的缺陷

超时问题

再来说说,为什么释放锁的时候要去做随机数值的校验。

  • 因为如果不做校验,有可能会释放到别的线程加的锁。

那么别的线程为什么能够获取到锁呢?是因为我们用了EX这个参数,设置了expire time。可以看看下图,如果不设置随机数校验会出现什么情况:
A获取到锁的时候,可能由于执行任务时间太长或者其他什么原因,锁过期了。而B获取到了锁,B在处理业务,那么此时如果A执行完了任务,去释放锁,就会释放到B现在持有到的锁。
Java架构直通车——基于Redis的Set NX实现分布式锁_第1张图片

GC 可能引发的安全问题

熟悉 Java 的同学肯定对 GC 不陌生,在 GC 的时候会发生 STW(Stop-The-World),这本身是为了保障垃圾回收器的正常执行。但是如果服务出现了 STW 且时间较长,就可能导致分布式锁进行了超时释放

单机/多机问题

如果 Redis 采用单机部署模式,那就意味着当 Redis 故障了,就会导致整个服务不可用。

而如果采用主从模式部署,我们想象一个这样的场景:服务 A 申请到一把锁之后,如果作为主机的 Redis 宕机了,那么 服务 B 在申请锁的时候就会从从机那里获取到这把锁。

关于这个问题,可以参考Java架构直通车——RedLock是否可以做分布式锁。

实践:基于Redis的Set NX实现分布式锁

我们新建项目,引入redis包:

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

配置好properties,Controller如下。

@RestController
@Slf4j
public class RedisLockController {
    @Autowired
    RedisTemplate redisTemplate;

    @RequestMapping("redisLock")
    public String redisLock(){
        log.info("我进入了方法");
        String key="Biz_code";
        String value= UUID.randomUUID().toString();
        RedisCallback<Boolean> callback=new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                //ifAbsent()就是setNX
                RedisStringCommands.SetOption setOption=RedisStringCommands.SetOption.ifAbsent();
                //过期时间30秒
                Expiration expiration=Expiration.seconds(30);
                //序列化key,value
                byte[] redis_key=redisTemplate.getKeySerializer().serialize(key);
                byte[] redis_value=redisTemplate.getValueSerializer().serialize(value);
                //执行setnx操作
                Boolean result=redisConnection.set(redis_key,redis_value,expiration,setOption);
                //返回操作结果
                return result;
            }
        };

        Boolean lock= (Boolean) redisTemplate.execute(callback);
        if (lock) {
            log.info("我进入了锁");
            //模拟任务执行15秒
            try {
                Thread.sleep(15000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //执行lua脚本,释放锁
                String script="if redis.call(\"get\",KEYS[1])==ARGV[1] then\n" +
                        "\treturn redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "\treturn 0\n" +
                        "end";
                RedisScript<Boolean> redisScript=RedisScript.of(script,Boolean.class);
                List keys= Arrays.asList(key);
                Boolean unlock= (Boolean) redisTemplate.execute(redisScript,keys,value);
                log.info("释放锁的结果:"+unlock);
            }
        }
        log.info("方法执行完成");
        return "方法执行完成";
    }
}

我们首先启动一个8080端口,运行结果如下:
Java架构直通车——基于Redis的Set NX实现分布式锁_第2张图片
结果没有问题,再启动一个8081端口,同时打两个请求。我们只查看后面一个没有获取到锁的结果:
Java架构直通车——基于Redis的Set NX实现分布式锁_第3张图片

基于Redisson实现分布式锁

Redisson提供了现成的API来实现分布式锁,可以参考:github redisson quickstart

// 4. Get Redis based Lock
RLock lock = redisson.getLock("myLock");

RLockReactive lockReactive = redissonReactive.getLock("myLock");

RLockRx lockRx = redissonRx.getLock("myLock");

在此不做赘述。

对比一下Redisson和我们自己实现的RedisLock,RedisLock没有支持阻塞。

你可能感兴趣的:(Java架构直通车)