并发扣减库存(小游戏抽奖使用)工作笔记自用

  /**
     * 加载优惠券缓存
     *
     * @param key        缓存key
     * @param expireTime 过期时间 s
     * @param supplier   数据源
     * @return {@link java.util.List<CouponStockBO>}
     */
    private List<CouponStockBO> loadCouponFormCache(String key, long expireTime, Supplier<Map<String, String>> supplier) {
        //缓存存在,直接返回缓存数据
        Map<String, String> cacheMap = cacheService.hGetAll(key);
        if (MapUtils.isNotEmpty(cacheMap)) {
            return buildCouponStock(cacheMap);
        }

        //缓存不存在,查询数据库
        //阻塞锁,一个线程加载缓存
        String lockKey = LOCK_KEY_PREFIX + key;
        String token = cacheLockService.tryBlockLock(lockKey, LOCK_KEY_EXPIRE, LOCK_TIMEOUT);
        // 超时从数据库中获取
        if (StringUtils.isBlank(token)) {
            log.info("[抽奖优惠卷加载到缓存]锁超时,从数据库获取数据");
            return buildCouponStock(supplier.get());
        }
        try {
            //查询缓存,防止重复加载数据
            Map<String, String> cache = cacheService.hGetAll(key);
            if (MapUtils.isNotEmpty(cache)) {
                return buildCouponStock(cache);
            }
            //查询数据库
            Map<String, String> stringStringMap = supplier.get();
            //没有数据直接返回
            if (MapUtils.isEmpty(stringStringMap)) {
                return Lists.newArrayList();
            }
            //存入缓存
            cacheService.hmSet(key, stringStringMap);
            cacheService.expire(key, expireTime, TimeUnit.SECONDS);
            log.info("[抽奖优惠卷加载到缓存] key = {} , values = {}", key, stringStringMap);
            //返回数据
            return buildCouponStock(stringStringMap);
        } finally {
            cacheLockService.unlock(lockKey, token);
        }

    }


/**
     * redis 扣减 优惠卷库存
     *
     * @param activityId 活动id
     * @param type       优惠券类型
     * @param id         优惠券id
     * @param sourceId   sourceId
     */
    @Override
    public void decrSock(Long activityId, Integer type, Long id, Long sourceId) {
        String key = CouponCacheKeyEnum.COUPON_STOCK_BY_TYPE.getKey(activityId, type, DateUtil.getFormatDate(new Date()));
        // 不存在直接返回
        if (!cacheService.exists(key)) {
            return;
        }
        Long stock = cacheService.hIncrBy(key, buildHashKey(id, sourceId), -1L);
        if (stock < 0) {
            // 无库存
            EventPublisher.push(PrizeMonitor.VALID_COUPON_NOT_EXISTS_ERROR, activityId, type, DateUtil.getWholeDateForNow());
            throw new GameException(CarMemberErrorEnum.MARKET_COUPON_STOCK_EMPTY);
        }

        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCompletion(int status) {
                    if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
                        log.info("[开始游戏事务回滚]数据库更新异常回滚");
                        cacheService.hIncrBy(key, buildHashKey(id, sourceId), 1L);
                    }
                }
            });
        }
    }

 因为每天优惠劵过期时间不同,每天需要加载一次。第一个线程进去时候保证加载进去库存数量,然后redis做预扣减。redis是单线程,且innerdb是的update是行级锁,扣减库存能够保证没问题。

  如果做热点抢购,最好使用redis异步扣减方案。保证不多扣。

 最后使用了事务管理器去监听事务成功与否,对库存做不一致补偿。

猜你喜欢

转载自blog.csdn.net/qq_39809613/article/details/107088662