必看企业级Redis锁资产巡检扫描业务场景实现(加锁限制扫描次数)
Posted DT辰白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了必看企业级Redis锁资产巡检扫描业务场景实现(加锁限制扫描次数)相关的知识,希望对你有一定的参考价值。
Redis实现资产巡检扫描
前言
最近小编在公司遇到这么一个需求,现在分享出来给大家一起讨论,用户需求:对设备资产巡检扫描做一个次数限制,就比如我们保安巡逻,每天固定巡逻几次,不能超出限制,还有很多列子,比如一个API接口限制请求次数,大概业务逻辑就是这样。
一、需求一:限制扫描次数
首先Redis的环境准备,这里小编不再多说,随便找我CSDN的博客,SpringBoot整合Redis的文章很多,都是基础功,我们是用RedisTemplate的方式来实现我们的这个需求。
1.1 业务分析
代码都是建立在需求上面的,所以我们再工作中,每个任务,每个功能都要先分析需求,再去实现具体过程,小编的思路经常都是这样,不会盲目的去写代码,至少你写的代码,不会全部是废弃的代码,而是不断的改进,这样的代码才是好的代码,因为可能今天这个接口的场景适用,明天就需要加入新的参数、新的业务逻辑等等,所以循循渐进才是代码之道。
我的分析:使用Redis的自增模式(计数器)来统计扫描次数,并限制请求次数。
计数器:是Redis 的原子性自增操作可实现的最直观的模式了,它的想法相当简单:每当某个操作发生时,向 Redis 发送一个 INCR 命令。
1.2 代码实现
使用Redis的计数器,就是INCR 命令实现统计扫描次数,假设这里5分钟之内扫描3次,当大于三次的时候,我们就给用户反馈,请5分钟后再来扫描,当然这里的5分钟,你也可以看做是5小时、5个周、5个月、5年后。
核心代码:
// 计数器 + 1 操作
redisTemplate.opsForValue().increment(key);
// 设置第一次扫描开始的过期时间
redisTemplate.expire(key,60 * 5,TimeUnit.SECONDS);
具体代码
@PostMapping("/test1/{id}/{key}")
public JSONObject test1(@PathVariable String id, @PathVariable String key){
JSONObject jsonObject = new JSONObject();
// 统计次数
Long increment = 0L;
Object time = redisTemplate.opsForValue().get(key);
// 第一次扫描的时候,Redis没有值
if(time == null){
// 计数器 +1 操作
increment = redisTemplate.opsForValue().increment(key);
// 设置5分钟过期时间
redisTemplate.expire(key,60 * 5,TimeUnit.SECONDS);
jsonObject.put("code",2000);
jsonObject.put("msg","扫描成功");
return jsonObject;
}else {
// 不是第一次扫描
Integer count = (Integer) time;
System.out.println("count->>>"+count);
// 是否大于3次
if(count < 3){
// 小于3次,每次计数器 +1 操作
increment = redisTemplate.opsForValue().increment(key);
jsonObject.put("code",2000);
jsonObject.put("msg","扫描成功");
return jsonObject;
}else {
// 否则5分钟内超出三次
jsonObject.put("code",5000);
jsonObject.put("msg","API调用限制3次");
return jsonObject;
}
}
}
调用接口分析
这里我们不同的用户对同一个资产进行扫描,但是每个资产在5分钟之内最多只能扫描3次。
这里资产设备FAC-1被用户1开启了第一次扫描:
下面我们继续扫描,随便用用户2、或者用户3去扫描,当我们调用了三次请求之后:
可以看到我们扫描的次数到达限制,一旦到达3次以后,就不再往Redis的计数器里面写入了,立即给用户提示,五分钟后再来扫描,这里我们是针对资产设备来做的key,并不是针对具体的哪个人只能扫描几次,只是针对这个资产设备允许被扫描的最大的次数,ok到这里结束了,这个需求搞定,好像没多大问题。
二、需求二:限制同一个位置同一时间只能有一个人扫描
1.1 业务分析
针对上面的限制请求次数,已经搞定了,为什么还有下面加锁的步骤呢?
我的分析:如果同一个设备同一个时间同时被两个不同的人扫描了,怎么办?所以这个时候就引入Redis的锁,可以帮助我们解决这个问题。
Redis 锁主要利用 Redis 的 setnx 命令。
- 加锁命令:SETNX key value,当键不存在时,对键进行设置操作并返回成功,否则返回失败。KEY 是锁的唯一标识,一般按业务来决定命名。
- 解锁命令:DEL key,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。
- 锁超时:EXPIRE key timeout, 设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。
1.2 伪代码实现
下面我们先来分析伪代码:
public void lock(){
try {
// 设置自动释放锁时间
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", "value", Duration.ofSeconds(10));
if (flag){
// 获取锁成功
// TODO 执行业务逻辑
}else {
//获取锁失败
// TODO 快速失败,响应给客户端
}
}finally {
// 最后一定要释放锁,以免造成死锁
redisTemplate.delete("key");
}
}
上述锁实现方式需要思考一些问题:
为什么锁要设置超时时间?
如果 setIfAbsent 成功,服务器挂掉、重启或网络问题等,锁没有设置超时时间变成死锁。
为什么释放锁?
一定记得在finally 块释放锁,以免造成死锁。
为什么要设置锁的value?
同一个key,被不同的人访问,锁是同一个无法区分当前锁住的对象,需要标识。
锁误解除了?
假设:用户A、B两个线程来尝试给lock加锁,用户A线程执行setIfAbsent先拿到锁(假如锁10秒后过期),用户B线程就在等待尝试获取锁,到这一步是没有问题的。继续往下执行,如果此时业务逻辑比较耗时,执行时间已经超过redis锁过期时间,这时用户A的线程的锁自动释放(删除key),用户B线程检测到lock这个key不存在,执行setIfAbsent命令也拿到了锁。但是,此时A线程执行完业务逻辑之后,还是会去释放锁(删除key),这就导致用户B线程的锁被用户A线程给释放了。
到这里我们只需要解决最后一个问题即可,锁误解除,遇到这样的场景怎么做呢?
设置不同的value来保证锁的唯一性。
1.3 具体代码实现
使用用户id和资产设备号来做Redis锁的key,这样就能保证锁的唯一性。
// 加锁,防止同时扫描
String LOCK_KEY = "lock";
// id->>>用户id key->>>资产设备号
String LOCK_VALUE = id + key;
// 假设设置30秒自动释放锁
Boolean b = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, LOCK_VALUE, Duration.ofSeconds(30));
//释放锁
if (LOCK_VALUE.equals(redisTemplate.opsForValue().get(LOCK_KEY))){
redisTemplate.delete(LOCK_KEY);
}
完整代码如下
@PostMapping("/test2/{id}/{key}")
public JSONObject incr(@PathVariable String id,@PathVariable String key) throws InterruptedException {
JSONObject jsonObject = new JSONObject();
// 加锁,防止同时扫描
String LOCK_KEY = "lock";
// id->>>用户id key->>>资产设备号
String LOCK_VALUE = id + key;
try {
// 假设设置30秒自动释放锁
Boolean b = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, LOCK_VALUE, Duration.ofSeconds(30));
if (b){
//获取锁成功
//执行业务逻辑
// 模拟业务超时
Thread.sleep(20000);
// 计数器key
String idKey = key;
Long increment = 0L;
Object time = redisTemplate.opsForValue().get(idKey);
if(time == null){
increment = redisTemplate.opsForValue().increment(idKey);
redisTemplate.expire(idKey,60*5,TimeUnit.SECONDS);
jsonObject.put("code",2000);
jsonObject.put("msg","扫描成功");
return jsonObject;
}else {
Integer count = (Integer) time;
System.out.println("count->>>"+count);
if(count < 3){
increment = redisTemplate.opsForValue().increment(idKey);
jsonObject.put("code",2000);
jsonObject.put("msg","扫描成功");
return jsonObject;
}else {
jsonObject.put("code",5000);
jsonObject.put("msg","API调用限制3次");
return jsonObject;
}
}
}else {
//获取锁失败
//快速失败,响应给客户端
jsonObject.put("code",5001);
jsonObject.put("msg","当前设备扫描中!");
return jsonObject;
}
}finally {
//释放锁
if (LOCK_VALUE.equals(redisTemplate.opsForValue().get(LOCK_KEY))){
redisTemplate.delete(LOCK_KEY);
}
}
}
这里我们来模拟业务超时,假设业务超时,其他线程需要获取锁,就需要等待:
// 模拟业务超时 睡眠20秒钟
Thread.sleep(20000);
1.4 调用API
先模拟A用户扫描资产设备DT1,并且另外一个用户B也去扫描DT1,这里可以写多线程模拟,同时请求接口,为了方便测试,我们使用休眠时间来模拟,也是一样的效果,当然测试并发的工具也很多,小编的文章中也有说到过,大家可自行尝试。
用户1扫描中,睡眠20秒中,此时用户2去扫描设备获取锁。
当扫描到达3次以后,限制请求:
ok,结束!!!!!!!!!!!!!!!!!!!!!!!!!
总结
上面需要注意的是锁的超时时间设置,需要把握好,比如业务超时,业务执行时间大于了锁的超时时间,通俗的来说就是:锁过期了,业务还没执行完(针对业务逻辑复杂,消耗时长较大的情况),这种情况又怎么解决呢?后面我们会继续来探讨这个问题。
以上是关于必看企业级Redis锁资产巡检扫描业务场景实现(加锁限制扫描次数)的主要内容,如果未能解决你的问题,请参考以下文章
Redis学习Redis分布式锁实现秒杀业务(乐观锁悲观锁)