add redis lock test
parent
9d6a001b85
commit
f601c051d4
|
@ -15,7 +15,7 @@ import java.util.Objects;
|
||||||
/**
|
/**
|
||||||
* 统一输出结果集
|
* 统一输出结果集
|
||||||
*
|
*
|
||||||
* @author lixiangrong
|
* @author ehlxr
|
||||||
* @since 2020/3/18.
|
* @since 2020/3/18.
|
||||||
*/
|
*/
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
|
|
@ -27,7 +27,7 @@ package io.github.ehlxr.enums;
|
||||||
/**
|
/**
|
||||||
* 自定义状态码
|
* 自定义状态码
|
||||||
*
|
*
|
||||||
* @author lixiangrong
|
* @author ehlxr
|
||||||
* @since 2020/3/18.
|
* @since 2020/3/18.
|
||||||
*/
|
*/
|
||||||
public enum CodeEnum {
|
public enum CodeEnum {
|
||||||
|
@ -36,71 +36,6 @@ public enum CodeEnum {
|
||||||
*/
|
*/
|
||||||
SUCCESSFUL(200, "success"),
|
SUCCESSFUL(200, "success"),
|
||||||
|
|
||||||
LOGIN_TYPE_ERROR(201, "登录类型错误"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户不存在
|
|
||||||
*/
|
|
||||||
USER_NOT_EXIST(202, "用户不存在"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 医生状态修改失败
|
|
||||||
*/
|
|
||||||
DOCTOR_STATUS_FAILURE(204,"医生状态修改失败"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 医生状态不正常
|
|
||||||
*/
|
|
||||||
DOCTOR_STATUS_NOT_NORMAL(205,"医生状态不正常"),
|
|
||||||
|
|
||||||
DOCTOR_ID_EMPTY(206,"医生不允许为空"),
|
|
||||||
|
|
||||||
DEVICE_LOGIN_NULL(209, "设备尚未登录"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功
|
|
||||||
*/
|
|
||||||
UC_CONNECT_ERROR(211, "用户中心异常,请联系管理员"),
|
|
||||||
|
|
||||||
UC_NOT_PHRID(212,"用户中心异常,未返回用户ID"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 诊断记录不存在
|
|
||||||
*/
|
|
||||||
RAINQUIRY_NOT_EXIST(301, "诊断记录不存在"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功
|
|
||||||
*/
|
|
||||||
LOGIN_TYPE_NULL(700, "登录方式不能为空"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功
|
|
||||||
*/
|
|
||||||
MOBILE_NULL(701, "手机号码不能为空"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功
|
|
||||||
*/
|
|
||||||
VCODE_ERROR(702, "验证码错误"),
|
|
||||||
|
|
||||||
MOBILE_HAVE(703, "此手机号已经注册过"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功
|
|
||||||
*/
|
|
||||||
VCODE_NULL_ERROR(704, "验证码不能为空"),
|
|
||||||
/**
|
|
||||||
* 成功
|
|
||||||
*/
|
|
||||||
GENDER_RELATION_ERROR(705, "性别和关系错误"),
|
|
||||||
|
|
||||||
ID_CARD_NO_ERROR(706, "身份证号不能为空"),
|
|
||||||
|
|
||||||
HEAD_IMG_ERROR(707, "人脸识别照片不能为空"),
|
|
||||||
|
|
||||||
DEVICE_CODE_ERROR(708, "设备号不能为空"),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 未知异常
|
* 未知异常
|
||||||
*/
|
*/
|
||||||
|
@ -114,43 +49,8 @@ public enum CodeEnum {
|
||||||
/**
|
/**
|
||||||
* 业务异常
|
* 业务异常
|
||||||
*/
|
*/
|
||||||
SERVICE_EXCEPTION(602, "service exception"),
|
SERVICE_EXCEPTION(602, "service exception");
|
||||||
|
|
||||||
/**
|
|
||||||
* 上善(三疗)的token没有获取到
|
|
||||||
*/
|
|
||||||
NO_SANLIAO_TOKEN(801, "上善(三疗)的Token没有"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No report generated
|
|
||||||
*/
|
|
||||||
NO_REPORT_GENERATED(603, "no report generated"),
|
|
||||||
|
|
||||||
NO_MATCH_DOCTOR(604,"没有空闲医生"),
|
|
||||||
|
|
||||||
NO_MATCH_DATA(604,"没有查询到数据"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 未选择全部检查项
|
|
||||||
*/
|
|
||||||
SELECT_EXAM_WRAN(2001, "未选择全部检查项"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建订单失败
|
|
||||||
*/
|
|
||||||
CREATE_ORDER_FAIL(2002,"创建订单失败"),
|
|
||||||
|
|
||||||
PENDING_ORDER_ERROR(2003,"抢单失败"),
|
|
||||||
|
|
||||||
INVALID_ORDER_ERROR(2004,"无效订单"),
|
|
||||||
|
|
||||||
INVALID_SHOP_ROLE_ERROR(2005,"当前用户无商铺管理员角色"),
|
|
||||||
|
|
||||||
DUPLICATE_VOUCHING_ORDER_ERROR(2006,"您已审核过此订单"),
|
|
||||||
|
|
||||||
GET_ORDER_ERROR(2007,"获取订单信息失败"),
|
|
||||||
|
|
||||||
GET_SHOP_ROLE_ERROR(2008,"获取商铺角色失败");
|
|
||||||
|
|
||||||
private final int code;
|
private final int code;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
@ -164,10 +64,6 @@ public enum CodeEnum {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCodeString() {
|
|
||||||
return code + "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,13 @@
|
||||||
|
|
||||||
package io.github.ehlxr.util;
|
package io.github.ehlxr.util;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.RedisTemplate;
|
||||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
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.scripting.support.StaticScriptSource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@ -40,13 +44,14 @@ import java.util.concurrent.TimeUnit;
|
||||||
* @author ehlxr
|
* @author ehlxr
|
||||||
* @since 2021-08-29 18:32.
|
* @since 2021-08-29 18:32.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@Component
|
@Component
|
||||||
public class RedisUtil {
|
public class RedisUtil {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(RedisUtil.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解锁 lua 脚本
|
* 解锁 lua 脚本
|
||||||
*/
|
*/
|
||||||
public static final String UNLOCK = "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then " +
|
private static final String UNLOCK = "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then " +
|
||||||
"return nil; " +
|
"return nil; " +
|
||||||
"end; " +
|
"end; " +
|
||||||
"local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1); " +
|
"local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1); " +
|
||||||
|
@ -85,6 +90,139 @@ public class RedisUtil {
|
||||||
"return 0;";
|
"return 0;";
|
||||||
private static RedisTemplate<String, String> redisTemplate;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加锁 不可重入
|
* 加锁 不可重入
|
||||||
*
|
*
|
||||||
|
|
|
@ -10,4 +10,10 @@
|
||||||
<artifactId>budd-demo</artifactId>
|
<artifactId>budd-demo</artifactId>
|
||||||
<name>budd-demo</name>
|
<name>budd-demo</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.ehlxr</groupId>
|
||||||
|
<artifactId>budd-common</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.ehlxr</groupId>
|
<groupId>io.github.ehlxr</groupId>
|
||||||
<artifactId>budd-common</artifactId>
|
<artifactId>budd-demo</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright © 2021 xrv <xrg@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.controller.redis;
|
||||||
|
|
||||||
|
import io.github.ehlxr.util.RedisUtil;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ehlxr
|
||||||
|
* @since 2021-08-30 10:08.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@Api(tags = "Redis 测试接口")
|
||||||
|
@RequestMapping("/redis")
|
||||||
|
public class RedisController {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(RedisController.class);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 锁测试
|
||||||
|
*/
|
||||||
|
@ApiOperation(value = "Redis 锁", notes = "Redis 锁测试")
|
||||||
|
@GetMapping("/lock/test")
|
||||||
|
public void lockTest() {
|
||||||
|
Runnable runnable = () -> {
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
log.info("{} retry lock...", Thread.currentThread().getName());
|
||||||
|
boolean lock = RedisUtil.getDistributedLock("lock", uuid, 30_000);
|
||||||
|
if (lock) {
|
||||||
|
log.info("{} get lock!", Thread.currentThread().getName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(1);
|
||||||
|
// ...
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
Boolean locak = RedisUtil.releaseDistributedLock("lock", uuid);
|
||||||
|
log.info("{} release lock {}", Thread.currentThread().getName(), locak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
new Thread(runnable, "t1").start();
|
||||||
|
new Thread(runnable, "t2").start();
|
||||||
|
}
|
||||||
|
}
|
6
pom.xml
6
pom.xml
|
@ -53,6 +53,12 @@
|
||||||
<artifactId>budd-common</artifactId>
|
<artifactId>budd-common</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.ehlxr</groupId>
|
||||||
|
<artifactId>budd-demo</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue