Redis实现计数器---接口防刷---升级版(Redis+Lua)

 强烈推荐一个大神的人工智能的教程:http://www.captainai.net/zhanghan 

【前言】

         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;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if (redis.call('get', KEYS[1]) == ARGV[1]) then\n");
        sb.append("\tredis.call('del', KEYS[1])\n");
        sb.append("\treturn true\n");
        sb.append("else\n");
        sb.append("\treturn false\n");
        sb.append("end");
        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);
                return false;
            }
            try {
                Thread.sleep(tryIntervalMillis);
            } catch (InterruptedException e) {
                logger.error("tryLock interrupted", e);
                return false;
            }
        }
    }

    /**
     * 解锁操作
     */
    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;
        }

        @Override
        public Class<T> getResultType() {
            return resultType;
        }

        @Override
        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("\treturn false\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;
        }

        @Override
        public Class<T> getResultType() {
            return resultType;
        }

        @Override
        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、用代码改变世界。

  • 15
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
### 回答1: 可以利用Redis的expire命令和Lua脚本来实现每天重置的计数器。具体步骤如下: 1. 在Redis中设置计数器的初始值,例如: ``` set counter 0 ``` 2. 利用Lua脚本编写一个脚本,实现计数器的自增,并且在每天零点时将计数器重置为0。脚本示例如下: ``` local current = tonumber(redis.call('get', KEYS[1])) or 0 local result = current + tonumber(ARGV[1]) redis.call('set', KEYS[1], result) if redis.call('ttl', KEYS[1]) == -1 then redis.call('expire', KEYS[1], ARGV[2]) end return result ``` 该脚本接受两个参数:计数器的键名和自增的值。该脚本会先获取当前计数器的值,然后将其加上自增的值,最后将结果保存回Redis中。如果计数器的过期时间(ttl)为-1,说明计数器没有设置过期时间,此时将计数器的过期时间设置为一天,即86400秒。 3. 调用Lua脚本实现计数器的自增,例如: ``` eval "脚本内容" 1 counter 1 86400 ``` 该命令调用了名为“脚本内容”的Lua脚本,将计数器的键名设置为“counter”,自增的值设置为1,过期时间设置为86400秒(即一天)。 这样,每次调用Lua脚本都会自动更新计数器的值,并且在每天零点时将计数器重置为0。 ### 回答2: Redis计数器可以用来实现每天重置重新开始的功能。具体实现步骤如下: 1. 首先,需要使用Redis的String类型来存储计数器的值。可以将计数器的名称作为键,计数器的值作为值,存储在Redis中。 2. 当每天开始时,需要将计数器的值重置为0。可以通过使用Redis的SET命令,将计数器的值设置为0来实现。 3. 当需要对计数器进行增加操作时,可以使用Redis的INCR命令。该命令会将计数器的值自增1,并返回增加后的值。 4. 当需要获取计数器的当前值时,可以使用Redis的GET命令,获取计数器的值。 5. 当每天结束时,如果需要重新开始计数,需要再次执行第二步的操作,将计数器的值重置为0。 通过以上步骤,可以实现每天重置重新开始的计数器功能。在每天开始时,将计数器重置为0,然后通过对计数器进行增加操作,可以实现每天计数的功能。当计数器的值不再需要时,可以通过使用Redis的DEL命令,将计数器Redis中移除。 ### 回答3: Redis计数器可以使用expire命令来实现每天重置重新开始的功能。首先,我们可以使用incr命令对计数器进行自增操作。每当计数器需要重置时,可以使用expire命令设置计数器的过期时间为第二天的零点。具体的实现步骤如下: 1. 首先,使用INCR命令将计数器自增,例如每次请求时可以执行INCR计数器的命令。可以将计数器的key设置为"counter:day:X",其中X是当天的日期,这样可以每天都创建一个新的计数器。 2. 接下来,使用TTL命令获取计数器的剩余过期时间。 3. 如果计数器的剩余过期时间小于等于0或者计数器不存在,说明需要重置计数器,可以执行EXPIRE命令将计数器的过期时间设置为第二天的零点。计数器的key可以在设置过期时间时使用通配符,例如"counter:day:*",将所有当天创建的计数器都重置。 通过这样的方式,我们可以保证每天计数器会在零点重新开始,实现每天重置的功能。同时,使用Redis计数器还可以实现其他一些功能,例如实时查看当前计数器的值、累加计数器等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

当年的春天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值