写了一个支持百万 QPS 的营销活动,欢迎大家交流沟通

84 天前
 bigbigeggs

这两天写了一个支持百万 QPS 的营销活动,把我想到的优化点全部用上了,甚至比一些工业级别的我感觉都优秀不少,在我自己的小水管,压不上去,如果哪位大佬有比较好的机器,欢迎压测一波,看看性能能到哪里去。欢迎大家沟通交流。

代码 github 链接

体检地址 点我 - 体验地址

优化点(难点、亮点)

代码中优化点用了 redis 预减缓存,随机比例获取奖品,高并发场景拦截大部分用户,乐观锁,mq 直接异步化发放奖品。基本上整个流程不会与数据库进行交互,瓶颈点几乎可以说是没有。这种架构,支撑百万,千万 qps 一点问题都没有。

  1. 核心发奖流程
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;
    }
2137 次点击
所在节点    分享创造
21 条回复
bigbigeggs
84 天前
![1]( https://imgur.com/tcxF4gE)

![1]( https://imgur.com/a/40qPQDB)

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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1068275

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX