簡單了解下redis嵌入lua腳本(隨便百度扒的):
Redis支持的LUA腳本與其優(yōu)勢
redis嵌入lua官方文檔
Redis悲觀鎖芍秆、樂觀鎖和調(diào)用Lua腳本的優(yōu)缺點
序言
本教程本著人和代碼其中一個能跑就行的原則螟碎。人菜勿噴迹栓,只接受技術(shù)性建議克伊。
近期看到了一些關(guān)于redis的文章說:哇redis牛皮愿吹、redis怎么這么快犁跪、哦喲坷衍!redis6竟然支持多線程了枫耳。迁杨。铅协。然后問題來了:多線程狐史?線程安全問題?哦喲侈玄,百度百度了解了下序仙,原來如此潘悼,此多線程非彼多線程(自己百度去治唤,這里不說這個)宾添。然后就看到了嵌入lua腳本這個騷操作,在繼續(xù)百度了下疙挺,哎喲铐然,不錯喲搀暑!拿來吧你。
蹭個熱度:吳簽和某時間管理大師參與多人運動(多線程)時已出現(xiàn)一個很細小(線程安全)的針眼冈绊,我的有點大埠啃,請你忍耐一下碴开,一夜之間讓花季程序猿痛苦流淚博秫。
正文
通過redis嵌入lua腳本,實現(xiàn)簡單的限流即寒、黑名單功能召噩。別說這也沒有那也沒有凹嘲,個性化功能自行開發(fā)周蹭。
其中有點小坑谷醉,最后再說。
說那么多萎攒,不如直接丟代碼耍休。Talk is cheap. Show me the code.
測試環(huán)境
win11
jdk8
Redis server v=5.0.9
springboot 2.4.7
Show me the code.
先來看看lua腳本
lua腳本存放在項目的resource目錄下的lua文件夾下面(路徑可以自己改,下面SelfRedisScript .java里面改成對應(yīng)的就行)
--- lua腳本:限流喧锦、黑名單專用燃少,慎改
--- 用于高并發(fā)情況下保證redis線程安全
--- 注意:
--- 1阵具、redis反序列化問題
--- 2阳液、完成lua腳本后帘皿,請在本地測試無誤后再提交代碼
--- 3、若lua腳本執(zhí)行報錯越庇,redis不會回滾已經(jīng)執(zhí)行的命令
-- 獲取傳遞進來的參數(shù)
local countKey = KEYS[1]
if countKey == nil then
return true
end
-- 獲取傳遞進來的閾值
local requestCount = KEYS[2]
-- 獲取傳遞進來的過期時間ttl
local requestTtl = KEYS[3]
-- 獲取redis參數(shù)
local countVal = redis.call('GET', countKey)
-- 如果不是第一次請求
if countVal then
-- 由于lua腳本接收到參數(shù)都會轉(zhuǎn)為String奉狈,所以要轉(zhuǎn)成數(shù)字類型才能比較
local numCountVal = tonumber(countVal)
-- 如果超過指定閾值桑驱,則返回true
if numCountVal >= tonumber(requestCount) then
return true
else
numCountVal = numCountVal + 1
redis.call('SETEX', countKey, requestTtl, numCountVal)
end
else
redis.call('SETEX', countKey, requestTtl, 1)
end
return false
Java代碼(SelfRedisScript .java)注入RedisScript
@Component
public class SelfRedisScript {
@Bean("redisScriptBoolean")
public DefaultRedisScript<Boolean> redisScriptBoolean() {
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit_blacklisted.lua")));
redisScript.setResultType(Boolean.class);
return redisScript;
}
}
Java代碼(RedisTemplateConfig.java)簡單配置RedisTemplate
@EnableCaching
@Configuration
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisTemplateConfig {
@Bean
@Primary
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
準備工作做完后,開始實現(xiàn)簡單的限流押框、黑名單
在過濾器里面實現(xiàn)功能
CommonConstants類中的常量、ApplicationConfig從application.yml從獲取值的代碼就不貼出來了兑徘,
WebUtils.returnResponse單獨列出來了,自行修改補充崭闲。
RedisConstants常量:/** * 限流機制key前綴 REQUEST_LIMIT:127.0.0.1:/api/test */ String REQUEST_LIMIT = "REQUEST_LIMIT:%s:%s"; /** * 黑名單機制key前綴 REQUEST_LIMIT:127.0.0.1: */ String REQUEST_BLACKLISTED = "REQUEST_BLACKLISTED:%s";
@Slf4j
public class RequestLimitFilter implements Filter {
private final ApplicationConfig applicationConfig;
private Long limitTimeSeconds;
private Integer limitCount;
private Long blacklistedTimeSeconds;
private Integer blacklistedCount;
private List<String> limitIgnores;
private RedisTemplate<String, Object> redisTemplate;
private DefaultRedisScript<Boolean> script;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestURI = request.getRequestURI();
if (!WebUtils.uriMatch(this.limitIgnores, requestURI)) {
// 獲取ip
String realIp = WebUtils.getIP(request);
// 黑名單限制
String blacklistedKey = String.format(RedisConstants.REQUEST_BLACKLISTED, realIp);
// key為空返回true,超過指定閾值返回true忘伞,其他返回false
Boolean blackPass = getPass(blacklistedKey, blacklistedCount, blacklistedTimeSeconds);
if (blackPass) {
WebUtils.returnResponse(response, JSONUtil.toJsonStr(R.failed(StatusCode.BLACKLISTED)));
return;
}
// 限流限制
String limitKey = String.format(RedisConstants.REQUEST_LIMIT, realIp, requestURI);
Boolean limitPass = getPass(limitKey, limitCount, limitTimeSeconds);
if (limitPass) {
WebUtils.returnResponse(response, JSONUtil.toJsonStr(R.failed(StatusCode.LIMITED)));
return;
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
Filter.super.destroy();
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 限流
this.limitCount = ObjectUtil.isNull(applicationConfig.getLimitCount())
? CommonConstants.REQUEST_LIMIT_COUNT : applicationConfig.getLimitCount();
this.limitTimeSeconds = ObjectUtil.isNull(applicationConfig.getLimitTimeSeconds())
? CommonConstants.REQUEST_LIMIT_TIME_SECONDS : applicationConfig.getLimitTimeSeconds();
// 黑名單
this.blacklistedCount = ObjectUtil.isNull(applicationConfig.getBlacklistedCount())
? CommonConstants.REQUEST_BLACKLISTED_COUNT : applicationConfig.getBlacklistedCount();
this.blacklistedTimeSeconds = ObjectUtil.isNull(applicationConfig.getBlacklistedTimeSeconds())
? CommonConstants.REQUEST_BLACKLISTED_TIME_SECONDS : applicationConfig.getBlacklistedTimeSeconds();
// 過濾請求薄翅,從application.yml從獲取值
this.limitIgnores = IterUtil.isEmpty(applicationConfig.getLimitIgnores())
? Collections.emptyList() : applicationConfig.getLimitIgnores();
// lua
this.redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
this.script = SpringContextHolder.getBean("redisScriptBoolean");
Filter.super.init(filterConfig);
}
public RequestLimitFilter(ApplicationConfig applicationConfig) {
this.applicationConfig = applicationConfig;
}
/**
* 調(diào)用lua腳本沙兰,獲取執(zhí)行結(jié)果
* @param key 緩存key
* @param count 請求閾值
* @param timeSeconds 攔截時間
* @return 執(zhí)行結(jié)果
*/
private Boolean getPass(String key, Integer count, Long timeSeconds) {
Boolean execute = redisTemplate.execute(script, Arrays.asList(key, String.valueOf(count), String.valueOf(timeSeconds)));
return execute == null ? true : execute;
}
}
// -------------------------------------------WebUtils工具類---------------------------------------------
public void returnResponse(HttpServletResponse response, String data) {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try (PrintWriter writer = response.getWriter()) {
// 通過 PrintWriter 將 data 數(shù)據(jù)直接 print 回去
writer.print(data);
} catch (IOException ignored) {
}
}
public String getIP(HttpServletRequest request) {
Assert.notNull(request, "HttpServletRequest is null");
String ip = request.getHeader(HEADER_X_REQUESTED_FOR);
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader(HEADER_X_FORWARDED_FOR);
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader(HEADER_PROXY_CLIENT_IP);
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader(HEADER_WL_PROXY_CLIENT_IP);
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader(HEADER_HTTP_CLIENT_IP);
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader(HEADER_HTTP_X_FORWARDED_FOR);
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return StrUtil.isBlank(ip) ? null : ip.split(",")[0];
}
最后注冊下RequestLimitFilter.java這個過濾器
@Component
@AllArgsConstructor
public class FilterRegistration {
private final ApplicationConfig applicationConfig;
@Bean
public FilterRegistrationBean<RequestLimitFilter> requestLimitFilter() {
FilterRegistrationBean<RequestLimitFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RequestLimitFilter(applicationConfig));
registration.addUrlPatterns("/*");
registration.setName("RequestLimitFilter");
registration.setOrder(1);
return registration;
}
}
展示下成果(計算規(guī)則自行調(diào)整)
請求即記錄
請求即記錄
時間段內(nèi)多次請求達到限流指定的請求閾值
達到限流指定的請求閾值
時間段內(nèi)多次請求已被限流后,繼續(xù)請求達到黑名單指定的請求閾值
達到黑名單指定的請求閾值
注意事項
- RedisTemplate配置的序列化問題
如果配置的是JdkSerializationRedisSerializer翘魄,就需要改成StringRedisSerializer鼎天,如果需要兩者兼容,那
就再給spring丟一個名為jdkRedisSerializer的Bean暑竟,然后在 @Autowired時但荤,添加@Qualifier("jdkRedisSerializer")指定注入Bean@Bean("jdkRedisSerializer") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer()); redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; }
- lua腳本執(zhí)行報錯問題
若lua腳本執(zhí)行報錯哑了,redis不會回滾已經(jīng)執(zhí)行的命令炕淮,所以在完成lua腳本后,請在本地測試無誤后再提交代碼