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 锁可以实现很小粒度的锁(我的例子中是租户+用户维度的锁),而且可以解决多节点的并发,是我心头的好!

 

posted @ 2021-12-28 13:04  源源猿  阅读(162)  评论(0编辑  收藏  举报