基于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被执行且不会被干扰,在很大程度上保证了原子操作;
下一篇:
架构设计(容量分析)