美文网首页
spring-data-redis实现分布式锁

spring-data-redis实现分布式锁

作者: 别赶路_去感受路 | 来源:发表于2018-12-09 16:22 被阅读0次

    Redis由于数据存储在内存(随机访问内存要比硬盘快100万倍),下面是Google 工程师Jeff Dean在分布式系统PPT中给出的各种访问速度参考值:

    访问方式 耗时
    L1 cache reference 读取CPU的一级缓存 ~0.5 ns
    L2 cache reference 读取CPU的二级缓存 ~7ns
    Mutex lock/unlock 互斥锁\解锁 ~100 ns
    Main memory reference 读取内存数据 ~100 ns
    Compress 1K bytes with Zippy 1k字节压缩 ~10,000 ns
    Send 2K bytes over 1 Gbps network 在1Gbps的网络上发送2k字节 ~20,000 ns
    Read 1 MB sequentially from memory 从内存顺序读取1MB ~250,000 ns
    Round trip within same datacenter 从一个数据中心往返一次,ping一下 ~500,000 ns
    Disk seek 磁盘搜索 ~10,000,000 ns
    Read 1 MB sequentially from network 从网络上顺序读取1兆的数据 ~10,000,000 ns
    Read 1 MB sequentially from disk 从磁盘里面读出1MB ~30,000,000 ns
    Send packet CA->Netherlands->CA 一个包的一次远程访问 ~150,000,000 ns

    由于基于内存随机访问效率极高,所以Redis设计为单线程操作,确保了线程安全。由于其线程安全,访问效率极高,但内存的存储空间较小,在实际项目应用中通常用来实现分布式锁和缓存。

    下面我们看一下如何利用spring-data-redis提供的接口实现redis分布式锁。

    一、SETNX设置KV并设置超时时间实现分布式锁

    从spring-data-redis源码中可以看到setIfAbsent底层就是调用了Redis的SETNX命令,setIfAbsent方法的源码如下:

    public Boolean setIfAbsent(K key, V value) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawValue = rawValue(value);
    
        return execute(new RedisCallback<Boolean>() {
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.setNX(rawKey, rawValue);
            }
        }, true);
    }
    

    继续查看connections.setNX, 最终是调用了BinaryClient的setnx方法

      public void setnx(final byte[] key, final byte[] value) {
        sendCommand(SETNX, key, value);
      }
    

    在调用了setIfAbsent方法,然后调用expire设置key的过期时间,获取锁的代码如下:

    import org.springframework.data.redis.core.StringRedisTemplate;
    
    @Autowired
    public StringRedisTemplate stringRedisTemplate;
    
    public boolean lock(String key, String lock, int timeout){
            boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, lock);
            if(!flag){
                if(LOG.isDebugEnabled()){
                    LOG.debug("Don't get redis lock.[{}]", getName());
                }
                return flag;
            }
            /**
             * 拿到锁设置锁key的超时时间
             */
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
            if(LOG.isDebugEnabled()){
                LOG.debug("Get redis lock.[{}]", getName());
            }
            return flag;
    }
    

    二、SETNX设置kv(v为系统当前时间+超时时间)和GETSET(设置一个值并返回旧值)实现分布式锁

    这种方式和第一种不同的是没有设置过期时间,而是将setNx的value值设置为系统当前+过期时间。

    public boolean lock(String key, int timeout){
            //锁的值为当前时间+超时时间
            long lockValue = System.currentTimeMillis() + timeout;
            if(stringRedisTemplate.opsForValue().setIfAbsent(key, String.valueOf(lockValue))) {
                return true;
            }
            //其他人获取不到锁执行如下代码
            //获取锁的值
            String currentLockValue = stringRedisTemplate.opsForValue().get(key);
    
            //锁的值小于当前时则锁已过期
            if (!StringUtils.isEmpty(currentLockValue) && Long.parseLong(currentLockValue) < System.currentTimeMillis()) {
                //getAndSet线程安全所以只会有一个线程重新设置锁的新值
                String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, String.valueOf(lockValue));
                //比较锁的getSet获取到的最近锁值和最开始获取到的锁值,如果不相等则证明锁已经被其他线程获取了。
                if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentLockValue)) {
                    return true;
                }
            }
            return false;
    }
    

    假设有A、B、C两个服务实例去获取锁,由于setIfAbsent是线程安全的所以同一时间只有一个服务能获取到锁,假设为A,那么B、C去获取锁时就一定会执行第一个if之后的代码,先获取到当前锁的值。

    String currentLockValue = stringRedisTemplate.opsForValue().get(key);
    
    • 如果值为空或者值大于系统当前则证明锁还没有超时,这种情况直接B、C都会return false。
    • 如果值不为空且值小于系统当前时间,则证明锁已经过期,B、C通过getSet获取上一个锁的值,由于getAndSet是线程安全的,所以B、C只有一个去执行getSet操作设置新的锁的值为当前时间+超时时间。
    • 当B执行完getSet之后C再执行getSet获取到的oldValue就不会等于C开始获取的currentLockValue
    if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentLockValue)) {
         return true;
    }
    

    此时C不满足上面这个代码的if条件return false。则B获得锁。

    总结

    1. 第一种方式:当执行完isetIfAbsent方法之后服务中断或宕机等就会导致该key没有设置过期时间,由于永远不过期导致分布式环境下的所有应用服务都获取不到该锁,最终死锁。尤其是在开发调试环境下,由于经常启停服务很容易出现这种情况。

    2. 第二种方式:要比第一种方式稳妥得多,但是也需要考虑A、B、C三个服务器上的系统时间问题,如果时间存在差异,如果A的系统时间比BC快很多的情况下,A获得锁之后,BC永远看到锁未过期永远获取不到锁。同样对于超大型的分布式应用在部署时还要考虑跨时区问题,当然在部署架构上应该避免这种情况,对于不同时区的服务应该使用不同的redis集群,服务和redis应该部署在一个时区中环境中。

    3. 结合业务场景合理的考虑超时时间的大小设置。

    参考资料:

    https://blog.csdn.net/jpc00939/article/details/79259242

    相关文章

      网友评论

          本文标题:spring-data-redis实现分布式锁

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