type
status
date
slug
summary
tags
category
icon
password
📌
This is one excerpt from the main website(CN) blog, check link below to know more.

Distributed Lock,Customized AOP,Redisson,SPEL

Distributed Lock

  • Background: The “Synchronized” lock in Java is implemented based on the JVM Monitor. In a clustered environment, multiple JVMs mean multiple monitors, which cannot achieve mutual exclusion. Therefore, there should be one single lock across multiple instances, which is called distributed lock.
  • It seems a Distributed Lock can be implemented with Redis?
      1. Redis can be accessed by multiple JVM instances.
      1. The SETNX command provides mutual exclusion.
      1. The DEL command can be used to release the lock.
      1. Redis executes commands in a single-threaded (serial) manner.
  • Potential Issues:
    • Direct Implementation:
      • SET lock thread1 NX EX 20 – This involves two main steps.
    • 1. Timeout Release:
      • The lock may not be properly released (for example, if redis instance crashes), which could lead to a deadlock. Setting an expiration time is necessary to mitigate this (in the example, the lock acquisition and expiration time setting are performed atomically).
    • 2. Storing thread Identifier:
      • The lock stores the identifier of the thread that acquired it, and it should only be deleted if the identifier matches. This helps prevent accidental release of the lock (however, it cannot be completely avoided).
        Image source(CN website): https://b11et3un53m.feishu.cn/wiki/wikcnkbaeh4T9AyYlM7rHSaKMsc
        notion image
        (IMG:thread 1 got blocked before its release of lock, the lock got itself released due to timeout which allowed thread 2 to get its share, yet thread 1 then awake and ruin its day)
        notion image
        (IMG:now thread 1 can no longer release the lock after timeout since the identifier is thread 2, not thread 1, and thread 1 cannot release the lock for the identifier does not correspond)
         
        However, since the check and deletion are not atomic, there is still a risk of accidental deletion.
        notion image
        (IMG:thread 1 may check the identifier before its release move, and then directly release after its awakening from block, rendering the identifier mechanism ineffective)
       
      From above, we learn:
      Timeout-based release does not entirely prevent accidental deletion, and lock release operations require atomicity, there is also a problem on delays in master-slave synchronization, and the same thread may not be able to acquire the same lock multiple times (potentially leading to deadlocks), therefore such issues need to be addressed:
      • 1. Timeout Issue: Use a WatchDog mechanism. When a lock is successfully acquired, a scheduled task is started that automatically extends the lock’s expiration before it expires, thereby preventing a timeout release. This task also stops if the instance crashes, avoiding deadlocks.
      • 2. Atomicity Issue: Use Lua scripts to ensure atomic operations.
      • 3. Lock Reentrancy: Similar to the behavior of synchronized, a hash can be used to record the holder and the reentrancy count. The lock is deleted when the count drops to zero.
      • 4. Master-Slave Synchronization Delay: Use the RedLock.
      • 5. Lock Failure Retry: Implement mechanisms to retry obtaining the lock upon failure.
  • Mature Solution:
    • Redisson provides a well-tested, comprehensive implementation that addresses these issues.

Redisson Quick Start

maven import

xml

<!--redisson--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> </dependency>
XML

Config class (with autoconfigure)

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.Read Redis Config from the param 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.config for redisson Config config = new Config(); if(cluster != null && !CollectionUtil.isEmpty(cluster.getNodes())){ // cluster mode config.useClusterServers() .addNodeAddress(convert(cluster.getNodes())) .setConnectTimeout(timeout) .setPassword(password); }else if(sentinel != null && !StrUtil.isEmpty(sentinel.getMaster())){ // sentinel mode config.useSentinelServers() .setMasterName(sentinel.getMaster()) .addSentinelAddress(convert(sentinel.getNodes())) .setConnectTimeout(timeout) .setDatabase(0) .setPassword(password); }else{ // single mode config.useSingleServer() .setAddress(String.format("redis://%s:%d", properties.getHost(), properties.getPort())) .setConnectTimeout(timeout) .setDatabase(0) .setPassword(password); } // 3.create redisson client return Redisson.create(config); } // include the redis protocol in case you forget 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 autoconfigure - this config class only takes effect when you import Redisson dependency
Where does this RedisProperties come from?:
from the nacos config center
notion image
the config file (you may add these to the local config file in case you do not use config center)
notion image
resources/META-INF/spring.factories(for autoconfigure, mainly for conditional config class):

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

Basic usage of redisson client

java

@Autowired private RedissonClient redissonClient; @Test void testRedisson() throws InterruptedException { // 1.Which lock is in interest RLock lock = redissonClient.getLock("anyLock"); try { // 2.Try get the lock,Param: waitTime、leaseTime、time unit boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); if (!isLock) { // fail logic .. } else { // success logic } } finally { // 4.release lock.unlock(); } }
Java
Business Scenario

java

/** * lock to avoid one may receive coupons more than single user limit */ @Override public void receiveCoupon(Long id) { // * distributed lock to avoid one passing single user limit(or use lua, lock-free) String lockKey = PromotionConstants.COUPON_RECEIVE_REDIS_LOCK_PREFIX + UserContext.getUser(); RLock lock = redissonClient.getLock(lockKey); boolean success = lock.tryLock(); if (!success) { throw new BizIllegalException("ReceiveCoupon Business Busy"); } try { // * illegal coupon id if (id == null) { return; } // * query redis for coupon info Coupon coupon = queryCouponByCache(id); boolean isFromRedis = coupon != null; // * no such info in redis if (coupon == null) { // * then goto mysql database coupon = couponService.getById(id); } // * no such coupon if (coupon == null) { throw new DbException("No Such Coupon:" + id); } // * If data not from Redis,then write to Redis and read from it (concurrency reason) if (!isFromRedis) { cacheCouponInfo(coupon); coupon = queryCouponByCache(id); if (coupon == null) { throw new BizIllegalException("Fail to receive coupon"); } } // * Is within issue time? LocalDateTime now = LocalDateTime.now(); if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) { throw new BizIllegalException("Coupon not issuing"); } // * stock enough? if (coupon.getTotalNum() <= 0) { throw new BizIllegalException("No enough stock"); } // * how much the user has received? String key = PromotionConstants.USER_COUPON_CACHE_PREFIX + id; Long userId = UserContext.getUser(); Object result = redisTemplate.opsForHash().get(key, userId.toString()); Integer receivedNum = 0; if (result != null) { receivedNum = Integer.parseInt(result.toString()); } else { // * 0 if no data receivedNum = lambdaQuery() .eq(UserCoupon::getUserId, userId) .eq(UserCoupon::getCouponId, id) .count(); } // * should not pass user limit if (receivedNum >= coupon.getUserLimit()) { throw new BizIllegalException("User limit reached"); } // * Update Redis: count already received and 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); // * Async write with 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:how long you can wait, you can retry repeatedly before the time runs out. -1 by default,return immediately after failure, no retry.
  • leaseTime:how long you can use the lock before compulsory release。by default 30,keep renewing with WatchDog.
  • TimeUnit:in literal

Distributed Lock AOP (General Scenario)

  • avoid boilerplate code coupling with business logic
  • mark pointcut with annotation, passing params(lock name[SPEL expression, dynamic name], waitTime, leaseTime, timeunit) of lock meanwhile

Annotation

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

Aspect

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
Implement Ordered interface so you make sure the lock wraps @Transactional(any value less than Integer.MAX_VALUE should do), i.e higher precedence(lower value) in AOP.
This value of @Transactional is by default Integer.MAX_VALUE(fyr, org.springframework.transaction.annotation.EnableTransactionManagement)
notion image
btw, you may find something similar at:
org.springframework.context.annotation.ConfigurationClassUtils
except it’s for @Configuration
notion image

ENUM

Lock Type(Strategy Pattern)

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
Simple factory pattern(another way)

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 implemented with pure array, likely faster than HashMap, fyr.
Implementation note: All basic operations execute in constant time. They are likely (though not guaranteed) to be faster than their HashMap counterparts.
Example in coupon discount strategy

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
Lock fail strategy

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 defines the time you may retry,no retry if no such param

Rewrite API with AOP

Results devoid of oversell(both for total and single user limit)
  • Find some lucky coupon
notion image
  • Fill its id in jmeter, configure threads and loop count (1k user, each try 3 times on the same coupon with 1 per user limit)
notion image
notion image
  • Done, check Redis 100 rows√ 1 per user√ 0 total left√
notion image
notion image
  • check DB 100 issue√
notion image
  • check summary report √
notion image
  • check jmeter report, should be only 100 true√
notion image
  • check reasons for failed http requests, 优惠卷库存不足(No enough inventory)√ 请求超时(Timeout for distributed lock retrying)√
notion image
notion image
notion image
  • Align with the failure strategy √
notion image
Full Code:
receive coupon
notion image
notion image

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
exchange coupon
notion image

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
 
Chrome extension recommendation中文主站访问右上角Main Site
Loading...
CamelliaV
CamelliaV
Java Dev; Medical CV; ACGN
Latest posts
Download Youtube 4K Video
2025-6-3
Customized AOP for Distributed Lock
2025-3-21
中文主站访问右上角Main Site
2025-3-21
Chrome extension recommendation
2025-2-24
Announcement
🧨Under Construction
 
2024-2025CamelliaV.

CamelliaV | Java Dev; Medical CV; ACGN


  1. 1 给予你的爱 Xi YuaN/Digital Vengeance/唢清
  2. 2 スペルビア帝国/夜 平松建治
  3. 3 Imagination QQHHh
  4. 4 virtues QQHHh
  5. 5 Tricolor (short ver.) Digital Vengeance/44
  6. 6 港口夜 - 四周年 月代彩
  7. 7 神よ、その黄昏よ 金﨑猛
  8. 8 絆炎 (English Ver) Katherine Eames
  9. 9 ラストエンゲージ~祈りの呪文 馬場泰久
  10. 10 an evening calm fripSide
  11. 11 フレスベルグの少女~風花雪月~ Caro
  12. 12 Answer 北原春希/小木曽雪菜
  13. 13 Kiss Kiss Kiss BENI
  14. 14 远航高歌 染音若蔡/阿南
  15. 15 Sentimental Blue Trident
  16. 16 目指す先にあるもの Falcom Sound Team J.D.K.
  17. 17 Night City r e l/Artemis Delta
  18. 18 Gimme×Gimme P*Light/Giga/初音ミク/鏡音リン
  19. 19 桃幻浪漫 Airots/Active Planets & AUGUST
  20. 20 DESIRE 美郷あき
  21. 21 镜花堂(feat.芬璃尔) 幻塔手游/Rux
  22. 22 she was sitting under the osmanthus tree 梶浦由記
给予你的爱 - Xi YuaN/Digital Vengeance/唢清
00:00 / 03:59