学成-缓存与锁
一、项目优化
在用户未认证状态下也可以访问,如果接口的性能不高,当高并发到来很可能耗尽整个系统的资源,将整个系统压垮,所以特别需要对这些暴露在外边的接口进行优化,这一类接口我们称之为白名单接口
1.常见性能指标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 1. 吞吐量 (TPS - Transactions Per Second) 核心理解: 系统每秒能搞定多少个“完整的业务流程”。 外卖比喻: 餐厅每秒钟能完整接单、做好饭菜并交到外卖员手里的总单量。 技术剖析: 这里的“事务(Transaction)”千万不要局限于数据库层面的事务(比如代码里的 @Transactional)。它指的是业务层面的一次闭环。比如用户发起一次“提交订单”:前端发送请求 -> 网关路由 -> 订单微服务创建记录 -> 扣减商品库存 -> 发起支付处理,这整个链路全部走完,向客户端返回成功,才算作 1 个完整的 TPS。
2. 响应时间 (Response Time, RT) 核心理解: 用户需要等待多久。它是决定用户体验的最直观指标。 外卖比喻: 顾客从手机上点击“下单”,到外卖员把饭菜送到手里的总耗时。 技术剖析: 响应时间不仅包含服务器执行代码和查询数据库的时间,还包含了网络请求在路上的传输时间、以及在网关或线程池里的排队等待时间。 除了你提到的最大、最小和平均响应时间,实际工程中通常更看重 P99 响应时间(即 99% 的请求都在多少毫秒内完成)。因为平均值很容易掩盖掉那 1% 体验极差的卡顿请求。
3. 每秒查询数 (QPS - Queries Per Second) 核心理解: 系统每秒能处理多少次具体的“查询/请求”动作。 外卖比喻: 顾客们每秒钟疯狂刷新了多少次外卖商家的菜单界面。 TPS 与 QPS 的深层关系: 简单链路 (QPS = TPS): 如果是一个单纯的“查询商品详情”接口,只查一次数据库就返回,那么完成一次查询业务(1 TPS)就对应一次接口请求(1 QPS) 复杂链路 (QPS > TPS): 你总结的 QPS = 2 * TPS 非常精准。在分布式架构中,假设一个“加入购物车”的业务动作(算作 1 TPS),需要系统内部先去请求“商品服务”查询状态,再去请求“营销服务”算满减。那么完成这 1 个 TPS 的背后,系统实际承受了 1 次主接口请求 + 2 次内部 RPC 调用的压力,此时对整个系统集群而言,QPS = 3。
4. 错误率 (Error Rate) 核心理解: 系统“掉链子”的概率。 外卖比喻: 餐厅接了 100 个单,结果因为厨房太忙,有 5 个单做错了或者直接漏单了,错误率就是 5%。 技术剖析: 性能测试时,脱离了错误率去谈吞吐量是毫无意义的。必须在“可接受的错误率”前提下寻找最大 TPS。当并发压力持续增大,一旦错误率飙升,通常意味着系统资源(CPU、内存或数据库连接池)已经耗尽。此时往往需要触发服务降级或熔断机制,以保护核心链路不被彻底打垮。
|
还有系统状态
2.jmeter工具
双击bat文件启动
1
| apache-jmeter-5.5\bin\jmeter.bat
|
新建线程组
创建线程
创建 http 请求

具体配置
设置结果展示

3.性能初步优化

日志等级为info
二、引入redis缓存
1.缓存实现

2.引入
src/main/resources/bootstrap.yml
1 2 3
| - data-id: redis-${spring.profiles.active}.yaml group: xuecheng-plus-common refresh: true
|
添加依赖
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.6.2</version> </dependency>
|
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public CoursePublish getCoursePublishCache(Long courseId){ Object jsonObj = redisTemplate.opsForValue().get("course:" + courseId); if(jsonObj!=null){ String jsonString = jsonObj.toString(); System.out.println("=================从缓存查================="); CoursePublish coursePublish = JSON.parseObject(jsonString, CoursePublish.class); return coursePublish; } else { System.out.println("从数据库查询..."); CoursePublish coursePublish = getCoursePublish(courseId); if(coursePublish!=null){ redisTemplate.opsForValue().set("course:" + courseId, JSON.toJSONString(coursePublish)); } return coursePublish; } }
|
3.缓存穿透
解决方式
方案一:缓存空值(Null 缓存)
查询数据库返回空时,主动将空结果写入缓存,并设置较短的 TTL(比如 5 分钟),之后从缓存查到null直接返回
方案二:布隆过滤器(Bloom Filter)
✅ 推荐 在缓存层前再加一道”门”,启动时将所有合法 key 加载到布隆过滤器,请求进来先经过它过滤:
1 2 3
| 请求 → 布隆过滤器判断 ↓ 不存在 → 直接返回空(拦截,不查缓存和DB) ↓ 可能存在 → 查缓存 → 查数据库
|
布隆过滤器有极小的误判率(认为存在但实际不存在),但永不漏判(认为不存在则一定不存在)
方案三:接口层拦截
对于可穷举的 key(如商品 ID 必须为正整数),在业务层直接做参数校验,非法格式直接 reject,不进入缓存逻辑
4.缓存雪崩
平时 Redis 替数据库”挡住”大量请求,一旦大批 key 在同一时刻集体过期,所有流量瞬间直打数据库——就像山坡上的积雪同时滑落,势不可挡
解决方式
1 2 3 4 5 6 7 8 9 10 11
| 过期时间加随机抖动 将所有 key 的 TTL 设为 基础时间 + random(0, 300s),让它们错峰失效,不再同时到期 最简单推荐首选 缓存永不过期 + 后台异步刷新 缓存本身不设 TTL,另起定时任务在后台定期更新数据写回缓存。请求永远打缓存,不穿透到 DB 彻底消除 互斥锁(Mutex) 缓存失效时,只允许一个线程去查 DB 并回填缓存,其他线程等待或返回旧数据,防止并发打穿 DB 防穿透 多级缓存 + 熔断降级 L1 本地缓存 + L2 Redis 双保险;超过 DB 阈值时自动熔断,返回托底数据,保护 DB 不被打垮
|
5.缓存击穿
一个并发访问量非常大的“热点”Key,在它的缓存过期的瞬间,有大量的并发请求同时涌入

在添加锁的时候,最好不要添加到整体方法内
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public CoursePublish getCoursePublishCache(Long courseId){ Object jsonObj = redisTemplate.opsForValue().get("course:" + courseId); if(jsonObj!=null){ String jsonString = jsonObj.toString(); System.out.println("=================从缓存查================="); if("null".equals(jsonString)){ return null; } CoursePublish coursePublish = JSON.parseObject(jsonString, CoursePublish.class); return coursePublish; } else { synchronized (this) { jsonObj = redisTemplate.opsForValue().get("course:" + courseId); if(jsonObj!=null) { String jsonString = jsonObj.toString(); if ("null".equals(jsonString)) { return null; } CoursePublish coursePublish = JSON.parseObject(jsonString, CoursePublish.class); return coursePublish; }
System.out.println("从数据库查询..."); CoursePublish coursePublish = getCoursePublish(courseId); redisTemplate.opsForValue().set("course:" + courseId, JSON.toJSONString(coursePublish)); return coursePublish; }
} }
|
三、多实例分布式锁
上述锁只有在同一个JVM进程中才能生效,为了多实例运行,使用到了同部锁,mysql乐观锁实现同步锁在之前有提到,但是这个方式对数据库查询数过多,故因此,利用其它方式
1.基于 Redis
Redis 凭借其极高的读写性能,是目前企业级项目中最常用的分布式锁方案
核心原理: 利用 Redis 的单线程机制和 SETNX(Set if Not eXists)命令来实现互斥
- 基础实现: 使用
SET key value NX PX 30000 命令
NX:保证只有在键不存在时才能设置成功(获取锁)
PX 30000:设置过期时间为 30 秒,防止应用宕机导致死锁
value:通常设置为一个全局唯一的随机值,解锁时需要校验这个值,防止误删别人的锁(解锁通常配合 Lua 脚本以保证原子性)
2.存在问题
死锁问题与原子性问题

误删别人的锁

业务超时与锁提前过期

3.redisson方式
3.1、阻塞与自旋
阻塞锁

自旋锁

redisson所使用的就是自旋锁的方式
3.2、redisson 具体原理
原子性
Hash 数据结构支持可重入锁

看门狗机制(Watch Dog)- 解决锁提前过期

3.3、具体实现
添加依赖
1 2 3 4 5
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.11.2</version> </dependency>
|
引入配置文件
1 2 3
| redisson: # 配置文件目录 config: classpath:singleServerConfig.yaml
|
具体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| RLock lock = redissonClient.getLock("course:" + courseId); lock.lock(); try { jsonObj = redisTemplate.opsForValue().get("course:" + courseId);
if (jsonObj != null) { String jsonString = jsonObj.toString(); if ("null".equals(jsonString)) { return null; } CoursePublish coursePublish = JSON.parseObject(jsonString, CoursePublish.class); return coursePublish; }
System.out.println("从数据库查询..."); CoursePublish coursePublish = getCoursePublish(courseId); redisTemplate.opsForValue().set("course:" + courseId, JSON.toJSONString(coursePublish)); return coursePublish;
}finally { lock.unlock(); }
|
最终实现缓存只查一次数据库