type
status
date
slug
summary
tags
category
titleIcon
password
icon
calloutIcon
🎀
自定义分布式锁,解耦业务代码与通用锁管理部分。
节选自:
分布式锁,自定义AOP,Redisson,设计模式,SPEL
这一部分在视频里完全没提,第二版飞书文档的Day11里有部分讲解,以下做理论部分要点精简+代码部分分析。
分布式锁
- 引入背景:Synchronized单机锁基于JVM的Monitor实现,在集群下多个JVM意味着多个Monitor,无法达到互斥的效果,需要在多个实例外设置同一把锁,即分布式锁。
- Redis实现分布式锁的可能性:1.Redis可以被多JVM实例共享访问;2.SETNX互斥命令;3.DEL释放锁;4.单线程执行命令(串行)
- Redis直接实现分布式锁与可能遇到的问题:
- 直接实现:SET lock thread1 NX EX 20,主要包括两步
- 1.超时释放:锁不一定正常释放(实例宕机),可导致死锁,需要设置过期时间(例中上锁与超时设置操作保持原子性)
- 2.存入标识:存入自身线程标识,删除时如果仍是自身标识才可删除,防止锁误删(避免不了)(图源:‣)
- 1.超时问题:WatchDog机制,锁成功开定时任务,锁到期前自动续期避免超时释放,同时宕机后一同停止,避免死锁
- 2.原子性问题:Lua脚本
- 3.锁重入:类似Synchronized,可使用Hash记录持有者与重入次数,次数0时删除
- 4.主从同步延迟:RedLock
- 5.锁失败重试


但判断与删除不是原子的,仍可能误删

超时释放难以避免锁误删,锁的操作需要原子性,主从同步存在延迟,同一线程无法多次获取同一锁可能死锁,所以需要解决:
成熟解决方案:Redisson
Redisson Quick Start
maven引入
xml
<!--redisson--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> </dependency>
XML
配置类与自动装配
java
package com.tianji.common.autoconfigure.redisson; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.tianji.common.autoconfigure.redisson.aspect.LockAspect; import lombok.extern.slf4j.Slf4j; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.time.Duration; import java.util.ArrayList; import java.util.List; @Slf4j @ConditionalOnClass({RedissonClient.class, Redisson.class}) @Configuration @EnableConfigurationProperties(RedisProperties.class) public class RedissonConfig { private static final String REDIS_PROTOCOL_PREFIX = "redis://"; private static final String REDISS_PROTOCOL_PREFIX = "rediss://"; @Bean @ConditionalOnMissingBean public LockAspect lockAspect(RedissonClient redissonClient){ return new LockAspect(redissonClient); } @Bean @ConditionalOnMissingBean public RedissonClient redissonClient(RedisProperties properties){ log.debug("尝试初始化RedissonClient"); // 1.读取Redis配置 RedisProperties.Cluster cluster = properties.getCluster(); RedisProperties.Sentinel sentinel = properties.getSentinel(); String password = properties.getPassword(); int timeout = 3000; Duration d = properties.getTimeout(); if(d != null){ timeout = Long.valueOf(d.toMillis()).intValue(); } // 2.设置Redisson配置 Config config = new Config(); if(cluster != null && !CollectionUtil.isEmpty(cluster.getNodes())){ // 集群模式 config.useClusterServers() .addNodeAddress(convert(cluster.getNodes())) .setConnectTimeout(timeout) .setPassword(password); }else if(sentinel != null && !StrUtil.isEmpty(sentinel.getMaster())){ // 哨兵模式 config.useSentinelServers() .setMasterName(sentinel.getMaster()) .addSentinelAddress(convert(sentinel.getNodes())) .setConnectTimeout(timeout) .setDatabase(0) .setPassword(password); }else{ // 单机模式 config.useSingleServer() .setAddress(String.format("redis://%s:%d", properties.getHost(), properties.getPort())) .setConnectTimeout(timeout) .setDatabase(0) .setPassword(password); } // 3.创建Redisson客户端 return Redisson.create(config); } private String[] convert(List<String> nodesObject) { List<String> nodes = new ArrayList<>(nodesObject.size()); for (String node : nodesObject) { if (!node.startsWith(REDIS_PROTOCOL_PREFIX) && !node.startsWith(REDISS_PROTOCOL_PREFIX)) { nodes.add(REDIS_PROTOCOL_PREFIX + node); } else { nodes.add(node); } } return nodes.toArray(new String[0]); } }
Java
ConditionalOnClass自动装配,引入Redisson依赖时配置才生效
入参的RedisProperties来源:


resources/META-INF/spring.factories:
xml
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.tianji.common.autoconfigure.mq.MqConfig,\ com.tianji.common.autoconfigure.mvc.JsonConfig,\ com.tianji.common.autoconfigure.mvc.MvcConfig,\ com.tianji.common.autoconfigure.mvc.ParamCheckerConfig,\ com.tianji.common.autoconfigure.mybatis.MybatisConfig,\ com.tianji.common.autoconfigure.redisson.RedissonConfig,\ com.tianji.common.autoconfigure.swagger.Knife4jConfiguration, \ com.tianji.common.autoconfigure.xxljob.XxlJobConfig
XML
基本使用
java
@Autowired private RedissonClient redissonClient; @Test void testRedisson() throws InterruptedException { // 1.获取锁对象,指定锁名称 RLock lock = redissonClient.getLock("anyLock"); try { // 2.尝试获取锁,参数:waitTime、leaseTime、时间单位 boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); if (!isLock) { // 获取锁失败处理 .. } else { // 获取锁成功处理 } } finally { // 4.释放锁 lock.unlock(); } }
Java
场景实例
java
/** * 领取优惠劵(分布式锁)悲观锁版本(数据库正常,但Redis超卖并且消息队列可能堆积写,Redis数据不正确,不能直接用,比如直接判断用户领取的卷数量) */ @Override public void receiveCoupon(Long id) { // * 分布式锁防止个人刷单(或lua脚本保证操作原子性,无锁方案) String lockKey = PromotionConstants.COUPON_RECEIVE_REDIS_LOCK_PREFIX + UserContext.getUser(); RLock lock = redissonClient.getLock(lockKey); boolean success = lock.tryLock(); if (!success) { throw new BizIllegalException("领卷业务繁忙"); } try { // * redis查询优惠劵信息 // * 没有查询mysql并放入 if (id == null) { return; } Coupon coupon = queryCouponByCache(id); boolean isFromRedis = coupon != null; // * redis无数据 if (coupon == null) { // * 查数据库 coupon = couponService.getById(id); } // * 校验优惠劵是否存在 if (coupon == null) { throw new DbException("目标优惠劵不存在:" + id); } // * 如果不是从Redis获取,写入Redis重读 if (!isFromRedis) { cacheCouponInfo(coupon); coupon = queryCouponByCache(id); if (coupon == null) { throw new BizIllegalException("优惠劵领取失败"); } } // * 判断发放时间 LocalDateTime now = LocalDateTime.now(); if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) { throw new BizIllegalException("优惠劵不在发放时间内"); } // * 判断库存 if (coupon.getTotalNum() <= 0) { throw new BizIllegalException("优惠劵库存不足"); } // * 统计用户已领取数量 String key = PromotionConstants.USER_COUPON_CACHE_PREFIX + id; Long userId = UserContext.getUser(); // * 可以通过修改此处读取再校验更新为一句increment无锁 Object result = redisTemplate.opsForHash().get(key, userId.toString()); Integer receivedNum = 0; // * redis有数据 if (result != null) { receivedNum = Integer.parseInt(result.toString()); } else { // * 如无数据COUNT返回0 receivedNum = lambdaQuery() .eq(UserCoupon::getUserId, userId) .eq(UserCoupon::getCouponId, id) .count(); } // * 校验单个用户限制领取数 if (receivedNum >= coupon.getUserLimit()) { throw new BizIllegalException("用户领取已达上限"); } // * 更新Redis 用户已领取数量与totalNum redisTemplate.opsForHash().increment(key, userId.toString(), 1L + (result == null ? receivedNum : 0L)); String couponCacheKey = PromotionConstants.COUPON_CACHE_PREFIX + id; // * 前面部分不加锁,可能出现超卖,需要校验结果 Long totalNum = redisTemplate.opsForHash().increment(couponCacheKey, "totalNum", -1L); // * 推送消息至MQ if (totalNum >= 0) { UserCouponDTO dto = new UserCouponDTO(); dto.setCouponId(id); dto.setUserId(userId); rabbitMqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVED, dto); } } finally { lock.unlock(); } }
Java
- waitTime:获取锁的等待时间。当获取锁失败后可以多次重试,直到waitTime时间耗尽。waitTime默认-1,即失败后立刻返回,不重试。
- leaseTime:锁超时释放时间。默认是30,同时会利用WatchDog来不断更新超时时间。需要注意的是,如果手动设置leaseTime值,会导致WatchDog失效。
- TimeUnit:时间单位
通用分布式锁AOP
避免通用的非业务代码对业务的侵入
通过注解标记切入点,同时传递锁参数(名称(解析SPEL表达式得到动态名);等待时间;超时时间;时间单位)
注解
java
package com.tianji.common.autoconfigure.redisson.annotations; import com.tianji.common.autoconfigure.redisson.enums.LockStrategy; import com.tianji.common.autoconfigure.redisson.enums.LockType; 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 Lock { /** * 加锁key的表达式,支持SPEL表达式 */ String name(); /** * 阻塞超时时长,不指定 waitTime 则按照Redisson默认时长 */ long waitTime() default 1; /** * 锁自动释放时长,默认是-1,其实是30秒 + watchDog模式 */ long leaseTime() default -1; /** * 时间单位,默认为秒 */ TimeUnit timeUnit() default TimeUnit.SECONDS; /** * 如果设定了false,则方法结束不释放锁,而是等待leaseTime后自动释放 */ boolean autoUnlock() default true; /** * 锁的类型,包括:可重入锁、公平锁、读锁、写锁 */ LockType lockType() default LockType.DEFAULT; /** * 锁策略,包括5种,默认策略是 不断尝试获取锁,直到成功或超时,超时后抛出异常 */ LockStrategy lockStrategy() default LockStrategy.FAIL_AFTER_RETRY_TIMEOUT; }
Java
切面
java
package com.tianji.common.autoconfigure.redisson.aspect; import com.tianji.common.autoconfigure.redisson.annotations.Lock; import com.tianji.common.exceptions.BizIllegalException; import com.tianji.common.utils.StringUtils; 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.util.ObjectUtils; import java.lang.reflect.Method; import java.util.regex.Matcher; import java.util.regex.Pattern; @Aspect public class LockAspect implements Ordered { /** * SPEL的正则规则 */ private static final Pattern pattern = Pattern.compile("(\\#\\{([^\\}]*)\\})"); /** * 方法参数解析器 */ private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); private final RedissonClient redissonClient; public LockAspect(RedissonClient redissonClient) { this.redissonClient = redissonClient; } //通过环绕加锁,方法执行前加锁,方法执行后根据注解使用解锁 @Around("@annotation(properties)") public Object handleLock(ProceedingJoinPoint pjp, Lock properties) throws Throwable { if (!properties.autoUnlock() && properties.leaseTime() <= 0) { // 不手动释放锁时,必须指定leaseTime时间 throw new BizIllegalException("leaseTime不能为空"); } // 1.基于SPEL表达式解析锁的 name String name = getLockName(properties.name(), pjp); // 2.得到锁对象 RLock rLock = properties.lockType().getLock(redissonClient, name); // 3.尝试获取锁 boolean success = properties.lockStrategy().tryLock(rLock, properties); if (!success) { // 获取锁失败,结束 return null; } try { // 4.执行被代理方法 return pjp.proceed(); } finally { // 5.释放锁 if (properties.autoUnlock()) { rLock.unlock(); } } } /** * 解析锁名称 * * @param name 原始锁名称 * @param pjp 切入点 * @return 解析后的锁名称 */ private String getLockName(String name, ProceedingJoinPoint pjp) { // 1.判断是否存在spel表达式 if (StringUtils.isBlank(name) || !name.contains("#")) { // 不存在,直接返回 return name; } // 2.构建context,也就是SPEL表达式获取参数的上下文环境,这里上下文就是切入点的参数列表 EvaluationContext context = new MethodBasedEvaluationContext( TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer); // 3.构建SPEL解析器 ExpressionParser parser = new SpelExpressionParser(); // 4.循环处理,因为表达式中可以包含多个表达式 Matcher matcher = pattern.matcher(name); while (matcher.find()) { // 4.1.获取表达式 String tmp = matcher.group(); String group = matcher.group(1); // 处理以T或#开头的表达式,避免重复添加# boolean isStaticOrVariable = group.startsWith("T(") || group.startsWith("#"); String expressionStr = isStaticOrVariable ? group : "#" + group; Expression expression = parser.parseExpression(expressionStr); // 4.3.解析出表达式对应的值 Object value = expression.getValue(context); // 4.4.用值替换锁名称中的SPEL表达式 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; } }
Java
注:原代码里缺少了Ordered接口实现,可能会导致优先级跟@Transactional冲突,无法保证锁边界大于事务边界,这里代码补上,其中Orderded接口的getOrder方法用于获取AOP切面执行优先级(越小越优先),事务@Transactional的默认是Integer.MAX_VALUE(参见org.springframework.transaction.annotation.EnableTransactionManagement)

顺带一提,在org.springframework.context.annotation.ConfigurationClassUtils下也能见到类似的东西,不过是用于@Configuration配置类的

枚举
锁类型枚举(策略模式)
java
package com.tianji.common.autoconfigure.redisson.enums; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; public enum LockType { DEFAULT(){ @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); }
Java
也可使用简单工厂模式(写法2)
java
package com.tianji.promotion.utils; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Component; import java.util.EnumMap; import java.util.Map; import java.util.function.Function; import static com.tianji.promotion.utils.MyLockType.*; @Component public class MyLockFactory { private final Map<MyLockType, Function<String, RLock>> lockHandlers; public MyLockFactory(RedissonClient redissonClient) { this.lockHandlers = new EnumMap<>(MyLockType.class); this.lockHandlers.put(RE_ENTRANT_LOCK, redissonClient::getLock); this.lockHandlers.put(FAIR_LOCK, redissonClient::getFairLock); this.lockHandlers.put(READ_LOCK, name -> redissonClient.getReadWriteLock(name).readLock()); this.lockHandlers.put(WRITE_LOCK, name -> redissonClient.getReadWriteLock(name).writeLock()); } public RLock getLock(MyLockType lockType, String name){ return lockHandlers.get(lockType).apply(name); } }
Java
EnumMap纯用数组实现,可能比HashMap更快
Implementation note: All basic operations execute in constant time. They are likely (though not guaranteed) to be faster than their HashMap counterparts.
实际上在业务里也有相关使用,优惠劵折扣策略里
java
package com.tianji.promotion.strategy.discount; import com.tianji.promotion.enums.DiscountType; import java.util.EnumMap; public class DiscountStrategy { private final static EnumMap<DiscountType, Discount> strategies; static { strategies = new EnumMap<>(DiscountType.class); strategies.put(DiscountType.NO_THRESHOLD, new NoThresholdDiscount()); strategies.put(DiscountType.PER_PRICE_DISCOUNT, new PerPriceDiscount()); strategies.put(DiscountType.RATE_DISCOUNT, new RateDiscount()); strategies.put(DiscountType.PRICE_DISCOUNT, new PriceDiscount()); } public static Discount getDiscount(DiscountType type) { return strategies.get(type); } }
Java
锁失败策略(策略模式)
java
package com.tianji.common.autoconfigure.redisson.enums; import com.tianji.common.autoconfigure.redisson.annotations.Lock; import org.redisson.api.RLock; public enum LockStrategy { /** * 不重试,直接结束,返回false */ SKIP_FAST() { @Override public boolean tryLock(RLock lock, Lock properties) throws InterruptedException { return lock.tryLock(0, properties.leaseTime(), properties.timeUnit()); } }, /** * 不重试,直接结束,抛出异常 */ FAIL_FAST() { @Override public boolean tryLock(RLock lock, Lock properties) throws InterruptedException { boolean success = lock.tryLock(0, properties.leaseTime(), properties.timeUnit()); if (!success) { throw new RuntimeException("请求太频繁"); } return true; } }, /** * 重试,直到超时后,直接结束 */ SKIP_AFTER_RETRY_TIMEOUT() { @Override public boolean tryLock(RLock lock, Lock properties) throws InterruptedException { return lock.tryLock(properties.waitTime(), properties.leaseTime(), properties.timeUnit()); } }, /** * 重试,直到超时后,抛出异常 */ FAIL_AFTER_RETRY_TIMEOUT() { @Override public boolean tryLock(RLock lock, Lock properties) throws InterruptedException { boolean success = lock.tryLock(properties.waitTime(), properties.leaseTime(), properties.timeUnit()); if (!success) { throw new RuntimeException("请求超时"); } return true; } }, /** * 不停重试,直到成功为止 */ KEEP_RETRY() { @Override public boolean tryLock(RLock lock, Lock properties) throws InterruptedException { lock.lock(properties.leaseTime(), properties.timeUnit()); return true; } }, ; public abstract boolean tryLock(RLock lock, Lock properties) throws InterruptedException; }
Java
waitTime参数决定重试时间,没有不重试
业务AOP版本
防止兑换优惠劵业务超卖效果展示
- 选择目标优惠劵

- 设置jmeter参数


- 运行结束,检查Redis 100条√ 1人1张√ 0剩余√


- 检查数据库 100√

- 检查汇总报告 √

- 检查jmeter报告搜索仅100请求成功 √

- 检查请求失败原因 优惠卷库存不足√ 请求超时(分布式锁获取超时)√



- 与策略一致 √

代码:
领取优惠劵


java
@Override @Lock(name = "#T(com.tianji.common.constants.PromotionConstants).COUPON_RECEIVE_REDIS_LOCK_PREFIX#T(com.tianji.common.utils.UserContext).getUser()") public void receiveCouponImplWithAnnotation(Long id) { // * 分布式锁防止个人刷单(或lua脚本保证操作原子性,无锁方案) // * redis查询优惠劵信息 // * 没有查询mysql并放入 if (id == null) { return; } Coupon coupon = queryCouponByCache(id); boolean isFromRedis = coupon != null; // * redis无数据 if (coupon == null) { // * 查数据库 coupon = couponService.getById(id); } // * 校验优惠劵是否存在 if (coupon == null) { throw new DbException("目标优惠劵不存在:" + id); } // * 如果不是从Redis获取,写入Redis重读 if (!isFromRedis) { cacheCouponInfo(coupon); coupon = queryCouponByCache(id); if (coupon == null) { throw new BizIllegalException("优惠劵领取失败"); } } // * 判断发放时间 LocalDateTime now = LocalDateTime.now(); if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) { throw new BizIllegalException("优惠劵不在发放时间内"); } // * 判断库存 if (coupon.getTotalNum() <= 0) { throw new BizIllegalException("优惠劵库存不足"); } // * 统计用户已领取数量 String key = PromotionConstants.USER_COUPON_CACHE_PREFIX + id; Long userId = UserContext.getUser(); // * 可以通过修改此处读取再校验更新为一句increment无锁 Object result = redisTemplate.opsForHash().get(key, userId.toString()); Integer receivedNum = 0; // * redis有数据 if (result != null) { receivedNum = Integer.parseInt(result.toString()); } else { // * 如无数据COUNT返回0 receivedNum = lambdaQuery() .eq(UserCoupon::getUserId, userId) .eq(UserCoupon::getCouponId, id) .count(); } // * 校验单个用户限制领取数 if (receivedNum >= coupon.getUserLimit()) { throw new BizIllegalException("用户领取已达上限"); } // * 更新Redis 用户已领取数量与totalNum redisTemplate.opsForHash().increment(key, userId.toString(), 1L + (result == null ? receivedNum : 0L)); String couponCacheKey = PromotionConstants.COUPON_CACHE_PREFIX + id; // * 前面部分不加锁,可能出现超卖,需要校验结果 Long totalNum = redisTemplate.opsForHash().increment(couponCacheKey, "totalNum", -1L); // * 推送消息至MQ if (totalNum >= 0) { UserCouponDTO dto = new UserCouponDTO(); dto.setCouponId(id); dto.setUserId(userId); rabbitMqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVED, dto); } }
Java
兑换优惠劵

java
@Override @Lock(name = "#T(com.tianji.common.constants.PromotionConstants).COUPON_EXCHANGE_REDIS_LOCK_PREFIX#T(com.tianji.common.utils.UserContext).getUser()") /** * * 其实只用锁两行 */ public void exchangeCouponWithAnnotation(String code) { // * 解析兑换码 long id = CodeUtil.parseCode(code); // * 查询兑换码 ExchangeCode exchangeCode = exchangeCodeService.getById(id); // * 是否存在 if (exchangeCode == null) { throw new DbException("目标兑换码不存在:" + id); } // * 判断是否兑换状态 // * 判断是否过期 LocalDateTime now = LocalDateTime.now(); if (exchangeCode.getStatus() != ExchangeCodeStatus.UNUSED || now.isAfter(exchangeCode.getExpiredTime())) { throw new BizIllegalException("兑换码已使用或已过期"); } // * 判断是否超出领取数量 // * 更新状态(优惠卷领取+1;用户卷新增记录) Coupon coupon = couponService.getById(exchangeCode.getExchangeTargetId()); Long userId = UserContext.getUser(); // * 其实只有这两行要锁,可以单独抽出函数锁 IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy(); userCouponService.checkAndCreateUserCouponWithCode(coupon, userId, exchangeCode.getId()); }
Java
- 作者:CamelliaV
- 链接:https://camelliav.netlify.app/article/lock-aop
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。