这两天写了一个支持百万 QPS 的营销活动,把我想到的优化点全部用上了,甚至比一些工业级别的我感觉都优秀不少,在我自己的小水管,压不上去,如果哪位大佬有比较好的机器,欢迎压测一波,看看性能能到哪里去。欢迎大家沟通交流。
代码 github 链接
体检地址 点我 - 体验地址
代码中优化点用了 redis 预减缓存,随机比例获取奖品,高并发场景拦截大部分用户,乐观锁,mq 直接异步化发放奖品。基本上整个流程不会与数据库进行交互,瓶颈点几乎可以说是没有。这种架构,支撑百万,千万 qps 一点问题都没有。
public boolean grantPrize(String phone, String activity) {
if (StringUtils.isAnyEmpty(activity, phone)) {
throw new RuntimeException(ERROR_MSG);
}
// phone 为幂等键
String key = StrUtil.format(ACTIVITY_PHONE_LOCK, activity, phone);
boolean success = RedisUtils.tryLock(key, redissonClient, () -> {
//1. 幂等处理,这里还可以优化,因为 grantId 是一个唯一索引,插入失败就是重复领取,但可能失败次数会比较多
MktActivityPrizeGrant mktActivityPrizeGrant = mktActivityPrizeGrantDao.getMktActivityPrizeGrant(phone);
if (mktActivityPrizeGrant != null && StringUtils.isNotEmpty(mktActivityPrizeGrant.getGrantId())) {
throw new RuntimeException("请勿重复领取");
}
// 2. 这里一个优化, 随机比例获取奖品,可以随时调整
int seed = ThreadLocalRandom.current().nextInt(0, 100) + 1; // 1-100
int random = NumberUtils.toInt(RedisUtils.get(CACHE_MKT_ACTIVITY_PRIZE_RANDOM, stringRedisTemplate));
if (seed > random) {
//log.warn("随机比例被拦截 seed = {}, random = {}", seed, random);
throw new RuntimeException("随机比例拦截 - " + ERROR_MSG);
}
// 3. 缓存预减库存
Long num = RedisUtils.decr(CACHE_MKT_ACTIVITY_PRIZE_NUM, stringRedisTemplate);
if (num == null || num < 0) {
// 将 redis 库存加回,可做可不做,看业务需求
RedisUtils.incr(CACHE_MKT_ACTIVITY_PRIZE_NUM, stringRedisTemplate);
throw new RuntimeException("redis 库存不足 - " + ERROR_MSG);
}
MktActivityPrize activityPrize = activityCacheService.getActivityPrize();
// 4. 真正数据库减库存,并且插入发奖记录
// 如果 redis 预减库存成功,这里大概率会成功,基本不会失败,如果失败,放弃重试,失败重试会影响系统性能,重试次数越多,对系统性能的影响越大。
Boolean execute = transactionTemplate.execute(status -> {
// 4.1 扣减库存
Integer update = mktActivityPrizeDao.occupyActivityPrize(activityPrize.getActivityId(), activityPrize.getPrizeId());
if (update == null || update <= 0) {
//log.warn("mysql 扣减库存失败 update = {}", update);
throw new RuntimeException("mysql 库存扣减失败 - " + ERROR_MSG);
}
// 4.2 插入发奖记录
MktActivityPrizeGrant grant = buildMktActivityPrizeGrant(phone, activityPrize);
Integer insert = mktActivityPrizeGrantDao.insert(grant);
if (insert == null || insert <= 0) {
//log.warn("mysql 插入发奖记录失败 insert = {}", insert);
throw new RuntimeException("mysql 插入发奖记录失败 - " + ERROR_MSG);
}
return true;
});
return execute;
});
return success;
}
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.