一、能不能用本地锁?

常见的本地锁有两种:synchronizedReentrantLock

synchronized 是 Java 中的一种内置锁机制,用于在代码块或方法上实现线程同步。

public synchronized void synchronizedMethod() {
    // 线程安全的代码
}

ReentrantLockjava.util.concurrent.locks 包下的锁实现,它提供了更多的控制和灵活性。

private final ReentrantLock lock = new ReentrantLock();
​
public void method() {
    lock.lock(); // 加锁
    try {
        // 线程安全的代码
    } finally {
        lock.unlock(); // 解锁,确保在最终块中释放锁
    }
}

为什么不能用本地锁?

  • 范围有限:本地锁仅在应用程序的单个实例中有效。如果你的应用程序在多台服务器上运行(即分布式环境),每个实例的本地锁相互独立,无法在集群中共享锁状态。

  • 竞争条件:不同实例上的本地锁无法相互感知,这意味着多个实例可能同时认为自己获得了锁,从而导致并发冲突。

而且,上面锁定的话,单个实例里的逻辑就变成串行了。如果想让不同的用户、不同的参数并行执行,还需要额外代码控制

二、什么是分布式锁?

分布式锁是一种用于在分布式系统中协调多个节点对共享资源的访问的机制。它确保在多个节点并发访问时,只有一个节点可以在某个时刻拥有特定资源的访问权,从而避免数据不一致、竞争条件或资源冲突的问题。

目前市场主要是以 Redis 实现的分布式锁为主,其中 Redisson 这个工具包中的分布式锁功能用的较多。

分布式锁比较关键的一个概念就是分布式锁 Key,这个应该如何定义?我们由以下几部分组成:

  • 分布式锁前缀

  • 请求路径

  • 当前访问用户

  • 参数 MD5

三、实战

3.1 自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
​
/**
 * 幂等注解,防止用户重复提交表单信息
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoDuplicateSubmit {
​
    /**
     * 触发幂等失败逻辑时,默认返回的错误提示信息,可自定义
     */
    String message() default "您操作太快,请稍后再试";
}

自定义注解上还有两个注解:

  1. @Target(ElementType.METHOD) 意味着只能在方法上使用

  2. @Retention(RetentionPolicy.RUNTIME) 意味着可以通过反射获取注解内的信息

定义的 message 属性可以让大家在使用的时候自定义错误提示信息

3.2 自定义 SpringAOP 切面

Pom.xml 文件添加 AOP 的依赖配置

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

防重复提交 AOP 代码如下所示:

/**
 * 防止用户重复提交表单信息切面控制器
 */
@Aspect
@RequiredArgsConstructor
public final class NoDuplicateSubmitAspect {
​
    private final RedissonClient redissonClient;
​
    /**
     * 增强方法标记 {@link NoDuplicateSubmit} 注解逻辑
     */
    @Around("@annotation(com.zkp.idempotent.NoDuplicateSubmit)")
    public Object noDuplicateSubmit(ProceedingJoinPoint joinPoint) throws Throwable {
        NoDuplicateSubmit noDuplicateSubmit = getNoDuplicateSubmitAnnotation(joinPoint);
        // 获取分布式锁标识
        String lockKey = String.format("no-duplicate-submit:path:%s:currentUserId:%s:md5:%s", getServletPath(), getCurrentUserId(), calcArgsMD5(joinPoint));
        RLock lock = redissonClient.getLock(lockKey);
        // 尝试获取锁,获取锁失败就意味着已经重复提交,直接抛出异常
        if (!lock.tryLock()) {
            throw new ClientException(noDuplicateSubmit.message());
        }
        Object result;
        try {
            // 执行方法原逻辑
            result = joinPoint.proceed();
        } finally {
            lock.unlock();
        }
        return result;
    }
​
    /**
     * @return 返回自定义防重复提交注解
     */
    public static NoDuplicateSubmit getNoDuplicateSubmitAnnotation(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
        return targetMethod.getAnnotation(NoDuplicateSubmit.class);
    }
​
    /**
     * @return 获取当前线程上下文 ServletPath
     */
    private String getServletPath() {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return sra.getRequest().getServletPath();
    }
​
    /**
     * @return 当前操作用户 ID
     */
    private String getCurrentUserId() {
        // 用户属于非核心功能,这里先通过模拟的形式代替。后续如果需要后管展示,会重构该代码
        return "1810518709471555585";
    }
​
    /**
     * @return joinPoint md5
     */
    private String calcArgsMD5(ProceedingJoinPoint joinPoint) {
        return DigestUtil.md5Hex(JSON.toJSONBytes(joinPoint.getArgs()));
    }
}

3.3 使用

@NoDuplicateSubmit
@PostMapping("/api/merchant-admin/coupon-template/create")
public Result<Void> createCouponTemplate(@RequestBody CouponTemplateSaveReqDTO requestParam) {
    couponTemplateService.createCouponTemplate(requestParam);
    return Results.success();
}

相关文章

基于自定义注解+AOP实现日志记录

敏感数据脱敏实现