这两天写了一个支持百万 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;
}
1
bigbigeggs OP |
2
buffzty 115 天前
不是你让压测的吗 500qps 挂了
|
3
bigbigeggs OP @buffzty 老哥,刚才我给下线了,怕扛不住。我想说有没有大佬有好的机器,可以把项目部署一下,压测一波
|
4
buffzty 115 天前
@bigbigeggs 阿里云抢占式服务器 32c 256g 一个小时几毛钱 随开随关
|
5
bigbigeggs OP @buffzty 好的 大佬 别刷了(哭了),明天有空 写了 ip 封禁,超过多少次的,直接给封了
|
6
buffzty 115 天前
我想帮你试试的,2000 个连接瞬间就死 没法测 qps 想让人测 qps 就别封 ip 先把自动重启做了 做成服务或者 docker
|
7
bigbigeggs OP @buffzty 小水管看来撑不住,我看日志 连接池不够用了,拿不到资源,cpu 飙到了 90%多了。明天我加大 web 容器连接池试试,再试试你说的抢占式服务器,明天研究下
|
8
dallaslu 115 天前
发到隔壁 hostloc 试试
|
9
zhhmax 115 天前
看了上面的一些评论,好奇你这个百万 QPS 的结论是怎么得出来的。
|
10
cyrivlclth 114 天前 2
一边说百万 QPS ,一边小水管,一边封 IP ,有够魔幻的
|
11
wantstark 114 天前
不觉明厉- -
|
12
NavsSite 114 天前
|
13
night98 114 天前
来个堆内令牌才能说百万 qps 吧,不然你这妥妥的 redis 热点 key
|
14
bigbigeggs OP @NavsSite @cyrivlclth @zhhmax 代码主要是思路和逻辑,值得学习。最主要代码是 grantPrize 这一块,逻辑我已经贴出来了。其中的思路,比如缓存,锁,随机比例拦截,预减库存等等思路都是处理高并发的手段,很多细节没有一一列举,包括如何保证 redis 库存和 mysql 一致,如果业务在活动中想修改库存怎么办,怎么保证不重复领取等等问题。 至于百万 qps ,也就非常好实现了,在实际 mysql 减库存之前,利用缓存,随机预处理已经拦截了 99%的流量,剩下 1%很大,也可以用 mq 异步来处理。一台 redis 达到 10W qps ,基本工业级别都是分布式,水平扩展,达到百万 qps 非常的简单。
|
15
bigbigeggs OP ”在我自己的小水管,压不上去,如果哪位大佬有比较好的机器,欢迎压测一波,看看性能能到哪里去“ 这里可能被大家误解了,我的意思是 我自己用我的服务器压不上去,我知道瓶颈点不在代码,是在于机器。而很多时候,我们做营销活动,不是说加机器就能把 qps 给提升上去的,所以思路很重要。我的意思是 如果哪位大佬有比较强劲的机器,我的代码都是开源的,很容易部署的,可以放在他的服务器压测一波,看看瓶颈点在哪里
|
16
Pantheoon 114 天前 1
兄弟说句不好听的,这些东西也就面试用用,实际真正复杂的根本就不是技术架构,而是如何用 2 天时间在屎山一样的代码中,拉齐一样是屎山代码的兄弟域,完成不知道多少 qps 的营销活动,并且在上线后被打爆如何扩机器的技术
|
17
cyrivlclth 113 天前
@bigbigeggs #14 你这样整,不如活动换个规则,直接先报名,报名之后直接抽取预发,后面直接白名单发奖就完事了,非要整个一起挤的场景除了面试,我想不出还有谁真的这样做了
|
18
zhangdafoye 113 天前
@Pantheoon 净说大实话 🤣
|
19
bigbigeggs OP @Pantheoon 🤣🤣 兄弟,的确真实。“而是如何用 2 天时间在屎山一样的代码中,拉齐一样是屎山代码的兄弟域” 这句话 我赞同,当然大部分是面试用用。不过在真实的工作中,遇到高并发,缓存这块还是需要第一时间想到的解决手段
|
20
bigbigeggs OP @cyrivlclth 这种营销手段,很少会有用户参与的,太没有参与感了。事实上,在一些节日大促,比如 618 双 11 ,的确会有一波不小的流量,等服务扛不住了,自然而然就会想到例如缓存,业务随机拦截,mq 异步等等,当然大部分业务不会遇到这种情况
|
21
cyrivlclth 112 天前
@bigbigeggs #20 现在 618 双 11 很多都是头一天 20 点前付定金,等到后面付尾款,大大减小了并发量
|