在开始提到Redis分布式锁之前,先说一下redis中的两个命令。
SETNX key value
setnx 是SET if Not eXists(如果不存在,则 SET)的简写。
用法如图,如果不存在set成功返回int的1,这个key存在了返回0。
SETEX key seconds value
上面这个命令的含义是:
将值 value
关 联到 key
,并将 key
的生存时间设为 seconds (以秒为单位)。
如果key
已经存在,setex
命令将覆写旧值。
其中setex
是一个原子性(atomic)操作,关联值和设置生存时间两个动作会在同一时间内完成。
这两个命令是实现redis分布式锁的关键,同时reddission底层也是通过这两个命令结合lua脚本完成redis分布式锁的
回到主题:
实现Redis的分布式锁,除了自己基于redis client原生api来实现之外,还可以使用开源框架:Redission
Redisson是一个企业级的开源Redis Client,也提供了分布式锁的支持,如果自己写代码来通过redis设置一个值,是通过下面这个命令设置的。
SET anyLock unique_value NX PX 30000
具体含义见下图:
这里设置的超时时间是30s,假如我超过30s都还没有完成业务逻辑的情况下,key会过期,其他线程有可能会获取到锁。
这样一来的话,第一个线程还没执行完业务逻辑,第二个线程进来了也会出现线程安全问题。所以我们还需要额外的去维护这个过期时间,太麻烦了~
而使用redission,只需要通过他的api中的lock和unlock即可完成分布式锁,他帮我们考虑了很多细节:
- redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
- redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?
redisson中有一个watchdog看门狗的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s
这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。
- redisson的"看门狗"逻辑保证了没有死锁发生。
注意:如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁)
比起自己手写代码会帮助开发者少关注贼多细节
同时redission支持多种连接模式
//单机
RedissonClient redisson = Redisson.create();
Config config = new Config();
config.useSingleServer().setAddress("myredisserver:6379");
RedissonClient redisson = Redisson.create(config);
//主从
Config config = new Config();
config.useMasterSlaveServers()
.setMasterAddress("127.0.0.1:6379")
.addSlaveAddress("127.0.0.1:6389", "127.0.0.1:6332", "127.0.0.1:6419")
.addSlaveAddress("127.0.0.1:6399");
RedissonClient redisson = Redisson.create(config);
//哨兵
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("127.0.0.1:26389", "127.0.0.1:26379")
.addSentinelAddress("127.0.0.1:26319");
RedissonClient redisson = Redisson.create(config);
//集群
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // cluster state scan interval in milliseconds
.addNodeAddress("127.0.0.1:7000", "127.0.0.1:7001")
.addNodeAddress("127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
下面直接贴代码,这里使用的是单机模式,切换自己改一下连接模式就行:
1.导入依赖:
<!-- org.redisson/redisson 分布式专用-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.5.3</version>
</dependency>
2.编写配置类:
@SpringBootApplication
public class RedisLockApplication {
public static void main(String[] args) {
SpringApplication.run(RedisLockApplication.class, args);
}
@Bean
public Redisson redisson(){
// 单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
3.上锁
@RestController
public class IndexController {
@Resource
private Redisson redisson;
@Resource
private StringRedisTemplate stringRedisTemplate; // 居然根据名字来找的,不是根据类型。。。
/**
* 代码有什么问题?
* 1.线程安全问题,多个线程同时访问就可能出现错误,用synchronized可以解决但是性能不好
* synchronized在高并发情况下还是有bug出现,会出现超卖,可以用jmeter压测
* 2.设置redis锁解决分布式场景之后,超时时间设置10s合理吗?适合场景问题?如果10s中之内没有处理完,处理到一半呢?
* 15s--10s后释放锁--还需要5s,5s后释放了其他人的锁
* 8s--5s后我的锁被人释放了,其他线程又来了
* 循环下去,锁的是别人....这不就完全乱套了,这锁完全没用啊
* 解决方法:你不是可能存在释放别人的锁的情况吗?那就设置识别号,识别到只能是自己的才能被释放
* 这只是解决了释放别人的锁的问题,你自己没有执行完就已经超时的问题呢?
* 答案:开启子线程定时器来延长超时时间咯,子线程每隔一段时间就去查看是否完成,没完成就加时,那这个一段时间要多长呢?
* 三分之一过期时间,其他人的实践经验。
* 所以:我们现在又要造轮子了吗?是否有其他人已经考虑过这个问题并做开源了呢?
* 那肯定有啊,不然我写这个干吗。redisson,比jedis强大,专对分布式
*
* 3.redisson
* 大概阐述一下这个锁的操作:
* 当一个redisson线程过来获取到锁时,后台会有其他线程去检查是否还持有锁,
* 还持有说明还没执行结束,就会继续延长锁的时间,大概10s去轮询。(三分之一)
* 另外一个线程过来,如果没有获取锁成功,就会while自旋尝试加锁。
* clientId他在底层实现了。
*
* 3.1如果使用的是Redis主从架构呢,主节点宕了,从节点怎么处理?但这是锁还没有被同步过去,其他线程就过来访问了呢?
* 3.2另外如何提升性能呢?
* - 商品库存分段存储,key不一样,每个段的数量越小性能不就越高嘛,而且锁定不同的key值
*
* @return
*/
@RequestMapping("/deduct_stock")
public String deductStock() {
// 0.标识号
String clientID = UUID.randomUUID().toString();
// 1.这个相当于一把锁,控制只能一个人来
String lockKey = "product001";
RLock lock = redisson.getLock(lockKey); // 3.1
try{
/*
// 2.获取锁
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "yjiewei");// jedis.setnx(key, value);
// 3.如果突发宕机,锁没有释放掉怎么办,key过期处理(10s),但是如果在获取锁之后就出问题呢,这一步也没有成功,大招:二合一
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
*/
// 2-3
/* Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientID, 10, TimeUnit.SECONDS);
if (!result){
return "error";
}
*/
// 3.1 解决过期时间内还未完成操作的问题
lock.lock(30, TimeUnit.SECONDS); // 先拿锁,再设置超时时间
// 4.真正操作商品库存
synchronized (this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock");
if (stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key, value);
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足!");
}
}
}finally {
lock.unlock(); // 释放锁
/*
if (clientID.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
// 5.释放锁,放在finally块中防止没有释放锁导致死锁问题
stringRedisTemplate.delete(lockKey);
}
*/
}
return "end";
}
}