Spring boot2.X 简单尝试 RedisTemplate 实现分布式锁

在上一篇播客 Spring boot 2.X 简单集成redis lettuce 已经简单集成的redis,客户端使用的是lettuce,接下来将尝试基于lettuce实现分布式锁。
分布式锁实现方式有多种,基于redis的分布式锁,数据库的乐观锁,基于Zookeeper的分布式锁。
我们要确保分布式锁可用,我们需要确保 :1、当一个线程获得锁之后,其他线程无法在获得。2、不会发生死锁问题。3、加锁和解锁必须是同一个线程。4、当redis只有部分节点可用时,也能确保加锁和解锁可以正常进行。

查询资料我们可以发现在Jedis组件中可以使用

jedis.set(String key, String requestId, String nxxx, String expx, int time)

第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,
分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。
requestId可以使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,
同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

基于lettuce 客户端实现示例:

public static  boolean lockSet (String key,String requestId,long expiredTime) {
		return template.execute (new RedisCallback <Boolean> () {
			@Override
			public Boolean doInRedis (
				RedisConnection redisConnection ) throws DataAccessException {
				Object nativeConnection = redisConnection.getNativeConnection();
				RedisSerializer<String> serializer = template.getStringSerializer();
				String status = null;
				
				// lettuce连接包下 redis 单机模式setnx
				if (nativeConnection instanceof RedisAsyncCommands) {
					logger.debug("lettuce single:---setKey:"+key+"---value"+requestId+"---maxTimes:"+expiredTime);
					
					status = ((RedisAsyncCommands ) nativeConnection).getStatefulConnection().sync()
						.set(serializer.serialize (key), serializer.serialize(requestId), SetArgs.Builder.nx().ex(expiredTime));
					logger.debug("lettuce single:---status:"+status);
					
				}
				
				//lettuce连接包,集群模式,ex为秒,px为毫秒
				if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
					logger.debug("lettuce Cluster:---setKey:"+key+"---value"+requestId+"---maxTimes:"+expiredTime);
					status = ((RedisAdvancedClusterAsyncCommands ) nativeConnection).getStatefulConnection().sync()
						.set(serializer.serialize (key), serializer.serialize(requestId),SetArgs.Builder.nx().ex(expiredTime));
					logger.debug("lettuce Cluster:---status:"+status);
				}
				if ( "OK".equalsIgnoreCase (status)) {
					return true;
				}
				return   false;
				
			}
		});
		
	}

(说明:代码中的template是基于上一篇博客中的实现[Spring boot 2.X 简单集成redis lettuce
上面的实现,我们在测试的时候会发现,它并不能完全满足我们真正的需求,当锁的自动失效之后,可能存在多个线程都同时获得锁的情况,我们在查询资料后得知:
setIfAbsent相当于jedis中的setnx,如果能赋值就返回true,如果已经有值了,就返回false
又尝试了下面的一种实现,

//加锁
public static boolean lockKey(String key, String value) {
		//即:在判断这个key是不是第一次进入这个方法
		if (template.opsForValue().setIfAbsent(key, value)) {
			//第一次,即:这个key还没有被赋值的时候
			return true;
		}
		String oldTime = template.opsForValue().get(key);
		if (StringUtils.isNotEmpty (oldTime)  && Long.parseLong(oldTime) < System.currentTimeMillis()) {
			String newTime = template.opsForValue().getAndSet(key, value);
			if (StringUtils.isNotEmpty(oldTime) && oldTime.equals(newTime)) {
				return true;
			}
		}
		return false;
	}
	//解锁
	public static boolean unlock(String key, String value) {
		try {
			if (StringUtils.equals (template.opsForValue().get(key),value) ){
				return template.opsForValue().getOperations().delete(key);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return  false;
	}

这种方式可以解决在锁失效之后,多个线程可能会同时获得锁的情况,但是还是存在缺陷。
在一个线程A获得锁之后,由于一些原因A执行的时间超过了锁的失效时间,那么此时A还在执行,但是其他线程因为锁已经失效了,也可以获得锁。这是一个种极端情况,但是也可能发生。我们在设计锁的失效时间时需要多方面的考虑,计算出一个合理的失效时间。可以考虑做一个守护线程来更新这个失效时间,但这种操作也不能100%避免出错,一直更新失效时间可能造成死锁的情况发生。

本文仅是个人学习时的笔记,仅供参考,如有不对之处,欢迎指出。

参考

https://www.cnblogs.com/ClareZjy/p/10448791.html

https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值