引言:为什么我们需要 “通用” 的分布式锁?

做 Java 开发的同学,几乎都遇到过这些场景:

  • 秒杀系统里的库存超卖,明明只有 100 件商品,最后卖出去 105 件;
  • 优惠券领取时的重复发放,用户一点击就多领了 2 张;
  • 定时任务集群部署时的重复执行,多台机器同时跑同一个任务。

这些问题的根源,都是单 JVM 内的synchronized/Lock搞不定了 —— 多实例部署时,每个实例的锁是独立的,根本拦不住跨机器的并发。

于是我们想到用 Redis 做分布式锁,但原生 Redis 命令(SETNX+EXPIRE)又有一堆坑:误删别人的锁、业务没执行完锁就过期、删锁时被打断导致死锁…

而 Redisson 虽然解决了这些问题,但直接用会有大量重复代码(每次都要getLock→tryLock→unlock),还没法灵活适配不同场景(有时要公平锁、有时要读写锁)。

这篇文章就带你从 0 到 1,一步步设计一个通用、可扩展、能直接丢到项目里用的 Redisson 分布式锁,从基础使用到 AOP 注解优化,再到动态锁名、灵活策略,最后用优惠券防超卖实战验证,全程附完整源码。

1. 先搞懂:为什么不选 Redis 原生锁,非要用 Redisson?

在设计之前,先明确一个核心问题:Redis 原生锁的坑,Redisson 是怎么填的?这是我们设计通用锁的基础。

1.1 Redis 原生锁的 3 个致命问题

用 Redis 做分布式锁,最开始会这么写:

// 1.加锁(SETNX)
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:coupon", "thread1", 10, TimeUnit.SECONDS);
if (!locked) {
    throw new RuntimeException("请稍后再试");
}
try {
    // 执行业务:减库存、发优惠券
} finally {
    // 2.释放锁
    redisTemplate.delete("lock:coupon");
}

但这代码藏着 3 个大坑:

  • 坑 1:误删别人的锁:如果线程 A 的业务执行慢,锁 10 秒后自动过期,线程 B 拿到锁;这时 A 执行完,会把 B 的锁删掉;
  • 坑 2:锁过期业务没执行完:10 秒过期时间设短了,业务还在跑锁就没了,导致并发问题;设长了又怕服务宕机导致死锁;
  • 坑 3:释放锁不是原子操作:如果线程 A 删锁时,刚判断完 “这是我的锁”,还没执行delete就被打断(比如 GC),锁过期后线程 B 拿到锁,A 恢复后还是会删掉 B 的锁。

1.2 Redisson 的 “神仙操作”:3 个问题一次性解决

Redisson 之所以成为主流,就是因为它内置了解决方案:

  1. 防误删:加锁时存入的是 “线程唯一标识”(比如UUID+线程ID),释放前先判断标识是否匹配;
  2. 看门狗机制:如果没手动设置锁过期时间(leaseTime=-1),Redisson 会启动一个后台线程,每隔 10 秒给锁续期 30 秒,直到业务执行完;
  3. 原子释放锁:用 Lua 脚本把 “判断标识 + 删除锁” 做成原子操作,避免中间被打断。

而且 Redisson 还支持可重入锁、公平锁、读写锁等多种锁类型,这为我们设计 “通用锁” 提供了基础。

2. 基础篇:Redisson 分布式锁的 “Hello World”

在设计通用锁之前,先快速掌握 Redisson 的基础使用 —— 这是后续优化的起点。

2.1 第一步:引入依赖 + 配置 RedissonClient

首先在pom.xml引入 Redisson 依赖(注意版本和 Redis 版本适配,这里用 3.23.3):

<!-- Redisson核心依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.3</version>
</dependency>
<!-- Spring Data Redis(如果项目没引入的话) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后配置RedissonClient(支持单点、集群、哨兵,这里以单点为例):

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 单点Redis配置:redis://IP:端口,密码如果没有可以去掉setPassword
        config.useSingleServer()
        .setAddress("redis://127.0.0.1:6379")
        .setPassword("123456")
        .setDatabase(0); // 指定Redis数据库索引

        // 集群配置(如果需要):
        // config.useClusterServers()
        //         .addNodeAddress("redis://127.0.0.1:7001")
        //         .addNodeAddress("redis://127.0.0.1:7002");

        return Redisson.create(config);
    }
}

2.2 第二步:基础锁使用(可重入锁)

直接注入RedissonClient,调用tryLock获取锁,unlock释放锁(必须在finally里释放,避免锁泄漏):

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CouponService {
    @Autowired
    private RedissonClient redissonClient;
    // 优惠券领取方法(防重复领取/超卖)
    public void receiveCoupon(Long couponId, Long userId) {
        // 1.定义锁名:用业务标识+唯一ID,避免锁冲突(比如coupon_123_user_456)
        String lockKey = "coupon:receive:" + couponId + ":" + userId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 2.尝试获取锁:waitTime=3秒(获取不到锁时最多等3秒),leaseTime=-1(用看门狗自动续期)
            boolean isLocked = lock.tryLock(3, -1, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new RuntimeException("请求太频繁,请稍后再试");
            }
            // 3.执行业务逻辑:校验库存、生成用户优惠券、扣减库存等
            System.out.println("用户" + userId + "成功领取优惠券" + couponId);
            // 这里写你的业务代码...
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("领取失败");
        } finally {
            // 4.释放锁:必须判断锁是否被当前线程持有,避免释放别人的锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

关键注意点

  • lockKey必须唯一:避免不同业务用同一个锁名,导致 “锁竞争” 或 “锁误删”;
  • leaseTime=-1:启用看门狗机制,自动续期;如果手动设置leaseTime=10,看门狗会失效,锁 10 秒后自动释放;
  • unlock前判断isHeldByCurrentThread:防止线程 A 没拿到锁,却释放了线程 B 的锁。

3. 优化 1:用 AOP + 注解解耦 —— 告别重复的锁代码

上面的基础用法有个问题:每个需要加锁的方法,都要写 “获取锁→判断→释放锁” 的重复代码,业务侵入性太强。

解决方案:用自定义注解 + AOP,把锁逻辑抽成通用切面,业务方法只需要加个注解就能实现加锁。

3.1 第一步:定义自定义注解@MyLock

注解里包含锁的核心参数:锁名、等待时间、过期时间、时间单位等,给默认值减少配置成本:

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
 * 自定义Redisson分布式锁注解
 */
@Target(ElementType.METHOD) // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface MyLock {
    /**
     * 锁的名称(支持SPEL表达式,比如"coupon:receive:#{#couponId}:#{#userId}")
     */
    String name();
    /**
     * 获取锁的最大等待时间(默认3秒)
     */
    long waitTime() default 3;
    /**
     * 锁的过期时间(默认-1,启用看门狗自动续期)
     */
    long leaseTime() default -1;
    /**
     * 时间单位(默认秒)
     */
    TimeUnit unit() default TimeUnit.SECONDS;
    /**
     * 获取锁失败时的提示信息
     */
    String failMsg() default "请求太频繁,请稍后再试";
}

3.2 第二步:编写 AOP 切面MyLockAspect

切面负责拦截加了@MyLock的方法,自动执行 “获取锁→执行业务→释放锁” 的逻辑:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
/**
 * Redisson分布式锁AOP切面
 */
@Aspect
@Component
public class MyLockAspect implements Ordered {
    @Autowired
    private RedissonClient redissonClient;
    // 切入点:拦截所有加了@MyLock注解的方法
    @Pointcut("@annotation(com.yourpackage.lock.MyLock)") // 替换成你的注解全路径
    public void lockPointCut() {}
    // 环绕通知:在方法执行前后处理锁逻辑
    @Around("lockPointCut() && @annotation(myLock)")
    public Object around(ProceedingJoinPoint joinPoint, MyLock myLock) throws Throwable {
        // 1.解析锁名(这里先简单处理,后面优化支持SPEL)
        String lockKey = myLock.name();
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 2.尝试获取锁
            boolean isLocked = lock.tryLock(
                myLock.waitTime(),
                myLock.leaseTime(),
                myLock.unit()
            );
            if (!isLocked) {
                throw new RuntimeException(myLock.failMsg());
            }
            // 3.获取锁成功,执行业务方法
            return joinPoint.proceed();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁失败");
        } finally {
            // 4.释放锁:只释放当前线程持有的锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    /**
     * 设置切面优先级:确保锁切面比事务切面先执行(避免事务未提交锁就释放)
     * 数值越小,优先级越高(事务切面默认优先级是Ordered.LOW_PRECEDENCE)
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 10;
    }
}

3.3 第三步:业务方法直接用注解

之前的receiveCoupon方法瞬间简洁了,只需要加个@MyLock注解,指定锁名即可:

@Service
public class CouponService {
    // 用注解替代重复的锁代码!锁名直接写业务标识
    @MyLock(name = "coupon:receive:#{#couponId}:#{#userId}", failMsg = "领取太频繁,请10秒后再试")
    public void receiveCoupon(Long couponId, Long userId) {
        // 业务逻辑不变:校验库存、生成用户优惠券、扣减库存
        System.out.println("用户" + userId + "成功领取优惠券" + couponId);
        // 你的业务代码...
    }
}

优化点说明

  • 解耦:锁逻辑和业务逻辑分离,后续修改锁逻辑不需要动业务代码;
  • 事务兼容:通过Ordered设置切面优先级,确保锁在事务开始前获取,事务提交后释放(避免 “锁释放了但事务没提交” 导致的并发问题);
  • 易用性:默认值减少配置,业务方只需要关注 “锁名” 和 “失败提示”。

4. 优化 2:支持多类型锁 —— 可重入 / 公平 / 读写锁自由切换

Redisson 支持多种锁类型,但上面的切面只支持 “可重入锁”(默认)。实际场景中,我们可能需要:

  • 公平锁:按请求顺序获取锁,避免线程饥饿;
  • 读写锁:读多写少场景(比如商品详情查询加读锁,更新加写锁),提高并发效率。

解决方案:在注解里加lockType参数,用枚举 + 工厂模式动态创建不同类型的锁。

4.1 第一步:定义锁类型枚举MyLockType

枚举里包含所有支持的锁类型,以及创建锁的方法:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.RReadWriteLock;
/**
 * 分布式锁类型枚举
 */
public enum MyLockType {
    /** 可重入锁(默认) */
    REENTRANT_LOCK {
        @Override
        public RLock createLock(RedissonClient redissonClient, String lockKey) {
            return redissonClient.getLock(lockKey);
        }
    },
    /** 公平锁 */
    FAIR_LOCK {
        @Override
        public RLock createLock(RedissonClient redissonClient, String lockKey) {
            return redissonClient.getFairLock(lockKey);
        }
    },
    /** 读锁(配合读写锁使用) */
    READ_LOCK {
        @Override
        public RLock createLock(RedissonClient redissonClient, String lockKey) {
            RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);
            return rwLock.readLock();
        }
    },
    /** 写锁(配合读写锁使用) */
    WRITE_LOCK {
        @Override
        public RLock createLock(RedissonClient redissonClient, String lockKey) {
            RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);
            return rwLock.writeLock();
        }
    };
    /**
     * 抽象方法:创建对应类型的锁
     */
    public abstract RLock createLock(RedissonClient redissonClient, String lockKey);
}

4.2 第二步:更新注解@MyLock,增加lockType参数

public @interface MyLock {
    // 原有参数...
    /**
     * 锁类型(默认可重入锁)
     */
    MyLockType lockType() default MyLockType.REENTRANT_LOCK;
}

4.3 第三步:修改切面MyLockAspect,动态创建锁

把 “创建锁” 的逻辑改成用枚举的createLock方法:

@Aspect
@Component
public class MyLockAspect implements Ordered {
    @Autowired
    private RedissonClient redissonClient;
    @Around("lockPointCut() && @annotation(myLock)")
    public Object around(ProceedingJoinPoint joinPoint, MyLock myLock) throws Throwable {
        // 1.解析锁名(后面优化SPEL)
        String lockKey = myLock.name();

        // 2.根据锁类型,动态创建锁(核心修改)
        RLock lock = myLock.lockType().createLock(redissonClient, lockKey);
        try {
            // 3.尝试获取锁(逻辑不变)
            boolean isLocked = lock.tryLock(
                myLock.waitTime(),
                myLock.leaseTime(),
                myLock.unit()
            );
            if (!isLocked) {
                throw new RuntimeException(myLock.failMsg());
            }
            // 4.执行业务
            return joinPoint.proceed();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁失败");
        } finally {
            // 5.释放锁(逻辑不变)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    // 其他方法...
}

4.4 第四步:业务方法使用不同类型的锁

示例 1:公平锁(比如秒杀场景,避免线程饥饿)
// 秒杀领取优惠券:用公平锁,按请求顺序处理
@MyLock(
    name = "coupon:seckill:#{#couponId}:#{#userId}",
    lockType = MyLockType.FAIR_LOCK,
    failMsg = "秒杀太火爆,请重试"
)
public void seckillCoupon(Long couponId, Long userId) {
    // 秒杀业务逻辑:扣减库存、生成订单...
}
示例 2:读写锁(商品详情查询 + 更新)
@Service
public class GoodsService {
    // 商品详情查询:加读锁,支持多线程同时读
    @MyLock(
        name = "goods:info:#{#goodsId}",
        lockType = MyLockType.READ_LOCK,
        leaseTime = 10 // 读锁过期时间设10秒,避免长期占用
    )
    public GoodsVO getGoodsInfo(Long goodsId) {
        // 查询商品详情(读操作)
        return goodsMapper.selectById(goodsId);
    }
    // 商品库存更新:加写锁,同一时间只能一个线程写
    @MyLock(
        name = "goods:info:#{#goodsId}",
        lockType = MyLockType.WRITE_LOCK,
        failMsg = "更新太频繁,请稍后再试"
    )
    public void updateGoodsStock(Long goodsId, Integer stock) {
        // 更新商品库存(写操作)
        goodsMapper.updateStock(goodsId, stock);
    }
}

5. 优化 3:支持 SPEL 动态锁名 —— 让锁名更灵活

之前的锁名是固定字符串 + 参数(比如coupon:receive:#{#couponId}:#{#userId}),但切面还没解析 SPEL 表达式,直接用会导致锁名变成coupon:receive:#{#couponId}:#{#userId},而不是期望的coupon:receive:123:456。

解决方案:在切面里加入 SPEL 表达式解析逻辑,动态替换锁名中的参数占位符。

5.1 第一步:在切面MyLockAspect中添加 SPEL 解析方法

利用 Spring 的SpelExpressionParser解析 SPEL 表达式,获取方法参数的值:

import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.aspectj.lang.reflect.MethodSignature;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Aspect
@Component
public class MyLockAspect implements Ordered {
    // SPEL表达式解析器(单例,避免重复创建)
    private final ExpressionParser spelParser = new SpelExpressionParser();
    // 匹配SPEL表达式的正则:#{...}
    private final Pattern spelPattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
    // 原有属性和方法...
    /**
     * 解析锁名中的SPEL表达式,替换为实际参数值
     */
    private String parseLockKey(String lockKey, ProceedingJoinPoint joinPoint) {
        // 1.如果锁名不含SPEL表达式,直接返回
        if (!spelPattern.matcher(lockKey).find()) {
            return lockKey;
        }
        // 2.获取方法签名和参数
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = methodSignature.getParameterNames(); // 参数名(比如couponId、userId)
        Object[] paramValues = joinPoint.getArgs(); // 参数值(比如123、456)
        // 3.构建SPEL上下文,把方法参数放入上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < paramNames.length; i++) {
            context.setVariable(paramNames[i], paramValues[i]);
        }
        // 4.匹配并替换所有SPEL表达式
        Matcher matcher = spelPattern.matcher(lockKey);
        while (matcher.find()) {
            // 获取SPEL表达式内容(比如"#couponId")
            String spelExpr = matcher.group(1);
            // 解析SPEL表达式,得到实际值(比如123)
            Object value = spelParser.parseExpression(spelExpr).getValue(context);
            // 替换锁名中的占位符
            lockKey = lockKey.replace(matcher.group(), value == null ? "" : value.toString());
        }
        return lockKey;
    }
    // 更新around方法,调用parseLockKey解析锁名
    @Around("lockPointCut() && @annotation(myLock)")
    public Object around(ProceedingJoinPoint joinPoint, MyLock myLock) throws Throwable {
        // 1.解析锁名(核心修改:加入SPEL解析)
        String lockKey = parseLockKey(myLock.name(), joinPoint);

        // 2.创建锁(逻辑不变)
        RLock lock = myLock.lockType().createLock(redissonClient, lockKey);
        // 后续逻辑不变...
    }
}

5.2 第二步:业务方法使用 SPEL 动态锁名

现在可以在锁名里用#{#参数名}引用方法参数,甚至支持简单的表达式(比如#{#user.id}):

@Service
public class OrderService {
    // 锁名:order:create:用户ID_订单类型(比如order:create:456_NORMAL)
    @MyLock(
        name = "order:create:#{#userId}_#{#orderType}",
        lockType = MyLockType.REENTRANT_LOCK
    )
    public void createOrder(Long userId, String orderType) {
        // 创建订单业务逻辑...
    }
    // 支持嵌套参数:比如#user.getId()
    @MyLock(name = "user:update:#{#user.getId()}")
    public void updateUser(User user) {
        // 更新用户信息...
    }
}

6. 优化 4:支持动态失败策略 —— 灵活应对不同场景

不同业务对 “获取锁失败” 的处理方式不同:

  • 秒杀场景:失败后直接提示 “重试”;
  • 定时任务场景:失败后跳过执行,避免重复;
  • 支付场景:失败后阻塞等待,直到获取锁。

解决方案:在注解里加failStrategy参数,用策略模式动态处理获取锁失败的逻辑。

6.1 第一步:定义失败策略枚举MyLockFailStrategy

枚举里包含不同的失败策略,以及对应的处理方法:

import com.yourpackage.exception.BizException; // 替换成你的业务异常类
/**
 * 分布式锁获取失败策略枚举
 */
public enum MyLockFailStrategy {
    /** 快速失败(默认):获取不到锁直接抛异常 */
    FAIL_FAST {
        @Override
        public void handleFail(String failMsg) {
            throw new BizException(failMsg);
        }
    },
    /** 快速跳过:获取不到锁直接返回null,不抛异常 */
    SKIP_FAST {
        @Override
        public void handleFail(String failMsg) {
            // 不抛异常,直接返回null(后续切面会返回null)
        }
    },
    /** 阻塞等待:一直等待直到获取锁(忽略waitTime,慎用!) */
    BLOCK_UNTIL_LOCKED {
        @Override
        public void handleFail(String failMsg) {
            // 该策略下,tryLock会一直等待,不会进入这里
        }
    };
    /**
     * 抽象方法:处理获取锁失败的逻辑
     */
    public abstract void handleFail(String failMsg);
}

6.2 第二步:更新注解和切面,支持失败策略

1. 更新@MyLock注解:
public @interface MyLock {
    // 原有参数...
    /**
     * 获取锁失败策略(默认快速失败)
     */
    MyLockFailStrategy failStrategy() default MyLockFailStrategy.FAIL_FAST;
}
2. 修改切面MyLockAspect:
@Around("lockPointCut() && @annotation(myLock)")
public Object around(ProceedingJoinPoint joinPoint, MyLock myLock) throws Throwable {
    // 1.解析锁名
    String lockKey = parseLockKey(myLock.name(), joinPoint);
    // 2.创建锁
    RLock lock = myLock.lockType().createLock(redissonClient, lockKey);
    try {
        // 3.根据失败策略,处理获取锁逻辑
        boolean isLocked;
        if (myLock.failStrategy() == MyLockFailStrategy.BLOCK_UNTIL_LOCKED) {
            // 阻塞等待策略:忽略waitTime,一直等直到获取锁
            lock.lock(myLock.leaseTime(), myLock.unit());
            isLocked = true;
        } else {
            // 其他策略:用tryLock获取锁
            isLocked = lock.tryLock(
                myLock.waitTime(),
                myLock.leaseTime(),
                myLock.unit()
            );
        }
        // 4.获取锁失败,按策略处理
        if (!isLocked) {
            myLock.failStrategy().handleFail(myLock.failMsg());
            return null; // 快速跳过策略返回null
        }
        // 5.执行业务
        return joinPoint.proceed();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new BizException("获取锁被中断");
    } finally {
        // 6.释放锁
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

6.3 第三步:业务方法使用不同失败策略

示例 1:定时任务(获取不到锁跳过执行)
@Service
public class ScheduledTaskService {
    // 定时任务:每天凌晨更新统计数据,获取不到锁直接跳过
    @Scheduled(cron = "0 0 0 * * ?")
    @MyLock(
        name = "task:update:statistics",
        failStrategy = MyLockFailStrategy.SKIP_FAST,
        waitTime = 0 // 不等待,获取不到直接跳过
    )
    public void updateStatistics() {
        // 更新统计数据逻辑...
        System.out.println("统计数据更新成功");
    }
}
示例 2:支付回调(必须获取锁,阻塞等待)
@Service
public class PayCallbackService {
    // 支付回调:必须处理,获取不到锁阻塞等待
    @MyLock(
        name = "pay:callback:#{#orderId}",
        failStrategy = MyLockFailStrategy.BLOCK_UNTIL_LOCKED,
        leaseTime = 60 // 锁过期时间60秒
    )
    public void handlePayCallback(String orderId, String payStatus) {
        // 处理支付回调逻辑:更新订单状态、通知用户...
    }
}

7. 实战:优惠券领取防超卖(完整案例)

用我们设计的通用分布式锁,解决 “优惠券超卖 / 重复领取” 问题,全程附测试结果。

7.1 业务场景

  • 优惠券总数 100 张,每人限领 1 张;
  • 模拟 1000 个并发请求,测试是否会超卖或重复领取。

7.2 数据库表设计(简化)

-- 优惠券表
CREATE TABLE `coupon` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL COMMENT '优惠券名称',
  `total_num` int NOT NULL COMMENT '总数量',
  `used_num` int NOT NULL DEFAULT 0 COMMENT '已使用数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户优惠券表(防重复领取)
CREATE TABLE `user_coupon` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `coupon_id` bigint NOT NULL COMMENT '优惠券ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_coupon_user` (`coupon_id`,`user_id`) COMMENT '唯一索引:避免重复领取'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

7.3 业务代码实现

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CouponService {
    @Autowired
    private CouponMapper couponMapper;
    @Autowired
    private UserCouponMapper userCouponMapper;
    /**
     * 领取优惠券(防超卖+防重复领取)
     * @param couponId 优惠券ID
     * @param userId 用户ID
     */
    @Transactional
    @MyLock(
        name = "coupon:receive:#{#couponId}:#{#userId}", // 锁名:优惠券ID+用户ID,确保一人一张
        lockType = MyLockType.REENTRANT_LOCK,
        waitTime = 5, // 最多等5秒
        failMsg = "优惠券领取太火爆,请稍后再试"
    )
    public void receiveCoupon(Long couponId, Long userId) {
        // 1.校验优惠券是否存在且有库存
        Coupon coupon = couponMapper.selectById(couponId);
        if (coupon == null) {
            throw new BizException("优惠券不存在");
        }
        if (coupon.getUsedNum() >= coupon.getTotalNum()) {
            throw new BizException("优惠券已领完");
        }
        // 2.校验用户是否已领取(配合数据库唯一索引,双重保障)
        UserCoupon exist = userCouponMapper.selectByCouponAndUser(couponId, userId);
        if (exist != null) {
            throw new BizException("你已领取过该优惠券");
        }
        // 3.扣减优惠券库存(用UPDATE ... WHERE 实现原子扣减,避免超卖)
        int rows = couponMapper.incrUsedNum(couponId);
        if (rows == 0) {
            throw new BizException("优惠券已领完");
        }
        // 4.生成用户优惠券记录
        UserCoupon userCoupon = new UserCoupon();
        userCoupon.setCouponId(couponId);
        userCoupon.setUserId(userId);
        userCouponMapper.insert(userCoupon);
    }
}

7.4 并发测试(用 JMeter)

测试配置:
  • 线程数:1000 个并发用户;
  • 循环次数:1 次;
  • 每个用户领取同一个优惠券(ID=1,总库存 100)。
测试结果:
  • 数据库coupon.used_num=100(无超卖);
  • user_coupon表新增 100 条记录(无重复领取);
  • 剩余 900 个请求提示 “领取太火爆” 或 “已领完”(符合预期)。

8. 设计思路复盘:通用组件的 “进化之路”

我们的分布式锁从 “基础使用” 到 “通用组件”,经历了 4 次核心优化,背后是通用组件设计的经典思路:

优化阶段

解决的问题

核心设计模式

收益

基础使用

分布式并发问题

-

解决单实例锁局限

AOP + 注解

锁代码重复、业务侵入

切面模式

解耦,业务方只需加注解

多类型锁

锁类型单一,不支持公平 / 读写锁

枚举 + 工厂模式

灵活适配不同场景

SPEL 动态锁名

锁名固定,不支持参数动态替换

SPEL 解析

锁名更灵活,避免硬编码

动态失败策略

失败处理方式单一

策略模式

适配不同业务的失败逻辑

9. 进阶方向:让你的分布式锁更健壮

  1. Redis 集群下的锁可靠性
    • 问题:Redis 主从切换时,主库锁还没同步到从库,主库宕机导致锁丢失;
    • 解决方案:使用 Redisson 的RedissonRedLock(红锁),同时在多个 Redis 节点加锁,确保多数节点加锁成功才算成功。
  1. 锁泄漏防护
    • 问题:服务宕机或unlock被跳过,导致锁长期占用;
    • 解决方案:即使启用看门狗,也建议设置合理的leaseTime(比如 30 秒),避免极端情况下的锁泄漏。
  1. 与其他锁方案对比
    • Redisson 锁:基于 Redis,性能高,适合高并发场景;
    • ZooKeeper 锁:基于 ZAB 协议,可靠性高,适合对一致性要求高的场景(比如分布式事务);
    • 选择建议:大多数业务场景优先用 Redisson,一致性要求极高时用 ZooKeeper。

完整代码

注解类(锁的注解添加了方法结束释放锁,其实就是在finally处判断是否需要自动删除)

package com.tianji.promotion.utils;

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;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLock {
    /**
     * 加锁key的表达式,支持SPEL表达式
     */
    String name();

    /**
     * 阻塞超时时长,不指定 waitTime 则按照Redisson默认时长
     */
    long waitTime() default 1;

    /**
     * 锁自动释放时长,默认是-1,其实是30秒 + watchDog模式
     */
    long leaseTime() default -1;

    /**
     * 时间单位,默认为秒
     */
    TimeUnit unit() default TimeUnit.SECONDS;

    /**
     * 锁的类型,包括:可重入锁、公平锁、读锁、写锁
     */
    MyLockType lockType() default MyLockType.RE_ENTRANT_LOCK;

    /**
     * 如果设定了false,则方法结束不释放锁,而是等待leaseTime后自动释放
     */
    boolean autoUnlock() default true;

    /**
     * 锁策略,包括5种,默认策略是 不断尝试获取锁,直到成功或超时,超时后抛出异常
     */
    MyLockStrategy lockStrategy() default MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT;
}

枚举类(将工厂代码优化进枚举类代码)

package com.tianji.promotion.utils;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

public enum MyLockType {
    RE_ENTRANT_LOCK(){
        @Override
        public RLock getLock(RedissonClient redissonClient, String name) {
            return redissonClient.getLock(name);
        }
    }, // 可重入锁
    FAIR_LOCK(){
        @Override
        public RLock getLock(RedissonClient redissonClient, String name) {
            return redissonClient.getFairLock(name);
        }
    }, // 公平锁
    READ_LOCK(){
        @Override
        public RLock getLock(RedissonClient redissonClient, String name) {
            return redissonClient.getReadWriteLock(name).readLock();
        }
    }, // 读锁
    WRITE_LOCK(){
        @Override
        public RLock getLock(RedissonClient redissonClient, String name) {
            return redissonClient.getReadWriteLock(name).writeLock();
        }
    }, // 写锁
    ;

    public abstract RLock getLock(RedissonClient redissonClient, String name);
}

切面类(添加了spel表达式解析与自动解锁)

核心改动

package com.tianji.promotion.utils;

import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
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.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered {

    private final RedissonClient redissonClient;

    @Around("@annotation(mylock)")
    public Object tryLock(ProceedingJoinPoint pjp, MyLock mylock) throws Throwable {
        if (!mylock.autoUnlock() && mylock.leaseTime() <= 0) {
            // 不手动释放锁时,必须指定leaseTime时间
            throw new BizIllegalException("leaseTime不能为空");
        }
        // 基于SPEL表达式解析锁的 name(由注解名字转换过来)
        String name = getLockName(mylock.name(), pjp);

        //创建锁对象
        RLock lock = mylock.lockType().getLock(redissonClient, name);
        //尝试获取锁
        boolean isLock = mylock.lockStrategy().tryLock(lock, mylock);

        //判断是否成功
        if(!isLock){
            //快速失败
            return null;
        }
        try {
            // 成功,执行业务逻辑
            return pjp.proceed();
        }finally {
            if (mylock.autoUnlock()) {
                lock.unlock();
            }
        }
    }


    /**
     * SPEL的正则规则
     */
    private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
    /**
     * 方法参数解析器
     */
    private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    /**
     * 解析锁名称
     * @param name 原始锁名称
     * @param pjp 切入点
     * @return 解析后的锁名称
     */
    private String getLockName(String name, ProceedingJoinPoint pjp) {
        // 1.判断是否存在spel表达式
        if (StringUtils.isBlank(name) || !name.contains("#")) {
            // 不存在,直接返回
            return name;
        }
        // 2.构建context
        EvaluationContext context = new MethodBasedEvaluationContext(
            TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
        // 3.构建解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 3.循环处理
        Matcher matcher = pattern.matcher(name);
        while (matcher.find()) {
            // 2.1.获取表达式
            String tmp = matcher.group();
            // 2.2.尝试解析
            Expression expression = parser.parseExpression("#" + matcher.group(1));
            Object value = expression.getValue(context);
            name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
        }
        return name;
    }

    private Method resolveMethod(ProceedingJoinPoint pjp) {
        // 1.获取方法签名
        MethodSignature signature = (MethodSignature)pjp.getSignature();
        // 2.获取字节码
        Class<?> clazz = pjp.getTarget().getClass();
        // 3.方法名称
        String name = signature.getName();
        // 4.方法参数列表
        Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
        return tryGetDeclaredMethod(clazz, name, parameterTypes);
    }

    private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
        try {
            // 5.反射获取方法
            return clazz.getDeclaredMethod(name, parameterTypes);
        } catch (NoSuchMethodException e) {
            Class<?> superClass = clazz.getSuperclass();
            if (superClass != null) {
                // 尝试从父类寻找
                return tryGetDeclaredMethod(superClass, name, parameterTypes);
            }
        }
        return null;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

业务类使用(支持动态锁名)

package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.domain.vo.UserCouponPageVO;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.query.UserCouponQuery;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.promotion.utils.CodeUtil;
import com.tianji.promotion.utils.MyLock;
import com.tianji.promotion.utils.MyLockStrategy;
import com.tianji.promotion.utils.MyLockType;
import lombok.RequiredArgsConstructor;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import com.tianji.promotion.domain.po.Coupon;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录,是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author goldenfox
 * @since 2025-09-23
 */
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;

    private final IExchangeCodeService codeService;

    @Override
    public void receiveCoupon(Long id) {
        // 参数校验
        if(id == null || id <= 0){
            throw new BadRequestException("请求优惠券id异常");
        }
        //用户登录
        Long userId = UserContext.getUser();
        if(userId == null){
            throw new BadRequestException("用户未登录");
        }
        //查询优惠券状态
        Coupon coupon = couponMapper.selectById(id);
        if(coupon == null){
            throw new BadRequestException("优惠券不存在");
        }
        if(coupon.getStatus() != CouponStatus.ISSUING){
            throw new BadRequestException("优惠券状态异常");
        }
        if(coupon.getIssueNum() >= coupon.getTotalNum()){
            throw new BadRequestException("优惠券已发放完毕");
        }
        IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
        userCouponService.checkAndCreateUserCoupon(coupon, userId, null);

    }

    @Transactional
    @MyLock(name ="lock:coupon:#{userId}",lockType = MyLockType.RE_ENTRANT_LOCK,lockStrategy = MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT)
    public void checkAndCreateUserCoupon(Coupon coupon, Long userId, Integer serialNum){
        // 在“校验优惠券状态”后补充时间校验
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
            throw new BadRequestException("优惠券发放已经结束或尚未开始");
        }
        //查询用户领取的优惠券数量
        List<UserCoupon> userCoupons = lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getCouponId, coupon.getId())
                .list();
        if(userCoupons != null && userCoupons.size() >= coupon.getUserLimit()){
            throw new RuntimeException("超出领取数量");
        }
        //更新优惠券已发放数量
        int r = couponMapper.incrIssueNum(coupon.getId());
        if(r == 0){
            throw new BadRequestException("优惠券库存不足");
        }

        saveUserCoupon(coupon, userId);

        //更新兑换码状态
        if(serialNum !=  null){
            codeService.lambdaUpdate()
                    .set(ExchangeCode::getUserId, userId)
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .eq(ExchangeCode::getId, serialNum)
                    .update();
        }
    }

    private void saveUserCoupon(Coupon coupon, Long userId) {
        // 2.有效期信息
        LocalDateTime termBeginTime = coupon.getTermBeginTime();
        LocalDateTime termEndTime = coupon.getTermEndTime();
        if (termBeginTime == null) {
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        //保存用户领取的优惠券
        UserCoupon userCoupon = new UserCoupon().setCouponId(coupon.getId())
                .setUserId(userId)
                .setTermBeginTime(termBeginTime)
                .setTermEndTime(termEndTime);
        //保存到用户优惠券表中
        save(userCoupon);
    }

    @Override
    public void exchangeCoupon(String code) {
        //校验解析兑换码
        long serialNum = CodeUtil.parseCode(code);
        Long userId = UserContext.getUser();

        //判断是否已经兑换(redis)
        boolean exchanged = true;
        synchronized (userId.toString().intern()){
            exchanged = codeService.updateExchangeMark(serialNum, true);
        }
        if(exchanged){
            throw new BadRequestException("兑换码已兑换");
        }
        try {
            //查询兑换码
            ExchangeCode exchangeCode = codeService.getById(serialNum);
            if(exchangeCode == null){
                throw new BadRequestException("兑换码不存在");
            }
            if(!exchangeCode.getExpiredTime().isAfter(LocalDateTime.now())){
                throw new BadRequestException("兑换码已过期");
            }

            // 校验并生成用户券
            // 查询优惠券
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            if(coupon == null){
                throw new BadRequestException("优惠券不存在");
            }

            // 校验并生成用户券,更新兑换码状态
            checkAndCreateUserCoupon(coupon, userId, Math.toIntExact(serialNum));

        }catch (Exception e){
            // 重置兑换的标记 0
            codeService.updateExchangeMark(serialNum, false);
            throw e;
        }
    }

    @Override
    public PageDTO<UserCouponPageVO> queryUserCouponList(UserCouponQuery query) {
        Integer status = query.getStatus();
        if (status == null) {
            throw new BadRequestException("请选择优惠券状态");
        }
        Long userId = UserContext.getUser();
        //分页查询
        Page<UserCoupon> page = lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getStatus, status)
                .page(query.toMpPageDefaultSortByCreateTimeDesc());
        //组装vo
        List<UserCoupon> records = page.getRecords();
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        // 2. 批量查询关联的优惠券模板(避免循环查库,优化性能)
        List<Long> couponIds = records.stream()
                .map(UserCoupon::getCouponId)
                .distinct()
                .collect(Collectors.toList());
        List<Coupon> coupons = couponMapper.selectBatchIds(couponIds);
        // 转为Map:key=优惠券模板ID,value=模板信息,方便快速查询
        Map<Long, Coupon> couponMap = coupons.stream()
                .collect(Collectors.toMap(Coupon::getId, coupon -> coupon));

        // 转换为VO列表
        List<UserCouponPageVO> voList = records.stream()
                .map(userCoupon -> {
                    UserCouponPageVO vo = new UserCouponPageVO();
                    // 复制UserCoupon中直接匹配的属性(如id、termEndTime)
                    BeanUtil.copyProperties(userCoupon, vo);

                    // 获取对应的优惠券模板,补充VO的业务属性
                    Coupon coupon = couponMap.get(userCoupon.getCouponId());

                    vo.setName(coupon.getName());
                    vo.setSpecific(coupon.getSpecific());
                    vo.setDiscountType(coupon.getDiscountType());
                    vo.setThresholdAmount(coupon.getThresholdAmount());
                    vo.setDiscountValue(coupon.getDiscountValue());
                    vo.setMaxDiscountAmount(coupon.getMaxDiscountAmount());
                    vo.setTermDays(coupon.getTermDays());
                    return vo;
                })
                .collect(Collectors.toList());

        // 包装分页结果
        return PageDTO.of(page, voList);
    }
}

10. 结语

分布式锁是 Java 后端开发的 “必考点”,也是高并发系统的 “基石”。本文从 “问题出发”,一步步带你设计通用 Redisson 锁,不仅教你怎么用,更教你 “为什么这么设计”。

如果这篇文章对你有帮助,欢迎点赞 + 收藏 + 关注,后续会分享更多 Java 高并发、分布式系统的实战教程!有问题也可以在评论区留言,一起讨论~

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐