基于Redis实现高并发计数器(lua脚本版)

1、业务需求背景

一个手机号一天限制发送5条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。

2、代码实现

2.1、RedisConfig.java

package com.demo.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
          
   

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
          
   
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // key采用String的序列化方式
        template.setKeySerializer(new StringRedisSerializer());
        // value序列化方式采用jackson
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

}

2.2、RedisController.java

package com.demo.limit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;

@RestController
@RequestMapping("/redis")
public class RedisController {
          
   

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @RequestMapping(value = "/redisIncr/{key}/{maxCount}/{expire}")
    public Long redisIncr(@PathVariable("key") String key, @PathVariable("maxCount") Integer maxCount, @PathVariable("expire") Integer expire) {
          
   
        Long result = null;
        try {
          
   
            //调用lua脚本并执行
            DefaultRedisScript<Long> limitRedisScript = new DefaultRedisScript<>();
            limitRedisScript.setResultType(Long.class);//返回类型是Long
            //redis_incr.lua文件存放在resources目录下的redis文件夹内
            limitRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/redis_incr.lua")));
            result = redisTemplate.execute(limitRedisScript, Arrays.asList(key), maxCount, expire);
            System.out.println("==" + result);
        } catch (Exception e) {
          
   
            e.printStackTrace();
        }
        return result;
    }

}

2.3、redis_incr.lua

-- 自增1
local times = redis.call("incr", KEYS[1])
-- 数值为1的时候设置KEY的超时时间
if times == 1 then
    redis.call(expire,KEYS[1],ARGV[2])
end
-- 判断是否超过设置的最大次数,如果是返回1
if times > tonumber(ARGV[1]) then
    return 1
end
-- 默认返回0表示没有超次数
return 0

3、测试效果

浏览器中连续敲以下链接6次,查看窗口和控制台打印的值,以下只截取部分过程图片 /key123/5/3000 表示 key123的计数器,超时时间为3000秒,计数超过5则返回1

4、总结

如果先incr命令自增次数,在设置expire失效时间时由于网络等原因没有将命令提交成功,就产生了一个永不过期的计数器KEY; 使用lua脚本使得set命令和expire命令一同到达Redis被执行且不会被干扰,在很大程度上保证了原子操作;
经验分享 程序员 微信小程序 职场和发展