0

0

如何使用Redis+Lua脚本实现计数器接口防刷功能

PHPz

PHPz

发布时间:2023-05-28 23:32:50

|

1765人浏览过

|

来源于亿速云

转载

    【实现过程】

    一、问题分析

     如果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没有得到执行,发生永久死锁)
         * 

    * 除非脚本在redis服务器执行时redis服务器发生崩溃,不过此种情况锁也会失效 */ private static final RedisScript 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 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 implements RedisScript { private final String script; private final String sha1; private final Class resultType; public RedisScriptImpl(String script, Class resultType) { this.script = script; this.sha1 = DigestUtils.sha1DigestAsHex(script); this.resultType = resultType; @Override public String getSha1() { return sha1; public Class getResultType() { return resultType; public String getScriptAsString() { return script; }

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

    (1)工具类            

    稿定AI绘图
    稿定AI绘图

    稿定推出的AI绘画工具

    下载
    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没有得到执行,发生永久死计数器)
         * 

    * 除非脚本在redis服务器执行时redis服务器发生崩溃,不过此种情况计数器也会失效 */ private static final RedisScript 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 implements RedisScript { private final String script; private final String sha1; private final Class resultType; public RedisScriptImpl(String script, Class resultType) { this.script = script; this.sha1 = DigestUtils.sha1DigestAsHex(script); this.resultType = resultType; @Override public String getSha1() { return sha1; public Class 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脚本实现计数器接口防刷功能

    相关专题

    更多
    c++主流开发框架汇总
    c++主流开发框架汇总

    本专题整合了c++开发框架推荐,阅读专题下面的文章了解更多详细内容。

    79

    2026.01.09

    c++框架学习教程汇总
    c++框架学习教程汇总

    本专题整合了c++框架学习教程汇总,阅读专题下面的文章了解更多详细内容。

    46

    2026.01.09

    学python好用的网站推荐
    学python好用的网站推荐

    本专题整合了python学习教程汇总,阅读专题下面的文章了解更多详细内容。

    121

    2026.01.09

    学python网站汇总
    学python网站汇总

    本专题整合了学python网站汇总,阅读专题下面的文章了解更多详细内容。

    12

    2026.01.09

    python学习网站
    python学习网站

    本专题整合了python学习相关推荐汇总,阅读专题下面的文章了解更多详细内容。

    15

    2026.01.09

    俄罗斯手机浏览器地址汇总
    俄罗斯手机浏览器地址汇总

    汇总俄罗斯Yandex手机浏览器官方网址入口,涵盖国际版与俄语版,适配移动端访问,一键直达搜索、地图、新闻等核心服务。

    71

    2026.01.09

    漫蛙稳定版地址大全
    漫蛙稳定版地址大全

    漫蛙稳定版地址大全汇总最新可用入口,包含漫蛙manwa漫画防走失官网链接,确保用户随时畅读海量正版漫画资源,建议收藏备用,避免因域名变动无法访问。

    370

    2026.01.09

    php学习网站大全
    php学习网站大全

    精选多个优质PHP入门学习网站,涵盖教程、实战与文档,适合零基础到进阶开发者,助你高效掌握PHP编程。

    45

    2026.01.09

    php网站搭建教程大全
    php网站搭建教程大全

    本合集专为零基础用户打造,涵盖PHP网站搭建全流程,从环境配置到实战开发,免费、易懂、系统化,助你快速入门建站!

    12

    2026.01.09

    热门下载

    更多
    网站特效
    /
    网站源码
    /
    网站素材
    /
    前端模板

    精品课程

    更多
    相关推荐
    /
    热门推荐
    /
    最新课程
    进程与SOCKET
    进程与SOCKET

    共6课时 | 0.3万人学习

    Redis+MySQL数据库面试教程
    Redis+MySQL数据库面试教程

    共72课时 | 6.3万人学习

    关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
    php中文网:公益在线php培训,帮助PHP学习者快速成长!
    关注服务号 技术交流群
    PHP中文网订阅号
    每天精选资源文章推送

    Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号