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

23 天前
 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;
    }
1938 次点击
所在节点    分享创造
21 条回复
cyrivlclth
20 天前
@bigbigeggs #20 现在 618 双 11 很多都是头一天 20 点前付定金,等到后面付尾款,大大减小了并发量

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

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

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

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

© 2021 V2EX