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 Cluster 字段模糊匹配及删除
May 27 Redis
浅谈Redis主从复制以及主从复制原理
May 29 Redis
Redis可视化客户端小结
Jun 10 Redis
redis实现的四种常见限流策略
Jun 18 Redis
源码分析Redis中 set 和 sorted set 的使用方法
Mar 22 Redis
Redis如何实现验证码发送 以及限制每日发送次数
Apr 18 Redis
windows安装 redis 6.2.6最新步骤详解
Apr 26 Redis
Redis 限流器
May 15 Redis
浅谈Redis缓冲区机制
Jun 05 Redis
浅谈Redis变慢的原因及排查方法
Jun 21 Redis
利用Redis实现点赞功能的示例代码
Jun 28 Redis
python中使用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和AJAX创建RSS聚合器的代码
2007/03/13 PHP
PHP 防恶意刷新实现代码
2010/05/16 PHP
php 数组排序 array_multisort与uasort的区别
2011/03/24 PHP
php while循环得到循环次数
2013/10/26 PHP
测试php函数的方法
2013/11/13 PHP
基于递归实现的php树形菜单代码
2014/11/19 PHP
Symfony2针对输入时间进行查询的方法分析
2017/06/28 PHP
详解JavaScript设计模式开发中的桥接模式使用
2016/05/18 Javascript
简单了解JavaScript操作XPath的一些基本方法
2016/06/03 Javascript
JavaScript必知必会(七)js对象继承
2016/06/08 Javascript
微信公众平台开发教程(六)获取个性二维码的实例
2016/12/02 Javascript
使用D3.js制作图表详解
2017/08/13 Javascript
JS实现为动态添加的元素增加事件功能示例【基于事件委托】
2018/03/21 Javascript
详解Nodejs get获取远程服务器接口数据
2019/03/26 NodeJs
Vue中消息横向滚动时setInterval清不掉的问题及解决方法
2019/08/23 Javascript
Vue Extends 扩展选项用法完整实例
2019/09/17 Javascript
[01:19]DOTA2城市挑战赛报名开始 开启你的城市传奇
2018/03/23 DOTA
跟老齐学Python之开始真正编程
2014/09/12 Python
全面分析Python的优点和缺点
2018/02/07 Python
Python递归实现汉诺塔算法示例
2018/03/19 Python
Tensorflow卷积神经网络实例进阶
2018/05/24 Python
Python HTML解析器BeautifulSoup用法实例详解【爬虫解析器】
2019/04/05 Python
python反编译学习之字节码详解
2019/05/19 Python
对Python的交互模式和直接运行.py文件的区别详解
2019/06/29 Python
python配置文件写入过程详解
2019/10/19 Python
Python将字典转换为XML的方法
2020/08/01 Python
纽约复古灵感的现代珠宝品牌:Lulu Frost
2018/03/03 全球购物
媒矿安全生产承诺书
2014/05/23 职场文书
委托书怎么写
2014/07/31 职场文书
副检察长四风问题对照检查材料思想汇报
2014/10/07 职场文书
2016年国陪研修感言
2015/11/18 职场文书
高中数学课堂教学反思
2016/02/18 职场文书
《牧场之国》教学反思
2016/02/22 职场文书
HTML基本元素标签介绍
2022/02/28 HTML / CSS
Win11安装升级时提示“该电脑必须支持安全启动”
2022/04/19 数码科技
Java实现带图形界面的聊天程序
2022/06/10 Java/Android