diff --git a/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java b/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java index 14cfa31..6f3b732 100644 --- a/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java +++ b/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java @@ -12,6 +12,7 @@ import xyz.playedu.api.constant.SystemConstant; import xyz.playedu.api.domain.User; import xyz.playedu.api.event.UserLoginEvent; import xyz.playedu.api.exception.LimitException; +import xyz.playedu.api.middleware.Lock; import xyz.playedu.api.request.frontend.LoginPasswordRequest; import xyz.playedu.api.service.JWTService; import xyz.playedu.api.service.UserService; @@ -45,6 +46,7 @@ public class LoginController { private UserLoginCache userLoginCache; @PostMapping("/password") + @Lock(key = "user-login:{#req.getEmail()}") public JsonResponse password(@RequestBody @Validated LoginPasswordRequest req) throws LimitException { String email = req.getEmail(); userLoginCache.check(email); diff --git a/src/main/java/xyz/playedu/api/middleware/Lock.java b/src/main/java/xyz/playedu/api/middleware/Lock.java new file mode 100644 index 0000000..b68a9be --- /dev/null +++ b/src/main/java/xyz/playedu/api/middleware/Lock.java @@ -0,0 +1,21 @@ +package xyz.playedu.api.middleware; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * @Author 杭州白书科技有限公司 + * @create 2023/3/12 10:44 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Lock { + String key(); + + long expire() default 10; + + TimeUnit timeUnit() default TimeUnit.SECONDS; +} diff --git a/src/main/java/xyz/playedu/api/middleware/impl/LockImpl.java b/src/main/java/xyz/playedu/api/middleware/impl/LockImpl.java new file mode 100644 index 0000000..0d1fa4c --- /dev/null +++ b/src/main/java/xyz/playedu/api/middleware/impl/LockImpl.java @@ -0,0 +1,46 @@ +package xyz.playedu.api.middleware.impl; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import xyz.playedu.api.exception.LimitException; +import xyz.playedu.api.middleware.Lock; +import xyz.playedu.api.util.RedisDistributedLock; + +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; + +/** + * @Author 杭州白书科技有限公司 + * @create 2023/3/12 10:45 + */ +@Aspect +@Component +public class LockImpl { + private final RedisDistributedLock redisDistributedLock; + + public LockImpl(RedisDistributedLock redisDistributedLock) { + this.redisDistributedLock = redisDistributedLock; + } + + @Around("@annotation(xyz.playedu.api.middleware.Lock)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + Lock distributedLock = method.getAnnotation(Lock.class); + String key = distributedLock.key(); + long expire = distributedLock.expire(); + TimeUnit timeUnit = distributedLock.timeUnit(); + boolean success = redisDistributedLock.tryLock(key, expire, timeUnit); + if (!success) { + throw new LimitException("请稍后再试"); + } + try { + return joinPoint.proceed(); + } finally { + redisDistributedLock.releaseLock(key); + } + } +} diff --git a/src/main/java/xyz/playedu/api/util/RedisDistributedLock.java b/src/main/java/xyz/playedu/api/util/RedisDistributedLock.java new file mode 100644 index 0000000..a68a3ed --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/RedisDistributedLock.java @@ -0,0 +1,52 @@ +package xyz.playedu.api.util; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + + +/** + * @Author 杭州白书科技有限公司 + * @create 2023/3/12 10:43 + */ +@Component +public class RedisDistributedLock { + + private final StringRedisTemplate redisTemplate; + private final ThreadLocal lockValue = new ThreadLocal<>(); + + public RedisDistributedLock(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public boolean tryLock(String key, long expire, TimeUnit timeUnit) { + String value = UUID.randomUUID().toString(); + Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, expire, timeUnit); + if (Boolean.TRUE.equals(success)) { + lockValue.set(value); + return true; + } + return false; + } + + public boolean releaseLock(String key) { + String value = lockValue.get(); + if (value == null) { + return false; + } + DefaultRedisScript script = new DefaultRedisScript<>( + "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", + Boolean.class + ); + Boolean success = redisTemplate.execute(script, Collections.singletonList(key), value); + if (Boolean.TRUE.equals(success)) { + lockValue.remove(); + return true; + } + return false; + } +} diff --git a/src/main/java/xyz/playedu/api/util/RedisLockUtil.java b/src/main/java/xyz/playedu/api/util/RedisLockUtil.java deleted file mode 100644 index a810835..0000000 --- a/src/main/java/xyz/playedu/api/util/RedisLockUtil.java +++ /dev/null @@ -1,56 +0,0 @@ -package xyz.playedu.api.util; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.script.DefaultRedisScript; - -import java.util.ArrayList; -import java.util.List; - -/** - * @Author 杭州白书科技有限公司 - * @create 2023/3/10 14:48 - */ -@Slf4j -public class RedisLockUtil { - - public final static String LUA_LOCK_CREATE = """ - if redis.call("GET", KEYS[1]) == ARGV[1] then - redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2]) - return "OK" - else - return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) - end - """; - - public final static String LUA_LOCK_REMOVE = """ - if redis.call("GET", KEYS[1]) == ARGV[1] then - return redis.call("DEL", KEYS[1]) - else - return 0 - end"""; - - public static boolean lock(String key, String value, Integer expire) { - DefaultRedisScript script = new DefaultRedisScript<>(); - script.setScriptText(LUA_LOCK_CREATE); - // 脚本中的keys - List keys = new ArrayList<>(); - keys.add(key); - - Object result = RedisUtil.handler().execute(script, keys, value, expire); - log.info("上锁结果 {}", result); - return false; - } - - public static boolean remove(String key, String value) { - DefaultRedisScript script = new DefaultRedisScript<>(); - script.setScriptText(LUA_LOCK_REMOVE); - script.setResultType(String.class); - // 脚本中的keys - List keys = new ArrayList<>(); - keys.add(key); - String result = RedisUtil.handler().execute(script, keys, value); - log.info("解锁结果 {}", result); - return "OK".equals(result); - } - -}