spring-data-redis实现分布式锁

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://www.jianshu.com/p/f0b510face8d
https://blog.csdn.net/jpc00939/article/details/79259242

你可能感兴趣的:(redis)