优雅的使用 Redis 分布式锁。
本文使用Redisson中实现的分布式锁。
- <dependency>
- <groupId>org.redissongroupId>
- <artifactId>redisson-spring-boot-starterartifactId>
- <version>3.14.1version>
- dependency>
- @Configuration
- public class RedissonConfiguration {
-
- // 此处更换自己的 Redis 地址即可
- @Value("${redis.addr}")
- private String addr;
-
- @Bean
- public RedissonClient redisson() {
- Config config = new Config();
- config.useSingleServer()
- .setAddress(String.format("%s%s", "redis://", addr))
- .setConnectionPoolSize(64) // 连接池大小
- .setConnectionMinimumIdleSize(8) // 保持最小连接数
- .setConnectTimeout(1500) // 建立连接超时时间
- .setTimeout(2000) // 执行命令的超时时间, 从命令发送成功时开始计时
- .setRetryAttempts(2) // 命令执行失败重试次数
- .setRetryInterval(1000); // 命令重试发送时间间隔
-
- return Redisson.create(config);
- }
- }
这样我们就可以在项目里面使用 Redisson 了。
Redis 分布式锁的工具类,主要是调用 Redisson 客户端实现,做了轻微的封装。
- @Service
- @Slf4j
- public class LockManager {
-
- /**
- * 最小锁等待时间
- */
- private static final int MIN_WAIT_TIME = 10;
-
- @Resource
- private RedissonClient redisson;
-
- /**
- * 加锁,加锁失败抛默认异常 - 操作频繁, 请稍后再试
- *
- * @param key 加锁唯一key
- * @param expireTime 锁超时时间 毫秒
- * @param waitTime 加锁最长等待时间 毫秒
- * @return LockResult 加锁结果
- */
- public LockResult lock(String key, long expireTime, long waitTime) {
- return lock(key, expireTime, waitTime, () -> new BizException(ResponseEnum.COMMON_FREQUENT_OPERATION_ERROR));
- }
-
- /**
- * 加锁,加锁失败抛异常 - 自定义异常
- *
- * @param key 加锁唯一key
- * @param expireTime 锁超时时间 毫秒
- * @param waitTime 加锁最长等待时间 毫秒
- * @param exceptionSupplier 加锁失败时抛该异常,传null时加锁失败不抛异常
- * @return LockResult 加锁结果
- */
- private LockResult lock(String key, long expireTime, long waitTime, Supplier
exceptionSupplier) { - if (waitTime < MIN_WAIT_TIME) {
- waitTime = MIN_WAIT_TIME;
- }
-
- LockResult result = new LockResult();
- try {
- RLock rLock = redisson.getLock(key);
- try {
- if (rLock.tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS)) {
- result.setLockResultStatus(LockResultStatus.SUCCESS);
- result.setRLock(rLock);
- } else {
- result.setLockResultStatus(LockResultStatus.FAILURE);
- }
- } catch (InterruptedException e) {
- log.error("Redis 获取分布式锁失败, key: {}, e: {}", key, e.getMessage());
- result.setLockResultStatus(LockResultStatus.EXCEPTION);
- rLock.unlock();
- }
- } catch (Exception e) {
- log.error("Redis 获取分布式锁失败, key: {}, e: {}", key, e.getMessage());
- result.setLockResultStatus(LockResultStatus.EXCEPTION);
- }
-
- if (exceptionSupplier != null && LockResultStatus.FAILURE.equals(result.getLockResultStatus())) {
- log.warn("Redis 加锁失败, key: {}", key);
- throw exceptionSupplier.get();
- }
-
- log.info("Redis 加锁结果:{}, key: {}", result.getLockResultStatus(), key);
-
- return result;
- }
-
- /**
- * 解锁
- */
- public void unlock(RLock rLock) {
- try {
- rLock.unlock();
- } catch (Exception e) {
- log.warn("Redis 解锁失败", e);
- }
- }
- }
加锁结果状态枚举类。
- public enum LockResultStatus {
- /**
- * 通信正常,并且加锁成功
- */
- SUCCESS,
- /**
- * 通信正常,但获取锁失败
- */
- FAILURE,
- /**
- * 通信异常和内部异常,锁状态未知
- */
- EXCEPTION;
- }
加锁结果类封装了加锁状态和RLock。
- @Setter
- @Getter
- public class LockResult {
-
- private LockResultStatus lockResultStatus;
-
- private RLock rLock;
- }
自此我们就可以使用分布式锁了,使用方式:
- @Service
- @Slf4j
- public class TestService {
-
- @Resource
- private LockManager lockManager;
-
- public String test(String userId) {
- // 锁:userId, 锁超时时间:5s, 锁等待时间:50ms
- LockResult lockResult = lockManager.lock(userId, 5000, 50);
-
- try {
- // 业务代码
- } finally {
- lockManager.unlock(lockResult.getRLock());
- }
-
- return "";
- }
- }
为了防止程序发生异常,所以每次我们都需要在finally代码块里手动释放锁。为了更方便优雅的使用 Redis 分布式锁,我们使用注解方式实现下。
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface Lock {
-
- /**
- * lock key
- */
- String value();
-
- /**
- * 锁超时时间,默认5000ms
- */
- long expireTime() default 5000L;
-
- /**
- * 锁等待时间,默认50ms
- */
- long waitTime() default 50L;
-
- }
- @Aspect
- @Component
- @Slf4j
- public class LockAnnotationParser {
-
- @Resource
- private LockManager lockManager;
-
- /**
- * 定义切点
- */
- @Pointcut(value = "@annotation(Lock)")
- private void cutMethod() {
- }
-
- /**
- * 切点逻辑具体实现
- */
- @Around(value = "cutMethod() && @annotation(lock)")
- public Object parser(ProceedingJoinPoint point, Lock lock) throws Throwable {
- String value = lock.value();
- if (isEl(value)) {
- value = getByEl(value, point);
- }
- LockResult lockResult = lockManager.lock(getRealLockKey(value), lock.expireTime(), lock.waitTime());
- try {
- return point.proceed();
- } finally {
- lockManager.unlock(lockResult.getRLock());
- }
- }
-
- /**
- * 解析 SpEL 表达式并返回其值
- */
- private String getByEl(String el, ProceedingJoinPoint point) {
- Method method = ((MethodSignature) point.getSignature()).getMethod();
- String[] paramNames = getParameterNames(method);
- Object[] arguments = point.getArgs();
-
- ExpressionParser parser = new SpelExpressionParser();
- Expression expression = parser.parseExpression(el);
- EvaluationContext context = new StandardEvaluationContext();
- for (int i = 0; i < arguments.length; i++) {
- context.setVariable(paramNames[i], arguments[i]);
- }
-
- return expression.getValue(context, String.class);
- }
-
- /**
- * 获取方法参数名列表
- */
- private String[] getParameterNames(Method method) {
- LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
- return u.getParameterNames(method);
- }
-
- private boolean isEl(String str) {
- return str.contains("#");
- }
-
- /**
- * 锁键值
- */
- private String getRealLockKey(String value) {
- return String.format("lock:%s", value);
- }
- }
下面使用注解方式使用分布式锁:
- @Service
- @Slf4j
- public class TestService {
- @Lock("'test_'+#user.userId")
- public String test(User user) {
- // 业务代码
- return "";
- }
- }
当然也可以自定义锁的超时时间和等待时间
- @Service
- @Slf4j
- public class TestService {
- @Lock(value = "'test_'+#user.userId", expireTime = 3000, waitTime = 30)
- public String test(User user) {
- // 业务代码
- return "";
- }
- }
优雅永不过时