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

  • 相关阅读:
    Android 在一个程序中启动另一个程序
    Android SDK Manager国内无法更新的解决方案
    Android studio 安装中遇到一些问题的解决办法,分享一下
    apache服务器伪静态配置说明
    POJ3253 Fence Repair【贪心】
    洛谷P1090 合并果子【贪心】
    POJ3069 Saruman's Army【贪心】
    洛谷P1012 拼数【字符串+排序】
    POJ3617 Best Cow Line【贪心】
    洛谷P1583 魔法照片【模拟+排序】
  • 原文地址:https://www.cnblogs.com/yuanyuanyuan/p/15740197.html
Copyright © 2011-2022 走看看