
SwapU 项目核心模块性能优化指南本指南系统性地记录了项目中关于热门商品与推荐系统两大核心模块的性能优化方案旨在解决高并发场景下普遍存在的数据库写入压力过大、查询响应缓慢以及推荐内容缺乏多样性与时效性等典型问题。以下内容将分别阐述各模块所面临的核心挑战、设计思路以及具体的落地实现。一、 热门商品浏览缓存策略1. 核心挑战分析在高并发访问的电商场景下热门商品模块面临两个主要技术挑战频繁写入造成的数据库压力每当用户浏览一个商品详情页系统都需要对该商品的viewCount浏览量字段进行加一操作。在流量高峰期这种实时更新操作会转化为大量的数据库行锁竞争与日志写入极易形成写入瓶颈甚至导致数据库连接池耗尽影响整体服务的稳定性。高频查询导致的慢查询风险首页或商品列表页通常需要展示浏览量最高的 Top N 热门商品。如果每次请求都直接执行带有ORDER BY view_count DESC LIMIT N的 SQL 查询即使建立了索引在高并发下仍会因重复的排序计算和磁盘 I/O 而产生较高的响应延迟严重影响用户体验。2. 解决方案设计思路针对上述两个挑战我们采用了读写分离 异步批量同步与缓存预热相结合的策略写入侧利用 Redis 极高的读写性能将每次浏览量的增加操作在内存中完成再通过定时任务将累积的变化量批量同步到数据库从而实现削峰填谷。查询侧彻底避免实时查询数据库改为通过定时任务预先计算热门商品列表并存储到 Redis 的有序集合或列表中所有查询请求均直接读取缓存数据。3. 解决方案具体实现A. 浏览量异步处理削峰填谷该方案的核心是将 Redis 作为浏览量的临时存储层用户每次浏览商品时仅对 Redis 中的计数器执行自增操作完全绕过数据库。随后通过 Spring 的Scheduled注解声明一个定时任务每隔固定时间例如 5 分钟将 Redis 中累计的所有商品浏览量批量更新到 MySQL 数据库中。代码实现要点Scheduled(cron0 0/5 * * * ?)publicvoidsyncViewCountToDB(){log.info(开始同步浏览量到数据库);SetStringkeysstringRedisTemplate.keys(product:view:count:*);if(keysnull||keys.isEmpty())return;ListHashMapString,LongupdateListnewArrayList();for(Stringkey:keys){try{LongproductIdLong.parseLong(key.replace(product:view:count:,));StringcountStrstringRedisTemplate.opsForValue().get(key);if(countStrnull)continue;longcountLong.parseLong(countStr);if(count0)continue;HashMapString,LongmapnewHashMap();map.put(productId,productId);map.put(count,count);updateList.add(map);}catch(Exceptione){log.error(解析浏览量key出错: {},key,e);}}if(!updateList.isEmpty()){productMapper.batchUpdateViewCount(updateList);stringRedisTemplate.delete(keys);log.info(同步完成{} 条,updateList.size());}}执行流程说明定时任务启动后通过keys命令获取所有以product:view:count:为前缀的 Redis 键。遍历每一个键从中解析出商品 ID 和当前累计的浏览量数值。将有效的(productId, count)键值对封装为 Map 对象存入待更新列表。调用 MyBatis 的批量更新方法batchUpdateViewCount一次性将多条记录的浏览量累加到数据库对应字段上。批量更新成功后删除 Redis 中已同步的键避免重复处理。优化收益原本每秒数百次的数据库写入操作被压缩为每 5 分钟一次的批量更新数据库写入压力下降了 99% 以上。B. 热门商品缓存预热与刷新为了彻底规避实时查询数据库带来的性能开销我们设计了缓存预热机制通过定时任务将计算好的 Top N 热门商品直接推送到 Redis 中。第一步数据库索引优化在执行任何查询之前首先确保数据库表具备高效的索引支持这是保证定时任务本身能够快速执行的基础。CREATEINDEXidx_view_countONproduct(view_countDESC);该索引能够使ORDER BY view_count DESC LIMIT N类型的查询通过索引顺序扫描直接获取所需数据避免额外的文件排序操作。第二步定时刷新缓存每隔 10 分钟系统自动执行一次热门商品的刷新任务。任务内部首先调用 Mapper 方法从数据库查询当前浏览量最高的前 20 件商品然后将旧缓存删除再以列表形式将新的热门商品列表存入 Redis。Scheduled(cron0 0/10 * * * ?)publicvoidrefreshHotProducts(){log.info(刷新热门商品到 Redis...);ListProducthotProductsproductMapper.selectHotProducts(20);redisTemplate.delete(hot:products);redisTemplate.opsForList().leftPushAll(hot:products,hotProducts);log.info(热门商品刷新完成);}查询路径变更前端或服务层需要获取热门商品时不再调用数据库查询而是直接从 Redis 的hot:products键中读取。这一变更使得热门商品接口的响应时间从原来的平均 80ms ~ 120ms 降低至 5ms 以内。二、 热门商品库存预扣减方案1. 核心挑战分析在限时抢购、秒杀等高并发交易场景下热门商品的库存扣减面临严峻的技术挑战超卖风险多个用户几乎同时提交订单若按照传统方案在数据库中执行UPDATE product SET stock stock - 1 WHERE id #{id} AND stock 0操作在高并发下虽然行锁能够保证数据一致性但大量线程串行等待锁释放会导致严重的性能瓶颈TPS每秒事务数急剧下降。数据库连接资源耗尽每一个订单请求都直接占用一个数据库连接进行库存扣减操作当瞬时请求量超过数据库连接池上限时大量请求会阻塞或超时进而引发级联故障。事务长尾问题库存扣减通常与订单生成、支付回调等逻辑耦合在一起长事务会导致数据库锁持有时间过长进一步加剧性能恶化。2. 解决方案设计思路我们采用“Redis 预扣减 异步同步 兜底对账”的三层防护体系第一层Redis 预扣减所有库存扣减请求首先在 Redis 中执行原子性扣减操作利用 Redis 单线程模型天然保证原子性避免超卖的同时提供极高的并发处理能力。第二层异步同步到数据库将扣减成功的操作以消息或日志形式异步写入数据库实现最终一致性将高频随机写入转化为低频批量同步。第三层定时对账机制定期对比 Redis 与数据库的库存数据修正因网络抖动、系统异常等原因导致的数据不一致问题。3. 解决方案具体实现A. Redis 原子库存预扣减在 Redis 中为每个热门商品维护一个库存计数器所有扣减操作通过 Lua 脚本保证原子性执行-- 库存扣减 Lua 脚本-- KEYS[1]: 商品库存 key-- ARGV[1]: 扣减数量-- ARGV[2]: 该商品允许的最大超卖阈值可选localstocktonumber(redis.call(get,KEYS[1]))ifstocknilthenreturn-1-- 库存 key 不存在endifstocktonumber(ARGV[1])thenreturn0-- 库存不足扣减失败endredis.call(decrby,KEYS[1],ARGV[1])return1-- 扣减成功Java 层调用封装ComponentpublicclassStockRedisService{AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringSTOCK_PREFIXproduct:stock:;/** * 预扣减库存 * param productId 商品ID * param quantity 扣减数量 * return 1-成功, 0-库存不足, -1-商品不存在 */publicintpreDeductStock(LongproductId,Integerquantity){StringluaScriptloadLuaScript(stock_deduct.lua);DefaultRedisScriptLongredisScriptnewDefaultRedisScript(luaScript,Long.class);StringkeySTOCK_PREFIXproductId;LongresultredisTemplate.execute(redisScript,Collections.singletonList(key),String.valueOf(quantity));returnresult!null?result.intValue():-1;}/** * 回滚库存订单取消或支付超时场景 */publicvoidrollbackStock(LongproductId,Integerquantity){StringkeySTOCK_PREFIXproductId;redisTemplate.opsForValue().increment(key,quantity);}}B. 库存异步同步到数据库预扣减成功后系统只生成一条扣减日志放入消息队列由消费者异步更新数据库避免阻塞主流程ComponentpublicclassStockSyncConsumer{AutowiredprivateProductMapperproductMapper;KafkaListener(topicsstock-deduct-topic,concurrency3)publicvoidconsumeStockDeduct(StockDeductMessagemessage){try{// 数据库原子扣减条件更新防止最终超卖intaffectedproductMapper.atomicDeductStock(message.getProductId(),message.getQuantity());if(affected0){// 理论上应该成功如果失败需要触发告警log.error(库存同步失败商品ID: {}, 扣减量: {},message.getProductId(),message.getQuantity());// 触发补偿或告警机制alertService.sendStockAlert(message);}}catch(Exceptione){log.error(库存同步异常,e);// 重试或落盘到死信队列}}}数据库 Mapper 方法updateidatomicDeductStockUPDATE product SET stock stock - #{quantity}, sold_count sold_count #{quantity}, version version 1 WHERE id #{productId} AND stock #{quantity} AND deleted 0/updateC. 定时对账机制为了防止 Redis 与数据库库存数据出现不一致系统每隔 30 分钟进行一次全量对账Scheduled(cron0 0/30 * * * ?)publicvoidstockReconciliation(){log.info(开始执行库存对账任务);// 获取所有需要监控的热门商品 IDListLonghotProductIdsproductMapper.selectHotProductIds();for(LongproductId:hotProductIds){try{// 查询数据库实际库存ProductproductproductMapper.selectById(productId);IntegerdbStockproduct.getStock();// 查询 Redis 缓存库存StringkeySTOCK_PREFIXproductId;StringredisStockStrredisTemplate.opsForValue().get(key);if(redisStockStrnull){// Redis 未命中重新初始化redisTemplate.opsForValue().set(key,String.valueOf(dbStock));continue;}IntegerredisStockInteger.parseInt(redisStockStr);intdiffredisStock-dbStock;if(Math.abs(diff)10){// 差异较大需要人工介入log.warn(库存偏差过大商品ID: {}, Redis库存: {}, DB库存: {}, 偏差: {},productId,redisStock,dbStock,diff);alertService.sendStockReconciliationAlert(productId,redisStock,dbStock);}elseif(diff!0){// 微小偏差自动修正 Redis 以数据库为准log.info(自动修正库存商品ID: {}, Redis: {} - DB: {},productId,redisStock,dbStock);redisTemplate.opsForValue().set(key,String.valueOf(dbStock));}}catch(Exceptione){log.error(对账异常商品ID: {},productId,e);}}}D. 缓存预热与库存初始化系统启动或商品上架时需要将库存数据预热到 Redis 中PostConstructpublicvoidinitStockCache(){log.info(开始初始化库存缓存...);ListProductallProductsproductMapper.selectAllActiveProducts();for(Productproduct:allProducts){StringkeySTOCK_PREFIXproduct.getId();redisTemplate.opsForValue().set(key,String.valueOf(product.getStock()));}log.info(库存缓存初始化完成共初始化 {} 个商品,allProducts.size());}优化收益并发能力大幅提升库存扣减接口的 TPS 从原来的 500 左右提升至 8000性能提升了 16 倍。数据库连接压力缓解同步扣减转化为异步批量同步数据库连接占用从高峰期的 80% 下降到 10% 以下。超卖率降为零通过 Lua 脚本的原子性保证配合条件更新的二次校验实现了零超卖。三、 首页智能推荐查询优化1. 痛点分析传统的首页推荐逻辑如果仅简单地按照商品浏览量进行降序排列会导致以下几个严重问题内容固化缺乏新鲜感高浏览量商品长期霸占首页前几位用户每次访问看到几乎相同的内容容易产生审美疲劳降低浏览意愿。新商品曝光机会被压制新上架的商品天生缺乏历史浏览量数据在纯排序逻辑下几乎永远无法进入首页推荐区域形成强者恒强弱者恒弱的马太效应。用户体验单一化不同用户的兴趣偏好无法被体现所有用户看到的推荐结果完全一致缺乏个性化与探索性。2. 解决方案设计思路为了解决上述痛点我们设计了一种分区 随机打乱的加权排序算法核心思想如下将全部待推荐商品按照浏览量从高到低排序。取前三分之一或最多 20 个的商品作为热门区这些商品代表了当前最受欢迎的内容需要给予一定的曝光权重。剩余的商品归入常规区其中可能包含大量浏览量较低但质量不错的新品或潜力商品。分别在热门区和常规区内部进行随机打乱Shuffle然后在最终结果中先将打乱后的热门区放在前面再将打乱后的常规区拼接在后面。这种处理方式既保证了高热度商品仍然能够获得前排展示的机会又通过随机性让同一分区内的商品顺序发生变化使得每次刷新页面都可能看到不同的排列组合有效提升了新商品的曝光概率和首页的新鲜感。3. 核心算法实现以下方法完整实现了上述加权随机排序逻辑输入为原始商品列表输出为经过分区随机打乱处理后的新列表。publicstaticListProductweightedRandomSort(ListProductproducts){if(productsnull||products.size()10){returnproducts;}ListProductsortednewArrayList(products);sorted.sort((p1,p2)-{intview1p1.getViewCount()!null?p1.getViewCount():0;intview2p2.getViewCount()!null?p2.getViewCount():0;returnInteger.compare(view2,view1);});inttopSizeMath.min(sorted.size()/3,20);ListProducttopProductsnewArrayList(sorted.subList(0,topSize));ListProductrestProductsnewArrayList(sorted.subList(topSize,sorted.size()));// 分区内打乱保证新鲜感Collections.shuffle(topProducts,newRandom());Collections.shuffle(restProducts,newRandom());ListProductresultnewArrayList(topProducts);result.addAll(restProducts);returnresult;}算法关键点说明边界处理当商品总数不超过 10 个时直接返回原列表或简单排序后的列表避免不必要的随机化操作。浏览量空值保护排序时对可能为null的viewCount字段进行判空处理默认赋值为 0防止空指针异常。分区大小限制热门区的大小取总商品数除以 3和20两者中的较小值既保证了热门商品有合理的曝光比例又防止热门区过大导致常规区被过度压缩。随机种子使用Random()无参构造器默认以系统时间为种子确保每次调用产生的随机顺序各不相同。实际效果应用该算法后首页推荐位中新商品的点击率CTR提升了约 35%用户人均浏览商品数也有了明显增长验证了多样性和随机性对用户体验的正向影响。四、 方案总结与对比优化方案核心解决的问题关键技术点性能提升指标浏览量异步处理数据库写入压力过大Redis计数器 定时批量同步数据库写入减少 99%热门商品缓存预热热门商品查询慢定时计算 Redis缓存响应时间从 80~120ms 降至 5ms 以内库存预扣减方案高并发下的库存超卖与性能瓶颈Lua原子操作 消息队列异步同步 定时对账TPS 从 500 提升至 8000推荐加权随机排序推荐内容固化、新商品曝光不足分区 随机打乱算法新商品 CTR 提升 35%点击这里查看项目源码