秒杀系统-商品详细页多级缓存实战一秒杀系统-商品详细页多级缓存实战二秒杀系统-商品详细页多级缓存实战三商品数据表模块技术难点问题此时有什么问题 目前这个方案有什么问题了我们慢慢发现一个问题只有分类并不能适应所有的需求比如 nike鞋和nikeT恤用户可能希望先看nike的所有商品这个模型就不能满足。我们想在这个关 系中加入“品牌”概念第二个版本商品分类品牌这样基本用户可以在首页上通过分类或者品牌找到自己想要的商品也可以直接查看热门的商品和新上架的商品。但是问题也来了用户在进入分类后展示在用户面前的是很多很多商品用户希望再通过筛选查询出更接近他目标的商品于是优秀的产品设计师设计出了类似这样的UI怎么设计分类管有哪些属性属性管有哪些可选值商品直接勾选对应的可选值前端筛选就是按分类带出对应属性再按属性选项过滤商品。作用筛选用的公共属性决定了商品属于哪个分类、能被用户怎么过滤。例子手机的「CPU 型号骁龙 865」「运行内存12GB」「屏幕尺寸6.6 英寸」牛仔裤的「裤型直筒」「版型修身」「腰型中腰」特点同分类下的商品共用这些属性比如所有手机都有 CPU、内存、屏幕尺寸一个商品绑定固定的属性值不会因为用户选了不同颜色 / 尺寸而改变只影响「能不能被搜出来」不影响价格、库存、图片这些东西一件商品的不同颜色不同尺寸是算一个商品还是多个商品。第四个版本商品分类品牌属性规格货品 SPU商品 SKU关系1 个货品SPU对应 N 个商品SKU一对多关系搜索引擎elasticsearch商品模块展示技术难点前端展示可以分为这么几个维度商品维度(标题、图片、属性等)、主商品维度商品介绍、规格参数、分类维度、商家维度、店铺维度等另外还有一些实时性要求比较高的如实时价格、实时促销、广告词、配送至、预售等是通过异步加载SPU Standard Product Unit 标准化产品单元,SPU是商品信息聚合的最小单位是一组可复用、易检索的标准化信息的集合该集合描述了一个产品的特性。SKU Stock keeping unit(库存量单位) SKU即库存进出计量的单位买家购买、商家进货、供应商备货、工厂生产都是依据SKU进行的在服装、鞋类商品中使用最多最普遍。 例如纺织品中一个SKU通常表示规格、颜色、款式。SKU是物理上不可分割的最小存货单元。SPUiPhone 15 就这一款机型不分颜色、内存SKUiPhone 15 黑色 128GiPhone 15 粉色 256GiPhone 15 蓝色 512G每一条都是独立 SKU单独算库存、单独定价、单独下单。单品页流量特点热点少大部分商品没人看只有少数爆款、热门商品是流量热点流量极度不均匀冷门商品流量极低少数商品扛住大部分访问。爬虫、比价软件疯狂抓取商品详情页是爬虫、比价工具、第三方导购站重点爬的页面请求量大、频率高、无间断真实用户流量之外还掺杂大量机器流量很容易把系统打垮。静态化处理FreeMarker 是一款模板引擎即基于模板和数据源生成输出文本html网页配置文件电子邮件源代码的通用工具。它是一个 java 类库最初被设计用来在MVC模式的Web开发框架中生成HTML页面它没有被绑定到Servlet或HTML或任意Web相关的东西上。也可以用于非Web应用环境中。模板编写使用FreeMarker Template Language(FTL)。使用方式类似JSP的EL表达式。模板中专注于如何展示数据模板之外可以专注于要展示什么数据优缺点这是静态化的一个痛点如果页面的公共模板比如页头、页脚、样式、布局改了需要把所有商品的 HTML 文件重新生成一遍。比如京东上亿个页面改一次模板就要全量重生成成本极高所以后面才会有动态模板、局部静态化的优化方案。京东的量级千万级商品部署在 50 台服务器 / 节点最终是上亿个静态 HTML 文件全靠静态化扛住超大流量。「1 个模板改了所有的静态化页面跟着改」这是静态化的一个痛点如果页面的公共模板比如页头、页脚、样式、布局改了需要把所有商品的 HTML 文件重新生成一遍。比如京东上亿个页面改一次模板就要全量重生成成本极高所以后面才会有动态模板、局部静态化的优化方案。架构方案的问题商品详情页静态化架构 ├─ 核心问题 │ ├─ 新增商品如何同步 │ │ ├─ 方案1scp/rsync推送低效不推荐 │ │ ├─ 方案2定时任务分布式锁简单但延迟高 │ │ └─ 方案3MQ消息通知各节点本地生成推荐 │ ├─ 数据变更如何同步 │ │ ├─ 全量重生成静态文件 │ │ └─ 动静分离动态数据用JS异步拉取 │ ├─ 模板修改如何生效 │ │ ├─ 全量重生成成本极高 │ │ ├─ 局部静态化公共部分动态引入 │ │ └─ 前后端分离放弃静态化 │ └─ 用户如何找到静态页面 │ ├─ 约定URL规则如 /product/{id}.html │ └─ Nginx URL重写 └─ 演进方向 ├─ 纯静态化早期 ├─ 动静分离主流 └─ 前后端分离SSR现代方案核心思路优点缺点1.文件推送scp/rsync一台服务器生成静态文件然后推送到所有节点实现简单直接粗暴节点越多同步成本越高商品数 × 节点数带宽压力大容易出现延迟和一致性问题2.定时任务各节点本地生成每台服务器定时从数据库拉取未静态化的商品本地生成 HTML无需文件同步节点间无依赖必须解决重复执行问题需要分布式锁如 Redis 锁、ZooKeeper定时任务有延迟实时性差3.消息中间件MQ商品新增 / 变更时发送消息到 MQ所有节点订阅 topic收到消息后本地生成 HTML实时性高天然支持多节点分发避免重复同步文件架构复杂度提升需要维护 MQ 集群需保证消息可靠性防丢、防重后台优化缓存提高请求的吞吐量除了减少磁盘IO还有网络IO我们可以发现请求redis其实也会涉及到网络IO我们所有的请求都要走xxx端口号。那有没有更好的优化思路了来同学们你们鲜花在哪儿优化方式解决什么问题优化的 IO 类型作用效果Redis 缓存替代查 MySQL减少磁盘 IO避开数据库磁盘读写大幅降低 DB 压力连接池Redis/DB频繁创建销毁网络连接优化网络 IO复用连接省去三次握手 / 断开的网络开销线程池请求串行阻塞、并发低优化网络 IO 并发处理多线程异步处理网络等待提升整体吞吐量JVM 本地缓存还要走 Redis 内网网络消除网络 IO本机内存直接读不用连 Redis零网络 IOCDN/Nginx 静态缓存后端还要处理请求彻底绕过应用 Redis直接返回静态页无网络、无磁盘 IO缓存专栏缓存一致性最终一致性实时一致性访问量大、QPS高、更新频率不是很高的业务数据一致性要求不高缓存击穿1.加锁在未命中缓存时通过加锁避免大量请求访问数据库2.不允许过期。物理不过期也就是不设置过期时间。而是逻辑上定时在后台异步的更新数据。3.采用二级缓存。L1缓存失效时间短L2缓存失效时间长。请求优先从L1缓存获取数据如果未命中则加锁保证只有一个线程去数据库中读取数据然后再更新到L1和L2中。然后其他线程依然在L2缓存获取数据。缓存穿透布隆过滤器HASH缓存雪崩随机过期时间不过期限流一致性为最终一致性和强一致性一般是mq cancl mysql分布式锁https://www.processon.com/view/link/6044dcb85653bb620cda200c不如八股package com.tuling.tulingmall.service.impl; import com.github.pagehelper.PageHelper; import com.tuling.tulingmall.common.constant.RedisKeyPrefixConst; import com.tuling.tulingmall.component.LocalCache; import com.tuling.tulingmall.component.zklock.ZKLock; import com.tuling.tulingmall.dao.FlashPromotionProductDao; import com.tuling.tulingmall.dao.PortalProductDao; import com.tuling.tulingmall.domain.*; import com.tuling.tulingmall.mapper.SmsFlashPromotionMapper; import com.tuling.tulingmall.mapper.SmsFlashPromotionSessionMapper; import com.tuling.tulingmall.model.SmsFlashPromotion; import com.tuling.tulingmall.model.SmsFlashPromotionExample; import com.tuling.tulingmall.model.SmsFlashPromotionSession; import com.tuling.tulingmall.model.SmsFlashPromotionSessionExample; import com.tuling.tulingmall.service.PmsProductService; import com.tuling.tulingmall.util.DateUtil; import com.tuling.tulingmall.util.RedisOpsUtil; import lombok.extern.slf4j.Slf4j; import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** * ,;,,; * ,;;( 社 * __ ,;; \ 会 * / \~~~ \ /\.) 主 * ,;( ) / |. 义 * ,; \ /-.,,( ) \ 码 * ) / ) / )| 农 * || || \) * (_\ (_\ * * author 图灵学院 * date Created in 2019/12/31 17:22 * version: V1.0 * slogan: 天下风云出我辈一入代码岁月催 * description: **/ Slf4j Service public class PmsProductServiceImpl implements PmsProductService { Autowired private PortalProductDao portalProductDao; Autowired private FlashPromotionProductDao flashPromotionProductDao; Autowired private SmsFlashPromotionMapper flashPromotionMapper; Autowired private SmsFlashPromotionSessionMapper promotionSessionMapper; Autowired private RedisOpsUtil redisOpsUtil; private MapString, PmsProductParam cacheMap new ConcurrentHashMap(); Autowired private LocalCache cache; /* * zk分布式锁 */ Autowired private ZKLock zkLock; private String lockPath /load_db; Autowired RedissonClient redission; /** * 获取商品详情信息 * * param id 产品ID */ public PmsProductParam getProductInfo(Long id) { PmsProductParam productInfo redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, PmsProductParam.class); if (null ! productInfo) { return productInfo; } RLock lock redission.getLock(lockPath id); try { if (lock.tryLock()) { productInfo portalProductDao.getProductInfo(id); System.out.println(走数据库 id); if (null productInfo) { return null; } FlashPromotionParam promotion flashPromotionProductDao.getFlashPromotion(id); if (!ObjectUtils.isEmpty(promotion)) { productInfo.setFlashPromotionCount(promotion.getRelation().get(0).getFlashPromotionCount()); productInfo.setFlashPromotionLimit(promotion.getRelation().get(0).getFlashPromotionLimit()); productInfo.setFlashPromotionPrice(promotion.getRelation().get(0).getFlashPromotionPrice()); productInfo.setFlashPromotionRelationId(promotion.getRelation().get(0).getId()); productInfo.setFlashPromotionEndDate(promotion.getEndDate()); productInfo.setFlashPromotionStartDate(promotion.getStartDate()); productInfo.setFlashPromotionStatus(promotion.getStatus()); } redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, productInfo, 360, TimeUnit.SECONDS); } else { productInfo redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, PmsProductParam.class); } } finally { if (lock.isLocked()){ if (lock.isHeldByCurrentThread()){ lock.unlock(); } } } return productInfo; } /*** * 直接访问数据库 * param id * return */ public PmsProductParam getProductInfo1(Long id) { PmsProductParam productInfo portalProductDao.getProductInfo(id); if (null productInfo) { return null; } FlashPromotionParam promotion flashPromotionProductDao.getFlashPromotion(id); if (!ObjectUtils.isEmpty(promotion)) { productInfo.setFlashPromotionCount(promotion.getRelation().get(0).getFlashPromotionCount()); productInfo.setFlashPromotionLimit(promotion.getRelation().get(0).getFlashPromotionLimit()); productInfo.setFlashPromotionPrice(promotion.getRelation().get(0).getFlashPromotionPrice()); productInfo.setFlashPromotionRelationId(promotion.getRelation().get(0).getId()); productInfo.setFlashPromotionEndDate(promotion.getEndDate()); productInfo.setFlashPromotionStartDate(promotion.getStartDate()); productInfo.setFlashPromotionStatus(promotion.getStatus()); } return productInfo; } /** * 获取商品详情信息 加入redis * * param id 产品ID */ public PmsProductParam getProductInfo2(Long id) { PmsProductParam productInfo null; //从缓存Redis里找 productInfo redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, PmsProductParam.class); if (null ! productInfo) { return productInfo; } productInfo portalProductDao.getProductInfo(id); System.out.println(我被执行了); if (null productInfo) { log.warn(没有查询到商品信息,id: id); return null; } checkFlash(id, productInfo); redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, productInfo, 3600, TimeUnit.SECONDS); return productInfo; } /** * 获取商品详情信息 加入redis 加入锁 * * param id 产品ID */ /** * 获取商品详情信息 加入redis 加入锁 * * param id 产品ID */ public PmsProductParam getProductInfo3(Long id) { PmsProductParam productInfo null; //从缓存Redis里找 productInfo redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, PmsProductParam.class); if (null ! productInfo) { return productInfo; } RLock lock redission.getLock(lockPath id); try { if (lock.tryLock()) { productInfo portalProductDao.getProductInfo(id); if (null productInfo) { log.warn(没有查询到商品信息,id: id); return null; } checkFlash(id, productInfo); redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, productInfo, 3600, TimeUnit.SECONDS); } else { productInfo redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, PmsProductParam.class); } } finally { if (lock.isLocked()) { if (lock.isHeldByCurrentThread()) lock.unlock(); } } return productInfo; } /** * 获取商品详情信息 分布式锁、 本地缓存、redis缓存 * * param id 产品ID */ public PmsProductParam getProductInfo4(Long id) { PmsProductParam productInfo null; productInfo cache.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id); if (null ! productInfo) { return productInfo; } productInfo redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, PmsProductParam.class); if (productInfo ! null) { log.info(get redis productId: productInfo); cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, productInfo); return productInfo; } RLock lock redission.getLock(lockPath id); try { if (lock.tryLock()) { productInfo portalProductDao.getProductInfo(id); if (null productInfo) { return null; } checkFlash(id, productInfo); log.info(set db productId: productInfo); redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, productInfo, 3600, TimeUnit.SECONDS); cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, productInfo); } else { log.info(get redis2 productId: productInfo); productInfo redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, PmsProductParam.class); if (productInfo ! null) { cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE id, productInfo); } } } finally { if (lock.isLocked()) { if (lock.isHeldByCurrentThread()) lock.unlock(); } } return productInfo; } private void checkFlash(Long id, PmsProductParam productInfo) { FlashPromotionParam promotion flashPromotionProductDao.getFlashPromotion(id); if (!ObjectUtils.isEmpty(promotion)) { productInfo.setFlashPromotionCount(promotion.getRelation().get(0).getFlashPromotionCount()); productInfo.setFlashPromotionLimit(promotion.getRelation().get(0).getFlashPromotionLimit()); productInfo.setFlashPromotionPrice(promotion.getRelation().get(0).getFlashPromotionPrice()); productInfo.setFlashPromotionRelationId(promotion.getRelation().get(0).getId()); productInfo.setFlashPromotionEndDate(promotion.getEndDate()); productInfo.setFlashPromotionStartDate(promotion.getStartDate()); productInfo.setFlashPromotionStatus(promotion.getStatus()); } } /** * add by yangguo * 获取秒杀商品列表 * * param flashPromotionId 秒杀活动ID关联秒杀活动设置 * param sessionId 场次活动IDfor example13:00-14:00场等 */ public ListFlashPromotionProduct getFlashProductList(Integer pageSize, Integer pageNum, Long flashPromotionId, Long sessionId) { PageHelper.startPage(pageNum, pageSize, sort desc); return flashPromotionProductDao.getFlashProductList(flashPromotionId, sessionId); } /** * 获取当前日期秒杀活动所有场次 * * return */ public ListFlashPromotionSessionExt getFlashPromotionSessionList() { Date now new Date(); SmsFlashPromotion promotion getFlashPromotion(now); if (promotion ! null) { SmsFlashPromotionSessionExample sessionExample new SmsFlashPromotionSessionExample(); //获取时间段内的秒杀场次 sessionExample.createCriteria().andStatusEqualTo(1);//启用状态 sessionExample.setOrderByClause(start_time asc); ListSmsFlashPromotionSession promotionSessionList promotionSessionMapper.selectByExample(sessionExample); ListFlashPromotionSessionExt extList new ArrayList(); if (!CollectionUtils.isEmpty(promotionSessionList)) { promotionSessionList.stream().forEach((item) - { FlashPromotionSessionExt ext new FlashPromotionSessionExt(); BeanUtils.copyProperties(item, ext); ext.setFlashPromotionId(promotion.getId()); if (DateUtil.getTime(now).after(DateUtil.getTime(ext.getStartTime())) DateUtil.getTime(now).before(DateUtil.getTime(ext.getEndTime()))) { //活动进行中 ext.setSessionStatus(0); } else if (DateUtil.getTime(now).after(DateUtil.getTime(ext.getEndTime()))) { //活动即将开始 ext.setSessionStatus(1); } else if (DateUtil.getTime(now).before(DateUtil.getTime(ext.getStartTime()))) { //活动已结束 ext.setSessionStatus(2); } extList.add(ext); }); return extList; } } return null; } //根据时间获取秒杀活动 public SmsFlashPromotion getFlashPromotion(Date date) { Date currDate DateUtil.getDate(date); SmsFlashPromotionExample example new SmsFlashPromotionExample(); example.createCriteria() .andStatusEqualTo(1) .andStartDateLessThanOrEqualTo(currDate) .andEndDateGreaterThanOrEqualTo(currDate); ListSmsFlashPromotion flashPromotionList flashPromotionMapper.selectByExample(example); if (!CollectionUtils.isEmpty(flashPromotionList)) { return flashPromotionList.get(0); } return null; } /** * 获取首页的秒杀商品列表 * * return */ public ListFlashPromotionProduct getHomeSecKillProductList() { PageHelper.startPage(1, 8, sort desc); FlashPromotionParam flashPromotionParam flashPromotionProductDao.getFlashPromotion(null); if (flashPromotionParam null || CollectionUtils.isEmpty(flashPromotionParam.getRelation())) { return null; } ListLong promotionIds new ArrayList(); flashPromotionParam.getRelation().stream().forEach(item - { promotionIds.add(item.getId()); }); PageHelper.clearPage(); return flashPromotionProductDao.getHomePromotionProductList(promotionIds); } Override public CartProduct getCartProduct(Long productId) { return portalProductDao.getCartProduct(productId); } Override public ListPromotionProduct getPromotionProductList(ListLong ids) { return portalProductDao.getPromotionProductList(ids); } /** * 查找出所有的产品ID * * return */ public ListLong getAllProductId() { return portalProductDao.getAllProductId(); } }