V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
bigbigeggs
V2EX  ›  分享创造

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

  •  
  •   bigbigeggs · 85 天前 · 2144 次点击
    这是一个创建于 85 天前的主题,其中的信息可能已经有所发展或是发生改变。

    这两天写了一个支持百万 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;
        }
    
    21 条回复    2024-08-30 09:17:37 +08:00
    bigbigeggs
        1
    bigbigeggs  
    OP
       85 天前
    ![1]( https://imgur.com/tcxF4gE)

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

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