必看企业级Redis锁资产巡检扫描业务场景实现(加锁限制扫描次数)

Posted DT辰白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了必看企业级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 命令。

  1. 加锁命令:SETNX key value,当键不存在时,对键进行设置操作并返回成功,否则返回失败。KEY 是锁的唯一标识,一般按业务来决定命名。
  2. 解锁命令:DEL key,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。
  3. 锁超时: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分布式锁的正确加锁与解锁方式

Redis学习Redis分布式锁实现秒杀业务(乐观锁悲观锁)

Redis学习Redis分布式锁实现秒杀业务(乐观锁悲观锁)

基于 Redis 分布式锁实现“秒杀”(含代码)

基于redis分布式锁实现“秒杀”