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
http://127.0.0.1:8080/redis/redisIncr/key123/5/3000
4、总结
- 如果先incr命令自增次数,在设置expire失效时间时由于网络等原因没有将命令提交成功,就产生了一个永不过期的计数器KEY;
- 使用lua脚本使得set命令和expire命令一同到达Redis被执行且不会被干扰,在很大程度上保证了原子操作;