java 高并发下超购问题解决
//@desc:java 高并发下锁机制初探
//@desc:码字不宜,转载请注明出处
//@author:张慧源 <turing_zhy@163.com>
//@date:2021/12/28
1.探究背景
大家可以看到在高并发下导致积分变动错误,所以想了一些办法解决
2.使用乐观锁去解决
a.添加了version字段
b.做了自定义注解去在乐观锁失败的时候进行重试
/** * 是否进行方法重试注解 * * @author abner<huiyuan.zhang @ hex-tech.net> * @date 2021-12-24 19:59:38 */ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface IsTryAgain { }
/** * 重试切面 * * @author abner<huiyuan.zhang @ hex-tech.net> * @date 2021-12-24 20:40:40 */ @Aspect @Component public class TryAgainAspect { /** * 默认重试几次 */ private static final int DEFAULT_MAX_RETRIES = 5; private int maxRetries = DEFAULT_MAX_RETRIES; public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } @Pointcut("@annotation(cn.hexcloud.m82.points.service.annotation.IsTryAgain)") public void retryOnOptFailure() { // pointcut mark } @Around("retryOnOptFailure()") @Transactional(rollbackFor = Exception.class) public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { int numAttempts = 0; do { numAttempts++; try { //再次执行业务代码 return pjp.proceed(); } catch (TryAgainException ex) { if (numAttempts > maxRetries) { //log failure information, and throw exception // 如果大于 默认的重试机制 次数,我们这回就真正的抛出去了 throw new ApiException(ApiResultEnum.ERROR_TRY_AGAIN_FAILED.getName()); } else { //如果 没达到最大的重试次数,将再次执行 System.out.println("=====正在重试=====" + numAttempts + "次"); } } } while (numAttempts <= this.maxRetries); return null; } }
c.到这里也没啥问题,但是我代码里面还有做幂等的校验
//检查幂等 UserPointsDetail idemInfo = iUserPointsDetailService.checkIdem(orderInAddScoreInDto.getOrderNo()); if (idemInfo != null) { throw new BizException(new RespInfo("该笔积分已经添加过了-idemStr:" + orderInAddScoreInDto.getOrderNo())); }
这个拦不住,所以我就想直接去加锁
2.1 小插曲,中间还想过用for update这种悲观锁去做解决自测锁表的情况很普遍
建议:如果不带主键,建议不要用for update悲观锁,有主键也慎用!
3.先做一个锁性能对代码并发性的影响探究
提前准备:安装abtest压测工具:https://www.jianshu.com/p/a7ee2ffb5c0f
a.不加锁下的并发
并发 195/s
b.synchronized 锁
并发28/s
c.lock 锁
并发 159/s
遗留问题:需要弄清Lock有哪些,每种的作用是什么
然后发现在集群环境下,这些锁也不能解决问题,需要用redis锁
4.Redis 分布式锁
依赖
<!--redis--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>${jedis.version}</version> </dependency> <!--配置redis client--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>${redisson.version}</version> </dependency>
助手函数
package cn.hexcloud.m82.points.service.utils.redis; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.annotation.Resource; /** * redis锁助手函数 * @author abner<huiyuan.zhang@hex-tech.net> * @date 2021-12-29 11:49:34 */ @Component @Slf4j public class RedisLockUtils { /** * 设置超时时间10秒 */ public static final int TIMEOUT = 10*1000; @Resource private StringRedisTemplate stringRedisTemplate; /** * 获取锁过期时间 * @author abner<huiyuan.zhang@hex-tech.net> * @date 2021-12-29 12:02:05 * @return 锁过期时间 */ public Long getLockOverdueTime(){ return System.currentTimeMillis() + TIMEOUT; } /** * 加锁 * @author abner<huiyuan.zhang@hex-tech.net> * @date 2021-12-29 11:50:07 * @param key * @param value 当前时间+超时时间 * @return */ public boolean lock(String key, String value){ if(stringRedisTemplate.opsForValue().setIfAbsent(key, value)){ return true; } String currentValue = stringRedisTemplate.opsForValue().get(key); //如果锁过期 if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){ //获取上一个锁的时间 String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value); if(!StringUtils.isEmpty(oldValue) && currentValue.equals(oldValue)){ return true; } } return false; } /** * 解锁 * @author abner<huiyuan.zhang@hex-tech.net> * @date 2021-12-29 11:51:04 * @param key * @param value 当前时间+超时时间 * @return */ public void unlock(String key, String value){ try{ String currentValue = stringRedisTemplate.opsForValue().get(key); if(!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){ stringRedisTemplate.opsForValue().getOperations().delete(key); } }catch (Exception e){ log.error("【Redis分布式锁】 解锁异常 {}", e.getMessage()); } } }
在impl中使用
Long lockOverdueTime = redisLockUtils.getLockOverdueTime(); String lockKey = Thread.currentThread().getStackTrace()[1].getMethodName() + ":" + partnerId + ":" + userId; boolean isLock = redisLockUtils.lock(lockKey, String.valueOf(lockOverdueTime)); if (!isLock) { throw new TryAgainException(ApiResultEnum.ERROR_TRY_AGAIN); } try { //业务代码 } catch (BizException e) { throw e; } finally { //解锁 redisLockUtils.unlock(lockKey, String.valueOf(lockOverdueTime)); }
redis 锁可以实现很小粒度的锁(我的例子中是租户+用户维度的锁),而且可以解决多节点的并发,是我心头的好!