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 相关文章推荐
在K8s上部署Redis集群的方法步骤
Apr 27 Redis
为Java项目添加Redis缓存的方法
May 18 Redis
详解Redis主从复制实践
May 19 Redis
浅谈redis缓存在项目中的使用
May 20 Redis
redis客户端实现高可用读写分离的方式详解
Jul 04 Redis
Redis性能监控的实现
Jul 09 Redis
使用redis实现延迟通知功能(Redis过期键通知)
Sep 04 Redis
redis数据一致性的实现示例
Mar 18 Redis
使用Redis实现点赞取消点赞的详细代码
Mar 20 Redis
redis复制有可能碰到的问题汇总
Apr 03 Redis
redis 解决库存并发问题实现数量控制
Apr 08 Redis
Redis过期数据是否会被立马删除
Jul 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扩展介绍与开发教程
2010/08/19 PHP
thinkphp的CURD和查询方式介绍
2013/12/19 PHP
ThinkPHP Mobile使用方法简明教程
2014/06/18 PHP
PHP扩展开发入门教程
2015/02/26 PHP
又拍云异步上传实例教程详解
2016/04/19 PHP
Redis使用Eval多个键值自增的操作实例
2016/11/04 PHP
/etc/php-fpm.d/www.conf 配置注意事项
2017/02/04 PHP
使用jQuery内容过滤选择器选择元素实例讲解
2013/04/18 Javascript
二叉树先序遍历的非递归算法具体实现
2014/01/09 Javascript
jQuery+jRange实现滑动选取数值范围特效
2015/03/14 Javascript
浅析BootStrap Treeview的简单使用
2016/10/12 Javascript
JSONP基础知识详解
2017/03/19 Javascript
Javascript实现信息滚动效果
2017/05/18 Javascript
JS原生轮播图的简单实现(推荐)
2017/07/22 Javascript
帝国cms首页列表页实现点赞功能
2017/10/30 Javascript
一个简单的node.js界面实现方法
2018/06/01 Javascript
vue中前进刷新、后退缓存用户浏览数据和浏览位置的实例讲解
2018/09/21 Javascript
jQuery表单元素过滤选择器用法实例分析
2019/02/20 jQuery
在Vue 中获取下拉框的文本及选项值操作
2020/08/13 Javascript
js实现点击按钮随机生成背景颜色
2020/09/05 Javascript
解决vant中 tab栏遇到的坑 van-tabs
2020/11/04 Javascript
nodejs处理tcp连接的核心流程
2021/02/26 NodeJs
详解Python中的日志模块logging
2015/06/19 Python
Django入门使用示例
2017/12/12 Python
1分钟快速生成用于网页内容提取的xslt
2018/02/23 Python
Django框架多表查询实例分析
2018/07/04 Python
使用python 将图片复制到系统剪贴中
2019/12/13 Python
python如何通过闭包实现计算器的功能
2020/02/22 Python
Myprotein意大利官网:欧洲第一运动营养品牌
2018/11/22 全球购物
英国蛋糕装饰用品一站式商店:Craft Company
2019/03/18 全球购物
幼儿园实习生辞职信
2014/01/20 职场文书
建筑系毕业生自我鉴定
2014/01/24 职场文书
满月酒主持词
2014/03/27 职场文书
机械专业求职信范文
2014/07/15 职场文书
仓管员岗位职责
2015/02/03 职场文书
PHP 技巧 * SVG 保存为图片(分享图生成)
2021/04/02 PHP