budd/budd-common/src/main/java/io/github/ehlxr/util/RedisUtil.java

312 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* The MIT License (MIT)
*
* Copyright © 2021 xrv <xrv@live.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package io.github.ehlxr.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.StaticScriptSource;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* redis 操作
*
* @author ehlxr
* @since 2021-08-29 18:32.
*/
@Component
public class RedisUtil {
private static final Logger log = LoggerFactory.getLogger(RedisUtil.class);
/**
* 解锁 lua 脚本
*/
private static final String UNLOCK = "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then " +
"return nil; " +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1); " +
"if (counter > 0) then " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"return 1; " +
"end; " +
"return nil;";
/**
* 锁前缀
*/
private static final String LOCK_PREFIX = "LOCK_";
/**
* 默认重试次数
*/
private static final Integer DEFAULT_RETRIES = 1;
/**
* 默认 10毫秒
*/
private static final Long DEFAULT_INTERVAL = 10L;
/**
* 加锁 lua 脚本
*/
private static final String LOCK = "if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;";
private static RedisTemplate<String, String> redisTemplate;
/**
* 释放分布式锁时使用的 lua 脚本,保证原子性
* <p>
* if (redis.call('get', KEYS[1]) == ARGV[1])
* then
* return redis.call('del', KEYS[1])
* else
* return 0
* end
*/
private static final String RELEASE_LOCK_LUA = "if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
/**
* 滑动窗口限流使用的 lua 脚本,保证原子性
* <p>
* local key = KEYS[1];
* local index = tonumber(ARGV[1]);
* local time_window = tonumber(ARGV[2]);
* local now_time = tonumber(ARGV[3]);
* local far_time = redis.call('lindex', key, index);
* if (not far_time)
* then
* redis.call('lpush', key, now_time);
* redis.call('pexpire', key, time_window+1000);
* return 1;
* end
* if (now_time - far_time > time_window)
* then
* redis.call('rpop', key);
* redis.call('lpush', key, now_time);
* redis.call('pexpire', key, time_window+1000);
* return 1;
* else
* return 0;
* end
*/
private static final String SLIDE_WINDOW_LUA = "local key = KEYS[1];\n" + "local index = tonumber(ARGV[1]);\n" + "local time_window = tonumber(ARGV[2]);\n" + "local now_time = tonumber(ARGV[3]);\n" + "local far_time = redis.call('lindex', key, index);\n" + "if (not far_time)\n" + "then\n" + " redis.call('lpush', key, now_time);\n" + " redis.call('pexpire', key, time_window+1000);\n" + " return 1;\n" + "end\n" + "\n" + "if (now_time - far_time > time_window)\n" + "then\n" + " redis.call('rpop', key);\n" + " redis.call('lpush', key, now_time);\n" + " redis.call('pexpire', key, time_window+1000);\n" + " return 1;\n" + "else\n" + " return 0;\n" + "end";
/**
* 获取分布式锁
*
* @param key key
* @param value value需要保证全局唯一用来删除分布式锁时判断身份使用
* @param expireTime 锁过期时间,毫秒,防止业务崩溃未删除锁,导致死锁
* @return 是否获取成功锁
*/
public static boolean getDistributedLock(String key, String value, long expireTime) {
boolean result = false;
try {
Boolean set = redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS);
result = set == null || set;
log.info("getLock redis key: {}, value: {}, expireTime: {}, result: {}", key, value, expireTime, result);
} catch (Exception e) {
log.error("getLock redis key: {}, value: {}, expireTime: {}", key, value, expireTime, e);
}
return result;
}
/**
* 释放分布式锁
*
* @param key key
* @param value value需要和获取锁时传入的一致
* @return 是否释放成功锁
*/
public static boolean releaseDistributedLock(String key, String value) {
Long execute = null;
try {
RedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_LOCK_LUA, Long.class);
execute = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
log.debug("releaseLock redis key: {}, value: {}, result: {}", key, value, execute);
} catch (Exception e) {
log.error("releaseLock redis key: {}, value: {}", key, value, e);
}
return Long.valueOf(1L).equals(execute);
}
/**
* 分布式限流队列,在时间窗口内(包含该时间点),判断是否达到限流的阀值
* 本接口实现的方法通过加锁避免并发问题,性能不高。只是为了说明限流逻辑如何实现
*
* @param key key
* @param count 限流阀值
* @param timeWindow 限流时间窗口
* @return 是否允许通过(通过即不进行限流)
*/
public static synchronized boolean slideWindow(String key, int count, long timeWindow) {
try {
long nowTime = System.currentTimeMillis();
ListOperations<String, String> list = redisTemplate.opsForList();
String farTime = list.index(key, count - 1);
if (farTime == null) {
list.leftPush(key, String.valueOf(nowTime));
redisTemplate.expire(key, timeWindow + 1000L, TimeUnit.MILLISECONDS);
return true;
}
if (nowTime - Long.parseLong(farTime) > timeWindow) {
list.rightPop(key);
list.leftPush(key, String.valueOf(nowTime));
redisTemplate.expire(key, timeWindow + 1000L, TimeUnit.MILLISECONDS);
return true;
}
return false;
} catch (Exception e) {
log.error("", e);
return false;
}
}
/**
* 分布式限流队列,在时间窗口内(包含该时间点),判断是否达到限流的阀值
* 本接口实现的方法通过 Lua 脚本避免并发问题,性能较高。
*
* @param key key
* @param count 限流阀值
* @param timeWindow 限流时间窗口
* @return 是否允许通过(通过即不进行限流)
*/
public static boolean slideWindowLua(String key, int count, long timeWindow) {
if (count <= 0 || timeWindow <= 0) {
return false;
}
Long execute = null;
try {
RedisScript<Long> redisScript = new DefaultRedisScript<>(SLIDE_WINDOW_LUA, Long.class);
execute = redisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(count - 1), String.valueOf(timeWindow), String.valueOf(System.currentTimeMillis()));
log.debug("slideWindowLua redis key: {}, count: {}, timeWindow: {}, result: {}", key, count, timeWindow, execute);
} catch (Exception e) {
log.error("slideWindowLua redis key: {}, count: {}, timeWindow: {}", key, count, timeWindow, e);
}
return Long.valueOf(1L).equals(execute);
}
/**
* 加锁 不可重入
*
* @param timeout 毫秒
*/
public static Boolean lock(String key, long timeout) {
return redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + key, "1", timeout, TimeUnit.MILLISECONDS);
}
/**
* 解锁 不可重入
*/
public static Boolean unlock(String key) {
return redisTemplate.delete(LOCK_PREFIX + key);
}
/**
* 加锁
*
* @param reentrantId 重入Id
* 超时时间 毫秒ms
*/
public static Boolean lock(String key, String reentrantId, long timeout) {
return lock(key, reentrantId, timeout, DEFAULT_RETRIES);
}
/**
* 加锁
*
* @param reentrantId 重入Id
* 超时时间 毫秒ms
* @param retries 重试次数
*/
public static Boolean lock(String key, String reentrantId, long timeout, int retries) {
return lock(key, reentrantId, timeout, retries, DEFAULT_INTERVAL);
}
/**
* 加锁
*
* @param reentrantId 重入Id
* 超时时间 毫秒ms
* @param retries 重试次数
* @param interval 每次重试间隔时间 毫秒
*/
public static Boolean lock(String key, String reentrantId, long timeout, int retries, long interval) {
String lockKey = LOCK_PREFIX + key;
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
script.setScriptSource(new StaticScriptSource(LOCK));
for (int i = 0; i < retries; i++) {
Object result = redisTemplate.execute(script, Collections.singletonList(lockKey), String.valueOf(timeout), reentrantId);
if (Objects.nonNull(result) && Objects.equals(1L, Long.valueOf(result.toString()))) {
return true;
}
try {
TimeUnit.MILLISECONDS.sleep(interval);
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}
return false;
}
/**
* 解锁
*
* @param reentrantId 重入ID
*/
public static Boolean unlock(String key, String reentrantId) {
String lockKey = LOCK_PREFIX + key;
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
script.setScriptSource(new StaticScriptSource(UNLOCK));
Object result = redisTemplate.execute(script, Collections.singletonList(lockKey), reentrantId);
if (Objects.isNull(result)) {
return null;
}
return Objects.equals(1L, Long.valueOf(result.toString()));
}
@Autowired
public void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {
RedisUtil.redisTemplate = redisTemplate;
}
}