diff --git a/budd-common/src/main/java/io/github/ehlxr/common/Result.java b/budd-common/src/main/java/io/github/ehlxr/common/Result.java index 126889e..c99632c 100644 --- a/budd-common/src/main/java/io/github/ehlxr/common/Result.java +++ b/budd-common/src/main/java/io/github/ehlxr/common/Result.java @@ -15,7 +15,7 @@ import java.util.Objects; /** * 统一输出结果集 * - * @author lixiangrong + * @author ehlxr * @since 2020/3/18. */ @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/budd-common/src/main/java/io/github/ehlxr/enums/CodeEnum.java b/budd-common/src/main/java/io/github/ehlxr/enums/CodeEnum.java index 5b5a7dc..1d2e834 100644 --- a/budd-common/src/main/java/io/github/ehlxr/enums/CodeEnum.java +++ b/budd-common/src/main/java/io/github/ehlxr/enums/CodeEnum.java @@ -27,7 +27,7 @@ package io.github.ehlxr.enums; /** * 自定义状态码 * - * @author lixiangrong + * @author ehlxr * @since 2020/3/18. */ public enum CodeEnum { @@ -36,71 +36,6 @@ public enum CodeEnum { */ 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 String message; @@ -164,10 +64,6 @@ public enum CodeEnum { return code; } - public String getCodeString() { - return code + ""; - } - public String getMessage() { return message; } diff --git a/budd-common/src/main/java/io/github/ehlxr/util/RedisUtil.java b/budd-common/src/main/java/io/github/ehlxr/util/RedisUtil.java index 231805a..a42e444 100644 --- a/budd-common/src/main/java/io/github/ehlxr/util/RedisUtil.java +++ b/budd-common/src/main/java/io/github/ehlxr/util/RedisUtil.java @@ -24,9 +24,13 @@ 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; @@ -40,13 +44,14 @@ import java.util.concurrent.TimeUnit; * @author ehlxr * @since 2021-08-29 18:32. */ -@SuppressWarnings("unused") @Component public class RedisUtil { + private static final Logger log = LoggerFactory.getLogger(RedisUtil.class); + /** * 解锁 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; " + "end; " + "local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1); " + @@ -85,6 +90,139 @@ public class RedisUtil { "return 0;"; private static RedisTemplate redisTemplate; + /** + * 释放分布式锁时使用的 lua 脚本,保证原子性 + *

+ * 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 脚本,保证原子性 + *

+ * 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 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 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 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); + } + /** * 加锁 不可重入 * diff --git a/budd-demo/pom.xml b/budd-demo/pom.xml index 3d3f349..64a4e1d 100644 --- a/budd-demo/pom.xml +++ b/budd-demo/pom.xml @@ -10,4 +10,10 @@ budd-demo budd-demo + + + io.github.ehlxr + budd-common + + diff --git a/budd-server/pom.xml b/budd-server/pom.xml index b1022fd..6ae1740 100644 --- a/budd-server/pom.xml +++ b/budd-server/pom.xml @@ -37,7 +37,7 @@ io.github.ehlxr - budd-common + budd-demo diff --git a/budd-server/src/main/java/io/github/ehlxr/controller/redis/RedisController.java b/budd-server/src/main/java/io/github/ehlxr/controller/redis/RedisController.java new file mode 100644 index 0000000..3619845 --- /dev/null +++ b/budd-server/src/main/java/io/github/ehlxr/controller/redis/RedisController.java @@ -0,0 +1,77 @@ +/* + * The MIT License (MIT) + * + * Copyright © 2021 xrv + * + * 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(); + } +} diff --git a/pom.xml b/pom.xml index d26b9bf..041b1c2 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,12 @@ budd-common ${project.version} + + + io.github.ehlxr + budd-demo + ${project.version} +