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

    技术2022-07-21  67

    /** * 加载优惠券缓存 * * @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异步扣减方案。保证不多扣。

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

    Processed: 0.009, SQL: 9