注意:所有文章除特别说明外,转载请注明出处.
Redis - 分布式
[TOC]
分布式锁(多JVM)
控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性
实现原理
互斥性:保证同一时间只有一个客户端可以拿到锁,也就是可以对共享资源进行操作
安全性:只有加锁的服务才能有解锁权限,也就是不能让a加的锁,bcd都可以解锁,如果都能解锁那分布式锁就没啥意义了
可能出现的情况就是a去查询发现持有锁,就在准备解锁,这时候忽然a持有的锁过期了,然后b去获得锁,因为a锁过期,b拿到锁,这时候a继续执行第二步进行解锁如果不加校验,就将b持有的锁就给删除了
避免死锁:出现死锁就会导致后续的任何服务都拿不到锁,不能再对共享资源进行任何操作了
保证加锁与解锁操作是原子性操作
这个其实属于是实现分布式锁的问题,假设a用redis实现分布式锁,假设加锁操作,操作步骤分为两步:1. 设置key set(key,value)。2. 给key设置过期时间,假设现在a刚实现set后,程序崩了就导致了没给key设置过期时间就导致key一直存在就发生了死锁。
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
互斥性:在任意时刻,只有一个客户端能持有锁
不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁
解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解
如何实现分布式锁?
使用redis实现分布式锁
set key value ex time nx
set()多参数,高版本jedis使用
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
\* 尝试获取分布式锁
\* @param jedis Redis客户
\* @param lockKey 锁
\* @param requestId 请求标识
\* @param expireTime 超期时间
\* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = **jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);**
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
第一个为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()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端
1 | public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { |
解锁
1 | public class RedisTool { |
错误解锁
1 | public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) { |
存在问题:问题在于如果调用
jedis.del()
方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()
之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了
setnx + 过期时间依旧存在问题
在高并发场景下,会出现误删不是自己的锁,删除掉别人的锁,出现一个锁永久性失效的问题
使用uuid解决,判断clientId跟redis中那把锁的value值是否一致,一致的话,才能进行删除锁。但还会出现有多个线程执行同一个代码块,对超时的锁进行续命,那么可以尝试使用==Redisson==实现分布式锁,这是Redis官方提供的Java组件。
引入依赖
在启动类中注入 redisson 的bean,可以在配置文件中添加redis的连接地址、参数,然后在代码中引入配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RedissonApplication {
public static void main(String[] args) {
SpringApplication.run(RedissonApplication.class, args);
}
Redisson redissonSentinel() {
//支持单机,主从,哨兵,集群等模式
//此为哨兵模式
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("redis://192.168.1.1:26379")
.setPassword("123456");
return (Redisson)Redisson.create(config);
}
}在redis中,设置一个库存数量为100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void test() throws Exception {
//设置一个key,aaa商品的库存数量为100
stringRedisTemplate.opsForValue().set("aaa","100");
Assert.assertEquals("100", stringRedisTemplate.opsForValue().get("aaa"));
}
String lockKey = "testRedisson";//分布式锁的key
public void testDistributed(){
//执行的业务代码
for(int i=0; i < 55; i++){
RLock lock = redisson.getLock(lockKey);
lock.lock(60, TimeUnit.SECONDS); //设置60秒自动释放锁 (默认是30秒自动过期)
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("aaa").toString());
if(stock > 0){
stringRedisTemplate.opsForValue().set("aaa",(stock-1)+"");
System.out.println("test2_:lockkey:"+lockKey+",stock:"+(stock-1)+"");
}
lock.unlock(); //释放锁
}
}