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安装启动及常见数据类型
Apr 14 Redis
redis实现排行榜功能
May 24 Redis
Redis集群的关闭与重启操作
Jul 07 Redis
Redisson实现Redis分布式锁的几种方式
Aug 07 Redis
Redis字典实现、Hash键冲突及渐进式rehash详解
Sep 04 Redis
使用redis生成唯一编号及原理示例详解
Sep 15 Redis
详解Redis在SpringBoot工程中的综合应用
Oct 16 Redis
使用Redis实现点赞取消点赞的详细代码
Mar 20 Redis
详解Redis的三种常用的缓存读写策略步骤
May 06 Redis
Redis基本数据类型List常用操作命令
Jun 01 Redis
浅谈Redis缓冲区机制
Jun 05 Redis
Redis配置外网可访问(redis远程连接不上)的方法
Dec 24 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+mysql扎实个人基本功
2008/03/27 PHP
PHP 文件编程综合案例-文件上传的实现
2013/07/03 PHP
PHP的拦截器实例分析
2014/11/03 PHP
ThinkPHP中RBAC类的四种用法分析
2014/11/24 PHP
php rsa 加密,解密,签名,验签详解
2016/12/06 PHP
浅谈PHP的反射API
2017/02/26 PHP
javascript 事件绑定问题
2011/01/01 Javascript
jquery表格内容筛选实现思路及代码
2013/04/16 Javascript
jQuery 获取URL的GET参数值的小例子
2013/04/18 Javascript
jQuery中filter()和find()的区别深入了解
2013/09/25 Javascript
node.js中的buffer.Buffer.isBuffer方法使用说明
2014/12/14 Javascript
javascript面向对象之共享成员属性与方法及prototype关键字用法
2015/01/13 Javascript
JavaScript实现SHA-1加密算法的方法
2015/03/11 Javascript
js控制元素显示在屏幕固定位置及监听屏幕高度变化的方法
2015/08/11 Javascript
JavaScript实现横向滑出的多级菜单效果
2015/10/09 Javascript
jQuery拖动元素并对元素进行重新排序
2015/12/30 Javascript
javascript DOM的详解及实例代码
2017/03/06 Javascript
Vue动态实现评分效果
2017/05/24 Javascript
jackson解析json字符串,首字母大写会自动转为小写的方法
2017/12/22 Javascript
linux 后台运行node服务指令方法
2018/05/23 Javascript
深入了解Hybrid App技术的相关知识
2019/07/17 Javascript
element-ui中Table表格省市区合并单元格的方法实现
2019/08/07 Javascript
小程序跳转到的H5页面再跳转回跳小程序的方法
2020/03/06 Javascript
[47:22]Mineski vs Winstrike 2018国际邀请赛小组赛BO2 第二场 8.16
2018/08/17 DOTA
[02:50]【扭转乾坤,只此一招】DOTA2全新版本永雾林渊开启新篇章
2020/12/24 DOTA
python如何修改装饰器中参数
2018/03/20 Python
python3对接mysql数据库实例详解
2019/04/30 Python
python实现简单颜色识别程序
2020/02/19 Python
django model的update时auto_now不被更新的原因及解决方式
2020/04/01 Python
Python多线程threading创建及使用方法解析
2020/06/17 Python
python实现npy格式文件转换为txt文件操作
2020/07/01 Python
使用HTML5和CSS3表单验证功能
2017/05/05 HTML / CSS
伊芙丽官方旗舰店:中国淑女一线品牌
2017/12/01 全球购物
俄罗斯香水和化妆品购物网站:Л’Этуаль
2018/05/10 全球购物
电子商务毕业生求职信
2013/11/10 职场文书
理工科学生的自我评价
2013/12/15 职场文书