您好,欢迎来到尚车旅游网。
搜索
您的当前位置:首页redis分布式锁实现方法介绍

redis分布式锁实现方法介绍

来源:尚车旅游网

幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:

C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0

C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。

反之,如果已超时,C3通过下面的操作来尝试获得锁:

GETSET lock.foo <current Unix time + lock timeout + 1>

通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。

如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。

注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。

五、代码实现

expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放
timeoutMsecs 锁等待超时,防止线程饥饿,永远没有入锁执行代码的机会

注意:项目里面需要先搭建好redis的相关配置

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis distributed lock implementation.
 *
 * @author zhengcanrui
 */
public class RedisLock {

 private static Logger logger = LoggerFactory.getLogger(RedisLock.class);

 private RedisTemplate redisTemplate;

 private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

 /**
 * Lock key path.
 */
 private String lockKey;

 /**
 * 锁超时时间,防止线程在入锁以后,无限的执行等待
 */
 private int expireMsecs = 60 * 1000;

 /**
 * 锁等待时间,防止线程饥饿
 */
 private int timeoutMsecs = 10 * 1000;

 private volatile boolean locked = false;

 /**
 * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs.
 *
 * @param lockKey lock key (ex. account:1, ...)
 */
 public RedisLock(RedisTemplate redisTemplate, String lockKey) {
 this.redisTemplate = redisTemplate;
 this.lockKey = lockKey + "_lock";
 }

 /**
 * Detailed constructor with default lock expiration of 60000 msecs.
 *
 */
 public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs) {
 this(redisTemplate, lockKey);
 this.timeoutMsecs = timeoutMsecs;
 }

 /**
 * Detailed constructor.
 *
 */
 public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {
 this(redisTemplate, lockKey, timeoutMsecs);
 this.expireMsecs = expireMsecs;
 }

 /**
 * @return lock key
 */
 public String getLockKey() {
 return lockKey;
 }

 private String get(final String key) {
 Object obj = null;
 try {
 obj = redisTemplate.execute(new RedisCallback<Object>() {
 @Override
 public Object doInRedis(RedisConnection connection) throws DataAccessException {
 StringRedisSerializer serializer = new StringRedisSerializer();
 byte[] data = connection.get(serializer.serialize(key));
 connection.close();
 if (data == null) {
 return null;
 }
 return serializer.deserialize(data);
 }
 });
 } catch (Exception e) {
 logger.error("get redis error, key : {}", key);
 }
 return obj != null ? obj.toString() : null;
 }

 private boolean setNX(final String key, final String value) {
 Object obj = null;
 try {
 obj = redisTemplate.execute(new RedisCallback<Object>() {
 @Override
 public Object doInRedis(RedisConnection connection) throws DataAccessException {
 StringRedisSerializer serializer = new StringRedisSerializer();
 Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
 connection.close();
 return success;
 }
 });
 } catch (Exception e) {
 logger.error("setNX redis error, key : {}", key);
 }
 return obj != null ? (Boolean) obj : false;
 }

 private String getSet(final String key, final String value) {
 Object obj = null;
 try {
 obj = redisTemplate.execute(new RedisCallback<Object>() {
 @Override
 public Object doInRedis(RedisConnection connection) throws DataAccessException {
 StringRedisSerializer serializer = new StringRedisSerializer();
 byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
 connection.close();
 return serializer.deserialize(ret);
 }
 });
 } catch (Exception e) {
 logger.error("setNX redis error, key : {}", key);
 }
 return obj != null ? (String) obj : null;
 }

 /**
 * 获得 lock.
 * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.
 * reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)
 * 执行过程:
 * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
 * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
 *
 * @return true if lock is acquired, false acquire timeouted
 * @throws InterruptedException in case of thread interruption
 */
 public synchronized boolean lock() throws InterruptedException {
 int timeout = timeoutMsecs;
 while (timeout >= 0) {
 long expires = System.currentTimeMillis() + expireMsecs + 1;
 String expiresStr = String.valueOf(expires); //锁到期时间
 if (this.setNX(lockKey, expiresStr)) {
 // lock acquired
 locked = true;
 return true;
 }

 String currentValueStr = this.get(lockKey); //redis里的时间
 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
 //判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
 // lock is expired

 String oldValueStr = this.getSet(lockKey, expiresStr);
 //获取上一个锁到期时间,并设置现在的锁到期时间,
 //只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
 if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
 //防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受

 //[分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
 // lock acquired
 locked = true;
 return true;
 }
 }
 timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

 /*
 延迟100 毫秒, 这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
 只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
 使用随机的等待时间可以一定程度上保证公平性
 */
 Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);

 }
 return false;
 }


 /**
 * Acqurired lock release.
 */
 public synchronized void unlock() {
 if (locked) {
 redisTemplate.delete(lockKey);
 locked = false;
 }
 }

}

调用:

RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000);
 try {
 if(lock.lock()) {
 //需要加锁的代码
 }
 }
 } catch (InterruptedException e) {
 e.printStackTrace();
 }finally {
 //为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,
 //操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。 ————这里没有做
 lock.unlock();
 }

六、一些问题

1、为什么不直接使用expire设置超时时间,而将时间的毫秒数其作为value放在redis中?

如下面的方式,把超时的交给redis处理:

lock(key, expireSec){
isSuccess = setnx key
if (isSuccess)
expire key expireSec
}

这种方式貌似没什么问题,但是假如在setnx后,redis崩溃了,expire就没有执行,结果就是死锁了。锁永远不会超时。

2、为什么前面的锁已经超时了,还要用getSet去设置新的时间戳的时间获取旧的值,然后和外面的判断超时时间的时间戳比较呢?

1.jpg

因为是分布式的环境下,可以在前一个锁失效的时候,有两个进程进入到锁超时的判断。如:

C0超时了,还持有锁,C1/C2同时请求进入了方法里面

C1/C2获取到了C0的超时时间

C1使用getSet方法

C2也执行了getSet方法

假如我们不加 oldValueStr.equals(currentValueStr) 的判断,将会C1/C2都将获得锁,加了之后,能保证C1和C2只能一个能获得锁,一个只能继续等待。

注意:这里可能导致超时时间不是其原本的超时时间,C1的超时时间可能被C2覆盖了,但是他们相差的毫秒及其小,这里忽略了。

更多redis知识请关注redis入门教程栏目。

Copyright © 2019- sceh.cn 版权所有 湘ICP备2023017654号-4

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务