一.缓存是什么缓存Cache是计算机系统中一种用于临时存储数据的技术其核心目的是提高数据访问速度和降低后端系统的负载。二.缓存的利与弊1利1.提升响应速度低延迟原理缓存通常基于内存如 Redis访问速度是纳秒/微秒级而数据库通常基于磁盘访问速度是毫秒级。2.保护后端数据库高吞吐原理大部分读请求被缓存拦截只有未命中的请求才会到达数据库。3.降低系统成本原理通过缓存抗住流量可以减少数据库服务器的数量或配置要求。4.削峰填谷原理在突发流量如秒杀活动到来时缓存可以像蓄水池一样吸收大量读请求。5.减少重复计算场景对于复杂的统计报表、AI 推理结果或耗时的 API 调用。2弊1.数据一致性问题最核心痛点问题缓存中的数据是数据库数据的副本。当数据库更新时如果缓存没有及时更新或删除用户就会读到“脏数据”旧数据。难度在高并发场景下保证“数据库”和“缓存”强一致性非常困难且代价高昂通常需要牺牲性能。大多数场景只能做到“最终一致性”。2.增加系统复杂度架构变重引入缓存意味着多了一个组件如 Redis 集群需要维护其高可用、持久化、扩容等。代码变复杂开发人员需要编写额外的逻辑来处理缓存命中、未命中、更新、删除、过期等策略代码出错概率增加。3.经典的“三大坑”如果不加小心缓存会引发三种典型的灾难性故障缓存穿透 (Cache Penetration)查询不存在的数据缓存不存请求直打数据库。恶意攻击者可利用此点搞挂数据库。缓存击穿 (Cache Breakdown)某个热点 Key突然过期此时大量并发请求同时涌入直接击穿缓存打到数据库。缓存雪崩 (Cache Avalanche)大量 Key 同时过期或者缓存服务整体宕机导致所有请求瞬间涌向数据库造成数据库崩溃。4.内存成本与容量限制成本内存比磁盘贵得多。为了获得高性能需要投入更多资金购买大容量内存。限制内存容量有限必须设计合理的淘汰策略如 LRU。如果策略不当可能导致频繁淘汰热点数据降低缓存命中率使缓存形同虚设。5.数据时效性延迟问题为了保证性能通常不会实时同步缓存。这意味着用户看到的数据可能不是最新的例如下单后库存显示未减需等待几秒刷新。影响对于对实时性要求极高的场景如银行余额、股票交易缓存的使用受到严格限制。三.添加缓存1.流程图2.实战应用3.代码实现Service public class ShopServiceImpl extends ServiceImplShopMapper, Shop implements IShopService { Autowired private StringRedisTemplate stringRedisTemplate; Override public Result queryById(Long id) { //从Redis中查询 String shopJson stringRedisTemplate.opsForValue().get(cache:shop: id); //判断是否存在 if (StrUtil.isNotBlank(shopJson)) { Shop shop JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } //不存在根据id查询数据库 Shop shop getById(id); //如果数据库中不存在返回结果 if (shop null) { return Result.fail(店铺不存在); } //存在写入Redis stringRedisTemplate.opsForValue().set(cache:shop: id, JSONUtil.toJsonStr(shop)); return Result.ok(shop); } }Service public class ShopTypeServiceImpl extends ServiceImplShopTypeMapper, ShopType implements IShopTypeService { Autowired private StringRedisTemplate stringRedisTemplate; Override public Result queryTypeList() { //去redis中查询 String shopTypeJson stringRedisTemplate.opsForValue().get(cache:shop:type); //判断是否存在 if (StrUtil.isNotBlank(shopTypeJson)){ ListShopType typeList JSONUtil.toList(shopTypeJson, ShopType.class); return Result.ok(typeList); } //查询数据库 ListShopType typeList query().orderByAsc(sort).list(); //不存在返回错误 if (typeList.isEmpty()) { // 返回成功状态但带上提示信息 return Result.fail(店铺不存在); } //存在写入redis中并返回结果 stringRedisTemplate.opsForValue().set(cache:shop:type, JSONUtil.toJsonStr(typeList)); return Result.ok(typeList); } }四..缓存更新策略1.三种策略根据业务需要选择低一致性方案和高一致性方案。2.主动更新的三种策略1.Cache Aside Pattern由缓存的调用者在更新数据库的同时更新缓存2.Read/Write Through Pattern缓存与数据库整合为一个服务由服务来维护一致性。调用者调用该服务无需关心缓存一致性问题。3.Write Behind Caching Pattern调用者只操作缓存由其它线程异步的将缓存数据持久化到数据库保证最终一致。企业中使用最多的为第一种策略3.操作缓存和数据库中需要考虑的问题操作缓存和数据库时有三个问题需要考虑删除缓存还是更新缓存更新缓存每次更新数据库都更新缓存无效写操作较多 ❌删除缓存更新数据库时让缓存失效查询时再更新缓存 ✅如何保证缓存与数据库的操作的同时成功或失败单体系统将缓存与数据库操作放在一个事务分布式系统利用TCC等分布式事务方案3.先操作缓存还是数据库图中展示的是“先删缓存再更新DB”与“先更新DB再删缓存”的并发问题。前者易导致旧数据被重新写入缓存后者在极端时序下可能使新值被旧查询覆盖。后者的发生频率较低所以我们选择先更新数据库在删除缓存4.代码实现Override public Result update(Shop shop) { Long id shop.getId(); if (id null) { return Result.fail(店铺id不能为空); } //更新数据库 updateById(shop); //删除缓存 stringRedisTemplate.delete(cache:shop: shop.getId()); return Result.ok(); }五.缓存穿透当客户端频繁请求根本不存在的数据如 ID 为 -1 或随机伪造 key由于缓存和数据库中都没有该数据每次请求都会直达数据库造成数据库压力剧增甚至被拖垮。这就是“缓存穿透”。1.缓存空对象即使查询结果为空比如数据库返回 null也把这个“空值”写入缓存并设置一个较短的过期时间TTL如 5 分钟。优点实现简单代码改动小。维护方便无需额外组件。缺点浪费内存大量无效 key 占用 Redis 空间。短期不一致如果数据后来被创建但缓存还在 TTL 内用户仍会读到旧空值。适用场景对一致性要求不高、数据量不大、追求快速上线的场景。2.布隆过滤器在请求到达 Redis 之前先通过布隆过滤器判断这个 key “是否可能存在”。→ 如果过滤器说“不存在”直接拒绝请求→ 如果说“可能存在”才放行去查缓存/数据库。布隆过滤器是概率型数据结构不会漏判false negative但可能误判false positive—— 即它说“有”实际可能没有但它说“没有”那一定没有。优点节省内存不存储真实 key只存哈希位图。无冗余 key避免缓存中堆积大量空值。缺点实现复杂需引入第三方库或自行实现。存在误判少数合法请求可能被错误拦截可通过调整参数降低概率。删除困难标准布隆过滤器不支持删除元素可用计数布隆过滤器解决。适用场景高并发、大数据量、对资源敏感且能接受极低误判率的系统。3.代码实现Override public Result queryById(Long id) { //从Redis中查询 String shopJson stringRedisTemplate.opsForValue().get(cache:shop: id); //判断是否存在 if (StrUtil.isNotBlank(shopJson)) { Shop shop JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } //判断是否为空值 if (shopJson ! null) { return Result.fail(店铺不存在); } //不存在根据id查询数据库 Shop shop getById(id); //如果数据库中不存在返回结果 if (shop null) { //存在写入Redis stringRedisTemplate.opsForValue().set(cache:shop: id, JSONUtil.toJsonStr(shop),30L,TimeUnit.MINUTES); return Result.fail(店铺不存在); } return Result.ok(shop); }增加了两个核心判断第一个是判断是否为空值第二个是如果是空值的话将空值存入redis中六.缓存雪崩缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机导致大量请求到达数据库带来巨大压力。解决方案给不同的Key的TTL添加随机值利用Redis集群提高服务的可用性给缓存业务添加降级限流策略给业务添加多级缓存七.缓存击穿缓存击穿是指在一个高并发系统中某个热点 KeyHot Key在缓存中过期失效的瞬间恰好有大量并发请求同时访问该 Key。由于缓存中已无数据所有请求会直接穿透缓存瞬间全部打到后端数据库上。这会导致数据库负载急剧飙升甚至造成数据库宕机进而导致整个系统服务不可用。有两种解决方法一种是互斥锁一种是逻辑过期1.互斥锁vs逻辑过期方案一互斥锁Mutex Lock优点没有额外的内存消耗不需要在缓存值中嵌入额外字段节省空间。保证数据一致性同一时间只有一个线程能重建缓存确保返回给用户的始终是最新数据。实现简单直观基于 Redis 的SETNX或分布式锁框架如 Redisson即可快速落地。缺点线程需要等待性能受影响未抢到锁的请求必须阻塞或轮询重试增加响应延迟。可能存在死锁风险若加锁后异常未释放锁或服务器宕机可能导致后续请求永久阻塞需配合 TTL finally 释放机制规避。方案二逻辑过期Logical Expiration优点线程无需等待性能较好即使缓存“逻辑上”已过期仍立即返回旧数据用户无感知后台异步更新不影响主流程。无死锁风险不依赖阻塞式锁机制彻底避免死锁问题。缺点不保证强一致性在缓存重建完成前部分用户可能读到稍旧的数据最终一致性。有额外内存消耗需在 Value 中存储逻辑过期时间戳略微增加内存占用。实现复杂度高需设计数据结构、管理异步线程池、处理双重检查等开发和维护成本较高。