Redis+Lua脚本实现计数器接口防刷功能(升级版)


Posted in Redis onFebruary 12, 2022

【前言】

Cash Loan(一):Redis实现计数器防刷 中介绍了项目中应用redis来做计数器的实现过程,最近自己看了些关于Redis实现分布式锁的代码后,发现在Redis分布式锁中出现一个问题在这版计数器中同样会出现,于是融入了Lua脚本进行升级改造有了Redis+Lua版本。

【实现过程】

一、问题分析

 如果set命令设置上,但是在设置失效时间时由于网络抖动等原因导致没有设置成功,这时就会出现死计数器(类似死锁);

二、解决方案

 Redis+Lua是一个很好的解决方案,使用脚本使得set命令和expire命令一同达到Redis被执行且不会被干扰,在很大程度上保证了原子操作;

为什么说是很大程度上保证原子操作而不是完全保证?因为在Redis内部执行的时候出问题也有可能出现问题不过概率非常小;即使针对小概率事件也有相应的解决方案,比如解决死锁一个思路值得参考:防止死锁会将锁的值存成一个时间戳,即使发生没有将失效时间设置上在判断是否上锁时可以加上看看其中值距现在是否超过一个设定的时间,如果超过则将其删除重新设置锁。       

三、代码改造

1、Redis+Lua锁的实现

package han.zhang.utils;
 
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DigestUtils;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.UUID;
public class RedisLock {
    private static final LogUtils logger = LogUtils.getLogger(RedisLock.class);
    private final StringRedisTemplate stringRedisTemplate;
    private final String lockKey;
    private final String lockValue;
    private boolean locked = false;
    /**
     * 使用脚本在redis服务器执行这个逻辑可以在一定程度上保证此操作的原子性
     * (即不会发生客户端在执行setNX和expire命令之间,发生崩溃或失去与服务器的连接导致expire没有得到执行,发生永久死锁)
     * <p>
     * 除非脚本在redis服务器执行时redis服务器发生崩溃,不过此种情况锁也会失效
     */
    private static final RedisScript<Boolean> SETNX_AND_EXPIRE_SCRIPT;
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then\n");
        sb.append("\tredis.call('expire', KEYS[1], tonumber(ARGV[2]))\n");
        sb.append("\treturn true\n");
        sb.append("else\n");
        sb.append("\treturn false\n");
        sb.append("end");
        SETNX_AND_EXPIRE_SCRIPT = new RedisScriptImpl<>(sb.toString(), Boolean.class);
    }
    private static final RedisScript<Boolean> DEL_IF_GET_EQUALS;
        sb.append("if (redis.call('get', KEYS[1]) == ARGV[1]) then\n");
        sb.append("\tredis.call('del', KEYS[1])\n");
        DEL_IF_GET_EQUALS = new RedisScriptImpl<>(sb.toString(), Boolean.class);
    public RedisLock(StringRedisTemplate stringRedisTemplate, String lockKey) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockKey = lockKey;
        this.lockValue = UUID.randomUUID().toString() + "." + System.currentTimeMillis();
    private boolean doTryLock(int lockSeconds) {
        if (locked) {
            throw new IllegalStateException("already locked!");
        }
        locked = stringRedisTemplate.execute(SETNX_AND_EXPIRE_SCRIPT, Collections.singletonList(lockKey), lockValue,
                String.valueOf(lockSeconds));
        return locked;
     * 尝试获得锁,成功返回true,如果失败立即返回false
     *
     * @param lockSeconds 加锁的时间(秒),超过这个时间后锁会自动释放
    public boolean tryLock(int lockSeconds) {
        try {
            return doTryLock(lockSeconds);
        } catch (Exception e) {
            logger.error("tryLock Error", e);
            return false;
     * 轮询的方式去获得锁,成功返回true,超过轮询次数或异常返回false
     * @param lockSeconds       加锁的时间(秒),超过这个时间后锁会自动释放
     * @param tryIntervalMillis 轮询的时间间隔(毫秒)
     * @param maxTryCount       最大的轮询次数
    public boolean tryLock(final int lockSeconds, final long tryIntervalMillis, final int maxTryCount) {
        int tryCount = 0;
        while (true) {
            if (++tryCount >= maxTryCount) {
                // 获取锁超时
                return false;
            }
            try {
                if (doTryLock(lockSeconds)) {
                    return true;
                }
            } catch (Exception e) {
                logger.error("tryLock Error", e);
                Thread.sleep(tryIntervalMillis);
            } catch (InterruptedException e) {
                logger.error("tryLock interrupted", e);
     * 解锁操作
    public void unlock() {
        if (!locked) {
            throw new IllegalStateException("not locked yet!");
        locked = false;
        // 忽略结果
        stringRedisTemplate.execute(DEL_IF_GET_EQUALS, Collections.singletonList(lockKey), lockValue);
    private static class RedisScriptImpl<T> implements RedisScript<T> {
        private final String script;
        private final String sha1;
        private final Class<T> resultType;
        public RedisScriptImpl(String script, Class<T> resultType) {
            this.script = script;
            this.sha1 = DigestUtils.sha1DigestAsHex(script);
            this.resultType = resultType;
        @Override
        public String getSha1() {
            return sha1;
        public Class<T> getResultType() {
            return resultType;
        public String getScriptAsString() {
            return script;
}

2、借鉴锁实现Redis+Lua计数器

(1)工具类            

package han.zhang.utils;
 
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DigestUtils;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
public class CountUtil {
    private static final LogUtils logger = LogUtils.getLogger(CountUtil.class);
    private final StringRedisTemplate stringRedisTemplate;
    /**
     * 使用脚本在redis服务器执行这个逻辑可以在一定程度上保证此操作的原子性
     * (即不会发生客户端在执行setNX和expire命令之间,发生崩溃或失去与服务器的连接导致expire没有得到执行,发生永久死计数器)
     * <p>
     * 除非脚本在redis服务器执行时redis服务器发生崩溃,不过此种情况计数器也会失效
     */
    private static final RedisScript<Boolean> SET_AND_EXPIRE_SCRIPT;
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("local visitTimes = redis.call('incr', KEYS[1])\n");
        sb.append("if (visitTimes == 1) then\n");
        sb.append("\tredis.call('expire', KEYS[1], tonumber(ARGV[1]))\n");
        sb.append("\treturn false\n");
        sb.append("elseif(visitTimes > tonumber(ARGV[2])) then\n");
        sb.append("\treturn true\n");
        sb.append("else\n");
        sb.append("end");
        SET_AND_EXPIRE_SCRIPT = new RedisScriptImpl<>(sb.toString(), Boolean.class);
    }
    public CountUtil(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    public boolean isOverMaxVisitTimes(String key, int seconds, int maxTimes) throws Exception {
        try {
            return stringRedisTemplate.execute(SET_AND_EXPIRE_SCRIPT, Collections.singletonList(key), String.valueOf(seconds), String.valueOf(maxTimes));
        } catch (Exception e) {
            logger.error("RedisBusiness>>>isOverMaxVisitTimes; get visit times Exception; key:" + key + "result:" + e.getMessage());
            throw new Exception("already Over MaxVisitTimes");
        }
    private static class RedisScriptImpl<T> implements RedisScript<T> {
        private final String script;
        private final String sha1;
        private final Class<T> resultType;
        public RedisScriptImpl(String script, Class<T> resultType) {
            this.script = script;
            this.sha1 = DigestUtils.sha1DigestAsHex(script);
            this.resultType = resultType;
        @Override
        public String getSha1() {
            return sha1;
        public Class<T> getResultType() {
            return resultType;
        public String getScriptAsString() {
            return script;
}

(2)调用测试代码

public void run(String... strings) {
        CountUtil countUtil = new CountUtil(SpringUtils.getStringRedisTemplate());
        try {
            for (int i = 0; i < 10; i++) {
                boolean overMax = countUtil.isOverMaxVisitTimes("zhanghantest", 600, 2);
                if (overMax) {
                    System.out.println("超过i:" + i + ":" + overMax);
                } else {
                    System.out.println("没超过i:" + i + ":" + overMax);
                }
            }
        } catch (Exception e) {
            logger.error("Exception {}", e.getMessage());
        }
    }

(3)测试结果

Redis+Lua脚本实现计数器接口防刷功能(升级版)

【总结】

       1、用心去不断的改造自己的程序;

       2、用代码改变世界。

到此这篇关于Redis+Lua实现计数器接口防刷(升级版)的文章就介绍到这了,更多相关Redis计数器内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
redis 查看所有的key方式
May 07 Redis
Redis Cluster 集群搭建你会吗
Aug 04 Redis
基于Redis的List实现特价商品列表功能
Aug 30 Redis
Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题
Feb 12 Redis
面试分析分布式架构Redis热点key大Value解决方案
Mar 13 Redis
redis击穿 雪崩 穿透超详细解决方案梳理
Mar 17 Redis
Redis高可用集群redis-cluster详解
Mar 20 Redis
Redis特殊数据类型HyperLogLog基数统计算法讲解
Jun 01 Redis
Redis keys命令的具体使用
Jun 05 Redis
关于Redis的主从复制及哨兵问题
Jun 16 Redis
Redis主从复制操作和配置详情
Sep 23 Redis
Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题
基于Redis zSet实现滑动窗口对短信进行防刷限流的问题
Feb 12 #Redis
聊聊redis-dump工具安装问题
Jan 18 #Redis
redis的list数据类型相关命令介绍及使用
Jan 18 #Redis
关于使用Redisson订阅数问题
Jan 18 #Redis
Redis中缓存穿透/击穿/雪崩问题和解决方法
linux下安装redis图文详细步骤
You might like
PHP计划任务、定时执行任务的实现代码
2011/04/23 PHP
php判断访问IP的方法
2015/06/19 PHP
3种php生成唯一id的方法
2015/11/23 PHP
php封装的pdo数据库操作工具类与用法示例
2019/05/08 PHP
php中关于换行的实例写法
2019/09/26 PHP
jQuery 常见学习网站与参考书
2009/11/09 Javascript
javascript权威指南 学习笔记之null和undefined
2011/09/25 Javascript
JavaScript 原型继承
2011/12/26 Javascript
js 字符串转换成数字的三种方法
2013/03/23 Javascript
javascript常用对话框小集
2013/09/13 Javascript
javascript创建createXmlHttpRequest对象示例代码
2014/02/10 Javascript
js实现完美兼容各大浏览器的人民币大小写相互转换
2015/10/29 Javascript
jQuery ajax中使用confirm,确认是否删除的简单实例
2016/06/17 Javascript
ionic进入多级目录后隐藏底部导航栏(tabs)的完美解决方案
2016/11/23 Javascript
使用BootStrap实现标签切换原理解析
2017/03/14 Javascript
解决Vue不能检测数组或对象变动的问题
2018/02/24 Javascript
vue webpack build资源相对路径的问题及解决方法
2020/06/04 Javascript
Vant 中的Toast设置全局的延迟时间操作
2020/11/04 Javascript
对pycharm代码整体左移和右移缩进快捷键的介绍
2018/07/16 Python
Python OpenCV 调用摄像头并截图保存功能的实现代码
2019/07/02 Python
python logging设置level失败的解决方法
2020/02/19 Python
Python使用monkey.patch_all()解决协程阻塞问题
2020/04/15 Python
python中有函数重载吗
2020/05/28 Python
opencv+pyQt5实现图片阈值编辑器/寻色块阈值利器
2020/11/13 Python
HTML5 通信API 跨域门槛将不再高、数据推送也不再是梦
2013/04/25 HTML / CSS
纽约著名的服装辅料来源:M&J Trimming
2017/07/26 全球购物
美国最大的在线寄售和旧货店:Swap.com
2018/08/27 全球购物
Famous Footwear加拿大:美国多品牌运动休闲鞋店
2018/12/05 全球购物
优秀毕业生自我鉴定
2014/01/19 职场文书
2014年端午节活动方案
2014/03/11 职场文书
大学新生军训方案
2014/05/03 职场文书
部门年终奖分配方案
2014/05/07 职场文书
小学生暑假安全公约
2015/07/14 职场文书
​(迎国庆)作文之我爱我的祖国
2019/09/19 职场文书
数据库的高级查询六:表连接查询:外连接(左外连接,右外连接,UNION关键字,连接中ON与WHERE的不同)
2021/04/05 MySQL
JavaScript 中for/of,for/in 的详细介绍
2021/11/17 Javascript