/**
* 加载优惠券缓存
*
* @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异步扣减方案。保证不多扣。
最后使用了事务管理器去监听事务成功与否,对库存做不一致补偿。