java性能-秒杀方案

Java
242
0
0
2023-06-02

秒杀方案:

1:拦截秒杀的高配刷新操作

解决方案:对秒杀商品页面独立设计,减少动态内容,页面内容静态化,用户请求不需要经过应用服务。

2:减库存操作

在应用端增加 redis 库存,通过redis分布式锁方式,库存的扣减和回滚在redis中处理,提高性能,当高并发的时候我们可以通过分片思想,将一个 sku 的库存分成N片,redis分布式锁只需要随机获取其中一片锁就可以了,N个分片可以支持N个锁,锁之间是隔离的,扣减的时候优先获取库存多的分片锁,回滚的时候优先获取库存少的锁,通过调整权重值的方式实现

3:秒杀服务隔离

1)通过直接改商品价格为秒杀价的方式处理,不再调用营销中心结算和创建订单

2)商品改为单品单售,这样可以去除很多业务场景,让整个下单业务逻辑变的简单,整个下单的io降到最低

4:提供性能方案

1)涉及到数据库查询类的操作,尽量改为redis或者本地缓存

2)部分影响性能的IO操作,在不影响主流程时,可以通过MQ的方式削峰填谷,使得接口处理能够尽量平稳

5:架构设计

java性能-秒杀方案

6:redis分布式事务和库存扣减

1)redis分布式事务

 @Component
@ Slf4j 
public class JedisLockUtil {

    private static final Long RELEASE_SUCCESS = 1L;
    private static final String LOCK_SUCCESS = "OK";
    /**
     * NX -- Only set the key if it does not already exist.
     * XX -- Only set the key if it already exist.
     */
    private static final String SET_IF_NOT_EXIST = "NX";
    /**
     * EX = seconds; PX = milliseconds
     */
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    @Autowired
    RedisSeckillUtil redisUtil;

    /**
     * redis分布式锁锁定
     *
     * @param activityNo
     * @return
     */
    public String lock(Long skuId, int sharding) {
        String key = getLockKey(skuId, sharding);
        String requestId = UUID.randomUUID().toString();
        log.info("JedisLockUtil lock init key:{},requestId:{}", key, requestId);
        // key是cat:seckill:lock::activityNo
        if (!tryGetDistributedLock(key, requestId, 1000, 5)) {
            throw new CatException(ResCode.CAT_SECKILL_BUSY);
        }
        log.info("JedisLockUtil lock success key:{},requestId:{}", key, requestId);
        return requestId;
    }

    /**
     * redis分布式锁释放
     *
     * @param activityNo
     * @param requestId
     * @return
     */
    public boolean releaseLock(Long skuId, Integer sharding, String requestId) {
        String key = getLockKey(skuId, sharding);
        boolean rs = releaseDistributedLock(key, requestId);
        log.info("JedisLockUtil releaseLock success key:{},requestId:{},rs:{}", key, requestId, rs);
        return rs;
    }

    /**
     * 分布式锁一个sku从1个改为分片个
     *
     * @param skuId
     * @return
     */
    public String getLockKey(Long skuId, int sharding) {
        return RedisSeckillConst.LOCK + skuId + RedisSeckillConst.SEPARATE + sharding;
    }

    /**
     * 根据activityNo获取redis的requestId
     *
     * @param activityNo
     * @return
     */
    public String getRequestId(String lockKey) {
        return redisUtil.get(lockKey, RedisSeckillConst.INDEXDB);
    }

    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param requestId  请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    private boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime, int retryTimes) {
        for (int i = 0; i < retryTimes; i++) {
            String result = redisUtil.tryGetDistributedLock(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime, RedisSeckillConst.INDEXDB);
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            } else {
                // 自旋操作
                try {
                    log.info("线程" + Thread.currentThread().getName() + "占用锁失败,自旋等待结果");
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    continue;
                }
            }
        }
        return false;
    }

    /**
     * 释放分布式锁
     *
     * @param jedis     Redis客户端
     * @param lockKey   锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    private boolean releaseDistributedLock(String lockKey, String requestId) {
        if (RELEASE_SUCCESS.equals(redisUtil.releaseDistributedLock(lockKey, requestId, RedisSeckillConst.INDEXDB))) {
            return true;
        }
        return false;
    }

}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114  

2)分布式锁拦截器

 @Target({ElementType.PARAMETER, ElementType.METHOD})    
@Retention(RetentionPolicy.RUNTIME)    
@Documented    
public  @interface RedisLock {
     String description()  default "";
}
123456  
 @Component
@Scope
@Aspect
@Order(1)
@Slf4j
//order越小越是最先执行,但更重要的是最先执行的最后结束。order默认值是2147483647
public class RedisLockAspect {

    @Autowired
    private JedisLockUtil jedisLockUtil;

    //Service层切点     用于记录错误日志
    @Pointcut("@annotation(io.wharfoo.biztier.cat.domain.service.impl.seckill.RedisLock)")
    public void lockAspect() {

    }

    @Around("lockAspect()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        boolean isLock = false;
        Object obj = null;
        String requestId = null;
        Long skuId = (Long) joinPoint.getArgs()[0];
        Integer sharding = (Integer) joinPoint.getArgs()[3];
        try {
            if (null != skuId) {
                requestId = jedisLockUtil.lock(skuId, sharding);
                isLock = true;
            }
            LogTimeUtil logTimeUtil = new LogTimeUtil();
            logTimeUtil.setStartTime();
            obj = joinPoint.proceed();
            log.info("RedisLockAspectLock skuId:{},sharding:{},cost:{}", skuId, sharding, logTimeUtil.getWasteTime());
        } catch (Throwable e) {
            e.printStackTrace();
            throw e;
        } finally {
            if (isLock) {//释放锁
                jedisLockUtil.releaseLock(skuId, sharding, requestId);
            }
        }
        return obj;
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344  

3)库存扣减和回滚、sku库存分片

 @Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private SeckillService seckillService;

    @Autowired
    private RedisSeckillUtil redisSeckillUtil;

    @Autowired
    private SeckillSkuRepository seckillSkuRepository;

    @Autowired
    private ConfigProperties configProperties;

    @Autowired
    private RedisStockLogRepository redisStockLogRepository;

    SeckillSkuConvertor seckillSkuConvertor = SeckillSkuConvertor.INSTANCE;
    /**
     * 加载 redis缓存 库存
     * 获取花猫cat_seckill_sku表数据(sku_id、packingUnitId和stock),30分钟内要开始的活动加载到redis缓存中
     * 如果activityNo是一样的,则不需要刷新缓存,如果不一致或者redis中不存在则在活动开始前预加载redis库存时需要刷新redis库存
     *
     * @return
     */
    @ Override 
    public Object loadRedisStock() {
        SeckillSku condition  condition = new SeckillSkuCondition();
        condition.setBeginTime(new Date(System.currentTimeMillis() + configProperties.getSeckillLoadSkuTime()));
        List<SeckillSkuE> list = seckillSkuRepository.queryList(condition);
        Map<String, String>  hash  = new HashMap<>();
        if (!CollectionUtils. isEmpty (list)) {
            for (SeckillSkuE seckillSkuE : list) {
                SeckillSku seckillSkuERedis = getSeckillSkuEByRedis(seckillSkuE.getSkuId(), seckillSkuE.getPackingUnitId());
                // 处理redis中的SeckillSku和数据库不一致的场景,需要刷新redis缓存,重新分配库存
                if (null == seckillSkuERedis || (null != seckillSkuERedis.getActivityNo() && !seckillSkuERedis.getActivityNo().equals(seckillSkuE.getActivityNo()))) {
                    Map<String, String> stockHash = new HashMap<>();
                    // 根据分片数,库存进行分片处理
                    int sharding = seckillSkuE.getSharding();
                    // 最后一个用尾差作为库存
                    int remain = seckillSkuE.getRemainStock();
                    int remainStock = seckillSkuE.getRemainStock() / sharding;
                    for (int i = 1; i < sharding; i++) {
                        remain -= remainStock;
                        stockHash.put(String.valueOf(i), String.valueOf(remainStock));
                    }
                    stockHash.put(String.valueOf(sharding), String.valueOf(remain));
                    SeckillSku seckillSku = seckillSkuConvertor.entityToDto(seckillSkuE);
                    hash.put(SeckillSkuE.getRedisEntityField(seckillSku.getSkuId(), seckillSku.getPackingUnitId()), JSON.toJSONString(seckillSku));
                    if (!org.springframework.util.CollectionUtils.isEmpty(stockHash)) {
                        redisSeckillUtil.hmset(SeckillSkuE.getRedisKey(seckillSkuE.getSkuId(), seckillSkuE.getPackingUnitId()), stockHash, RedisSeckillConst.INDEXDB);
                    }
                }
            }
        }
        if (!org.springframework.util.CollectionUtils.isEmpty(hash)) {
            redisSeckillUtil.hmset(SeckillSkuE.getRedisEntityKey(), hash, RedisSeckillConst.INDEXDB);
        }
        return null;
    }

    @Override
    public SeckillSku getSeckillSkuEByRedis(Long skuId, Long packingUnitId) {
        String entity_key = SeckillSkuE.getRedisEntityKey();
        String value = redisSeckillUtil.hget(entity_key, SeckillSkuE.getRedisEntityField(skuId, packingUnitId), RedisSeckillConst.INDEXDB);
        if (StringUtil.isNotEmptyAndNotNull(value)) {
            return JSON.parseObject(value, SeckillSku.class);
        }
        SeckillSkuCondition condition = new SeckillSkuCondition();
        condition.setSkuId(skuId);
        condition.setPackingUnitId(packingUnitId);
        List<SeckillSkuE> seckillSkuES = seckillSkuRepository.queryList(condition);
        if (CollectionUtils.isNotEmpty(seckillSkuES)) {
            Date curDate = new Date();
            for (SeckillSkuE seckillSkuE : seckillSkuES) {
                if (seckillSkuE.getStartTime().compareTo(curDate) >= 0 && seckillSkuE.getEndTime().compareTo(curDate) <= 0) {
                    SeckillSku seckillSku = seckillSkuConvertor.entityToDto(seckillSkuE);
                    redisSeckillUtil.hset(SeckillSkuE.getRedisEntityKey(), SeckillSkuE.getRedisEntityField(seckillSku.getSkuId(), seckillSku.getPackingUnitId()),
                            JSON.toJSONString(seckillSku), RedisSeckillConst.INDEXDB);
                    return seckillSku;
                }
            }
        }
        return null;
    }

    @Override
    public Map<String, SeckillSku> getSeckillSkuEMapByRedis() {
        Map<String, SeckillSku> seckillSkuMap = new HashMap<>();
        String entity_key = SeckillSkuE.getRedisEntityKey();
        Map<String, String> map = redisSeckillUtil.hgetall(entity_key, RedisSeckillConst.INDEXDB);
        if (!org.springframework.util.CollectionUtils.isEmpty(map)) {
            Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, String> entry = iterator.next();
                seckillSkuMap.put(entry.getKey(), JSON.parseObject(entry.getValue(), SeckillSku.class));
            }
        }
        return seckillSkuMap;
    }

    /**
     * 是否是秒杀的sku
     *
     * @param skuId
     * @return
     */
    @Override
    public boolean isContainSeckillSku(List<String> fieldList) {
        String entity_key = SeckillSkuE.getRedisEntityKey();
        Map<String, String> values = redisSeckillUtil.hgetall(entity_key, RedisSeckillConst.INDEXDB);
        if (!org.springframework.util.CollectionUtils.isEmpty(values)) {
            for (String field : fieldList) {
                String value = values.get(field);
                if (null != value) {
                    SeckillSku seckillSku = JSON.parseObject(value, SeckillSku.class);
                    if (seckillSkuConvertor.dtoToEntity(seckillSku).isSeckill()) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * 获取秒杀的库存
     *
     * @param skuId
     * @return
     */
    @Override
    public Integer getSeckillSkuStock(Long skuId, Long packingUnitId, int sharding) {
        String stockRedisKey = SeckillSkuE.getRedisKey(skuId, packingUnitId);
        String stock = redisSeckillUtil.hget(stockRedisKey, String.valueOf(sharding), RedisSeckillConst.INDEXDB);
        if (io.wharfoo.biztier.sign.util.StringUtil.isNotEmptyAndNotNull(stock)) {
            return Integer.valueOf(stock);
        }
        Integer remain = 0;
        // 获取已锁库存,剩余库存=库存-已锁定库存
        SeckillSku seckillSku = seckillService.getSeckillSkuEByRedis(skuId, packingUnitId);
        if (null != seckillSku) {
            Long lockQuantity = redisStockLogRepository.sumLockedByCondition(seckillSku.getActivityNo(), skuId, packingUnitId, sharding);
            if (null == lockQuantity || lockQuantity < 0) {
                log.info("getSeckillSkuStock sumLockedByCondition is error activityNo:{}, skuId:{}, packingUnitId:{}, sharding:{}, type:{}, lockQuantity:{}", seckillSku.getActivityNo(), skuId, packingUnitId, sharding, seckillSku.getType(), lockQuantity);
            }
            if (lockQuantity == null || lockQuantity < 0) {
                lockQuantity = 0L;
            }
            log.info("getSeckillSkuStock sumLockedByCondition activityNo:{}, skuId:{}, packingUnitId:{}, sharding:{}, type:{}, lockQuantity:{}", seckillSku.getActivityNo(), skuId, packingUnitId, sharding, seckillSku.getType(), lockQuantity);
            // 如果是最后一个分片,需要根据尾差获取分配的库存,其他的根据库存除以分片数
            int shardingStock = seckillSku.getStock() / seckillSku.getSharding();
            if (sharding == seckillSku.getSharding()) {
                shardingStock = seckillSku.getStock() - (seckillSku.getSharding() - 1) * shardingStock;
            }
            remain = shardingStock - lockQuantity.intValue();
            if (remain < 0) {
                remain = 0;
            }
        }
        redisSeckillUtil.hset(stockRedisKey, String.valueOf(sharding), String.valueOf(remain), RedisSeckillConst.INDEXDB);
        return remain;
    }

    /**
     * 更新redis库存
     * RedisLock:redis分布式锁,更新redis库存前先获取分布式锁
     *
     * @param skuId
     * @param quantity
     * @return
     */
    @RedisLock
    @Override
    public Long updateSkuStock(Long skuId, Long packingUnitId, Long quantity, Integer sharding) {
        if (quantity == 0) {
            throw new CatException(ResCode.CAT_SECKILL_SKU_QUANTITY_EMPTY);
        }
        // 获取redis库存
        Integer stock = getSeckillSkuStock(skuId, packingUnitId, sharding);
        // 扣减库存需要校验库存
        if (quantity < 0) {
            if (stock <= 0) {
                throw new CatException(ResCode.CAT_SECKILL_SKU_STOCK_EMPTY);
            }
            if (stock < Math.abs(quantity)) {
                throw new CatException(ResCode.CAT_SECKILL_SKU_STOCK_LOW);
            }
        }
        String key = SeckillSkuE.getRedisKey(skuId, packingUnitId);
        Long remain = redisSeckillUtil.hincrBy(key, String.valueOf(sharding), quantity, RedisSeckillConst.INDEXDB);
        log.info("updateSeckillSkuStock skuId:{},sharding:{},quantity:{},stock:{},remain:{}", skuId, sharding, quantity, stock, remain);
        return remain;
    }

    /**
     * 获取当前的分片
     * 如果是扣减库存,获取所有的分片库存,库存小于扣减的库存需要过滤掉,库存值作为权重值,随机值获取后获取当前分片值,优先库存多的先扣减
     * 如果是增加库存,获取所有的分片库存,每片分配的库存-库存值作为权重值,随机值获取后获取当前分片值,优先剩余库存少的先增加
     *
     * @return
     */
    public Integer getStockRedisSharding(Long skuId, Long packingUnitId, Long quantity) {
        SeckillSku seckillSkuERedis = getSeckillSkuEByRedis(skuId, packingUnitId);
        int stock = seckillSkuERedis.getStock() / seckillSkuERedis.getSharding();
        List<SeckillSku> shardingStockList = getStockRedis(skuId, packingUnitId);
        if (quantity < 0) {
            shardingStockList = shardingStockList.stream().filter(e -> e.getRemainStock() >= Math.abs(quantity) && e.getRemainStock() > 0).collect(Collectors.toList());
        } else if (quantity > 0) {
            shardingStockList = shardingStockList.stream().filter(e -> e.getRemainStock() > 0).map(e -> {
                if (e.getSharding().compareTo(seckillSkuERedis.getSharding()) == 0) {
                    // 计算尾差
                    e.setRemainStock(seckillSkuERedis.getStock() - (seckillSkuERedis.getSharding() - 1) * stock - e.getRemainStock());
                } else {
                    e.setRemainStock(stock - e.getRemainStock());
                }
                return e;
            }).collect(Collectors.toList());
        }
        // 获取权重值
        int allProportion = 0;
        for (SeckillSku seckillSku : shardingStockList) {
            allProportion += seckillSku.getRemainStock();
        }
        // 获取分片值
        if (allProportion > 0) {
            int random = new Random().nextInt(allProportion);
            int curProportion = 0;
            for (SeckillSku seckillSku : shardingStockList) {
                curProportion += seckillSku.getRemainStock();
                if (curProportion >= random) {
                    return seckillSku.getSharding();
                }
            }
        }
        return RedisSeckillConst.SHARDING;
    }

    /**
     * 获取当前skuId的所有的库存
     * key:分片值,value:当前分片库存
     *
     * @param skuId
     * @param packingUnitId
     * @return
     */
    private List<SeckillSku> getStockRedis(Long skuId, Long packingUnitId) {
        List<SeckillSku> list = new ArrayList<>();
        String stockRedisKey = SeckillSkuE.getRedisKey(skuId, packingUnitId);
        Map<String, String> stockMap = redisSeckillUtil.hgetall(stockRedisKey, RedisSeckillConst.INDEXDB);
        if (!org.springframework.util.CollectionUtils.isEmpty(stockMap)) {
            Iterator<Map.Entry<String, String>> iterator = stockMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, String> entry = iterator.next();
                SeckillSku seckillSku = new SeckillSku();
                seckillSku.setSharding(NumberUtil.toInt(entry.getKey(), RedisSeckillConst.SHARDING));
                seckillSku.setRemainStock(NumberUtil.toInt(entry.getValue(), 0));
                list.add(seckillSku);
            }
        }
        return list;
    }
}