美文网首页
Redis的Setnx命令实现分布式锁

Redis的Setnx命令实现分布式锁

作者: 砒霜拌辣椒 | 来源:发表于2020-08-13 23:34 被阅读0次

    首先,分布式锁和我们平常讲到的锁原理基本一样,目的就是确保在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法、变量。

    在一个进程中,也就是一个jvm或者说应用中,我们很容易去处理控制,在java.util并发包中已经为我们提供了这些方法去加锁,比如synchronized关键字或者Lock锁,都可以处理。

    但是如果在分布式环境下,要保证多个线程同时只有1个能访问某个资源,就需要用到分布式锁。这里我们将介绍用Redis的setnx命令来实现分布式锁。

    其实目前通常所说的setnx命令,并非单指redis的setnx key value这条命令,这条命令可能会在后期redis版本中删除。

    一般代指redis中对set命令加上nx参数进行使用,set这个命令,目前已经支持这么多参数可选:

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

    从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:

    • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
    • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
    • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
    • XX :只在键已经存在时,才对键进行设置操作。

    1、示例

    注入bean

    @Bean
    public ValueOperations<String, String> stringOperations(StringRedisTemplate redisTemplate) {
        return redisTemplate.opsForValue();
    }
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @Slf4j
    public class SetnxTest {
        @Autowired
        private ValueOperations<String, String> stringOperations;
    
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
    
        @Test
        public void testSetnx() {
            // 同时并发5个线程竞争去获取锁
            IntStream.range(0, 5).parallel().forEach(i -> {
                String uuid = IdUtil.randomUUID();
                Boolean lock = stringOperations.setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
                log.info("是否获取锁:[{}]", lock);
                if (!lock) return;
                // 获得锁后的操作
                // ...
    
                // 最后释放锁
                redisTemplate.delete("lock");
            });
        }
    }
    

    这里同时启动5个线程并发往redis中存储lock这个key(key可以自定义,但需要一致),同时设置10秒的过期时间。
    setIfAbsent这个函数实现的功能与setnx命令一样,代表如果没有这个key则set成功获取到锁,否则set失败没有获取到锁。
    获得锁后进行资源的操作,最后释放锁。

    执行效果

    2020-08-13 22:23:48,200 [INFO] [ForkJoinPool.commonPool-worker-9] [net.zhaoxiaobin.redistemplate.SetnxTest:31] [] 是否获取锁:[true]
    2020-08-13 22:23:48,278 [INFO] [ForkJoinPool.commonPool-worker-4] [net.zhaoxiaobin.redistemplate.SetnxTest:31] [] 是否获取锁:[false]
    2020-08-13 22:23:48,278 [INFO] [ForkJoinPool.commonPool-worker-2] [net.zhaoxiaobin.redistemplate.SetnxTest:31] [] 是否获取锁:[false]
    2020-08-13 22:23:48,278 [INFO] [main] [net.zhaoxiaobin.redistemplate.SetnxTest:31] [] 是否获取锁:[false]
    2020-08-13 22:23:48,279 [INFO] [ForkJoinPool.commonPool-worker-11] [net.zhaoxiaobin.redistemplate.SetnxTest:31] [] 是否获取锁:[false]
    

    可以看到同时只有1个线程能够获取到锁。

    2、总结

    使用setnx命令方式虽然操作比较简单方便,但是会有如下问题:

    1.当前线程如果获得了锁就不能再重入了(在锁释放前不能再获得锁)。

    可以在再次获取锁时,如果锁被占用就get值,判断值是否是当前线程存的随机值,如果是则再次执行set命令重新上锁;当然为了保证原子性这些操作都要用lua脚本来执行。

    2.获取锁方式非阻塞,其它线程没有获取到锁就只能失败返回了,只适合于个别业务场景。

    可以使用while循环重复执行setnx命令,并设置一个超时时间退出循环。

    3.锁过期时间如果设置太短或者拿到锁的线程执行完方法耗时特别长,方法没等执行完,锁就自动释放了,然后别的线程也能拿到锁,导致不安全的操作。类似的还有几种情况:

    因为网络延迟,等拿到锁时,锁已经过期了,可以设置超时时间远小于锁自动过期时间来解决。

    程序拿到锁后进入GC导致程序暂定,恢复后锁过期了。

    Redis所在的机器发生了时钟漂移(Clock Drift),时间向前跳跃导致锁提前过期。

    可以尽量把锁自动过期的时间设的冗余一些。但也不能彻底解决。

    4.锁自动过期后,第二个线程拿到锁开始操作,第一个拿到锁的等执行完回手就把别人的锁给删了,那么就会产生并发安全问题。

    可以在删除锁的时候先get值,判断值是否是当前线程存的随机值,只有相同才执行删锁的操作;当然也要使用lua脚本执行来保证原子性。

    5.在Cluster集群模式下,如果redis出现故障,进行主从切换,这个时候主机的锁如果还未同步到从机上,就会导致其他线程同样也可以获取到锁。

    分布式锁需要满足的特性

    • 互斥性。
    • 锁超时。
    • 支持阻塞和非阻塞。
    • 可重入性。
    • 高可用。

    综上:使用setnx命令来实现分布式锁并不是一个很严谨的方案,如果是Java技术栈,我们可以使用Redisson库来解决以上问题,接下来的文章会介绍如何使用。

    Redisson实现分布式锁
    Redlock实现分布式锁

    参考链接

    相关文章

      网友评论

          本文标题:Redis的Setnx命令实现分布式锁

          本文链接:https://www.haomeiwen.com/subject/mudwdktx.html