小天管理 发表于 2024年8月27日 发表于 2024年8月27日 这两天写了一个支持百万 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; }
已推荐帖子