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

 更新时间:2022年02月09日 16:34:03   作者:当年的春天  
这篇文章主要介绍了Redis+Lua脚本实现计数器接口防刷功能,使用脚本使得set命令和expire命令一同达到Redis被执行且不会被干扰,在很大程度上保证了原子操作,对Redis实现计数器接口防刷功能感兴趣的朋友一起看看吧

【前言】

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)测试结果

【总结】

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

       2、用代码改变世界。

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

相关文章

  • 深入浅析Redis 集群伸缩原理

    深入浅析Redis 集群伸缩原理

    Redis 集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容,也可以下线部分节点进行缩容,接下来通过本文给大家分享Redis 集群伸缩原理,感兴趣的朋友一起看看吧
    2021-05-05
  • 浅谈为什么单线程的redis那么快

    浅谈为什么单线程的redis那么快

    本文主要介绍了为什么单线程的redis那么快,主要介绍了几点原因,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-09-09
  • Redis shake实现可视化监控的示例代码

    Redis shake实现可视化监控的示例代码

    Redis可视化监控是通过监控Redis服务器的各项指标和状态,并将其以可视化的方式展示给用户,本文给大家介绍了Redis shake实现可视化监控,并通过代码示例讲解的非常详细,需要的朋友可以参考下
    2024-03-03
  • Redis中3种特殊的数据类型(BitMap、Geo和HyperLogLog)

    Redis中3种特殊的数据类型(BitMap、Geo和HyperLogLog)

    这篇文章主要给大家介绍了关于Redis中3种特殊的数据类型(BitMap、GEOADD和GEODIST)的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-03-03
  • Redis6.0搭建集群Redis-cluster的方法

    Redis6.0搭建集群Redis-cluster的方法

    这篇文章主要介绍了Redis6.0搭建集群Redis-cluster的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-05-05
  • redis过期回调坑的解决

    redis过期回调坑的解决

    Redis提供了一种过期回调的机制,可以在某个键过期时触发一个回调函数,然而,在实际使用中,我们往往会遇到一些灾难性的问题,其中一个就是在使用过期回调的时候,我们可能会遭遇到无法预料的错误,本文就详细的介绍一下
    2023-09-09
  • Redis通用命令介绍以及key的层级结构讲解

    Redis通用命令介绍以及key的层级结构讲解

    这篇文章主要介绍了Redis通用命令以及key的层级结构,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-12-12
  • 全网最完整的Redis新手入门指导教程

    全网最完整的Redis新手入门指导教程

    这篇文章主要给大家介绍了Redis新手入门的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • Redis 配置文件重要属性的具体使用

    Redis 配置文件重要属性的具体使用

    Redis在IT公司中的使用率自不必说,今天我们就来介绍一下Redis 配置文件重要属性的具体使用,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-05-05
  • Redis 实现同步锁案例

    Redis 实现同步锁案例

    这篇文章主要介绍了Redis 实现同步锁案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01

最新评论