本文记录在学习Redis过程中的关键技术实践与踩坑思考,包含个人在实际学习中的具体过程 与遇到的问题 以及知识点总结
希望可以给一起学习的大家带来帮助ヽ( ̄▽ ̄)ノ
文章目录
2.商户查询缓存
2.1什么是缓存
缓存就是数据交换的缓冲区,是存储数据的临时地方,一般读写性能较高
缓存的作用:
- 降低后端负载
- 提高读写效率,降低响应时间
缓存的成本:
- 数据一致性成本
- 代码维护成本
- 运维成本
2.2添加商户缓存
具体流程

ShopController
java
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
ShopServiceImpl
java
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 根据id查询
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
//1.从Redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.若存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4.若不存在根据id查询数据库
Shop shop = getById(id);
//5.若不存在返回错误
if(shop == null){
return Result.fail("店铺不存在");
}
//6.若存在返回数据并将其存入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
为啥判断String时用isNotBlank
str != null:只能防 null,防不住""。如果 Redis 存了个空串,转 JSON 会报错。!str.isEmpty():防不住 null,会报空指针异常 (NPE)。StrUtil.isNotBlank(str):同时防住了null、""和" "。只要里面有一点点有效字符,它才认为是"有数据"
2.3添加商户类型缓存
ShopTypeController
java
@GetMapping("list")
public Result queryTypeList() {
return typeService.queryTypeList();
}
ShopTypeServiceImpl
java
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryTypeList() {
//1.从Redis中查询店铺类型
String key = "cache:shop:type";
String shopTypeJSON = stringRedisTemplate.opsForValue().get(key);
//2.判断缓存是否存在
if(StrUtil.isNotEmpty(shopTypeJSON)){
//3.存在,直接返回
List<ShopType> typeList = JSONUtil.toList(shopTypeJSON, ShopType.class);
return Result.ok(typeList);
}
//4.不存在,查询数据库
// 使用 MyBatis-Plus 查询所有数据,通常店铺类型需要按排序字段(sort)升序排列
List<ShopType> typeList = this.list(new LambdaQueryWrapper<ShopType>()
.orderByAsc(ShopType::getSort));
//5.若不存在,返回错误
if(typeList == null || typeList.size() == 0){
return Result.fail("店铺类型不存在");
}
//6.若存在,写入Redis,返回数据
String jsonStr = JSONUtil.toJsonStr(typeList);
stringRedisTemplate.opsForValue().set(key,jsonStr,30, TimeUnit.MINUTES);
return Result.ok(typeList);
}
List<ShopType> typeList = this.list(new LambdaQueryWrapper<ShopType>().orderByAsc(ShopType::getSort));
- 调用mybatisplus的list方法,执行查询并返回
List<ShopType>集合new LambdaQueryWrapper<ShopType>()穿件Lambda构造器.orderByAsc(ShopType::getSort)构建排序语句
2.4缓存更新策略
为解决缓存数据的不一致,有以下策略:

业务场景:
- 低一致性要求:使用内存淘汰机制,eg:店铺类型的查询缓存
- 高一致性需求:主动更新,并以超市剔除作为兜底方案,eg:店铺详情查询的缓存
主动更新也有3种策略:

最常用的就是第一种
01操作缓存和数据库时需要考虑三个问题:
-
删除缓存还是更新缓存
- 更新缓存:每次更新数据库都需要更新缓存,无效写操作比较多 ❌️
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存 ✅️
-
如何保证缓存和数据库的操作同时成功或失败
- 单体系统:将缓存和数据库操作放在一个事务
- 分布式系统:利用TCC等分布式事务方案
-
先操作缓存还是数据库
操作Redis比操作数据库快得多,两种方法都有可能造成数据不一致,但是先操作数据库的这个方法发生不一致的可能性更低
2.5实现商铺缓存与数据库的双写一致
给查询商铺的缓存添加超时剔除和主动更新的策略:
- 根据id查询店铺时,若缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时期限
- 根据id修改店铺时,先修改数据库再删除缓存

在查询操作这里添加超时期限
ShopController
java
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
return shopService.update(shop);
}
更新操作
java
/**
* 更新店铺
* @param shop
* @return
*/
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if(id == null){
return Result.fail("店铺id不能为空");
}
//1.更新数据库
update(shop);
//2.删除缓存
stringRedisTemplate.delete("cache:shop:" + shop.getId());
return Result.ok();
}
2.6缓存穿透的解决思路
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存用不生效,这些请求都会打到数据库
常见的解决方案有两种:
- 缓存空对象
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致
- 布隆过滤
- 优点:内存占用少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能

2.7解决商铺查询的缓存穿透问题
使用缓存空对象的方法解决

修改一下根据id查询的方法
java
/**
* 根据id查询
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
//1.从Redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.若存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//判断命中的是否为空值
if(shopJson != null){
return Result.fail("店铺信息不存在");
}
//4.若不存在根据id查询数据库
Shop shop = getById(id);
//5.若不存在返回错误
if(shop == null){
//将空值写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id,"",30,TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
//6.若存在返回数据并将其存入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),2, TimeUnit.MINUTES);
return Result.ok(shop);
}
java//判断命中的是否为空值 if(shopJson != null){ return Result.fail("店铺信息不存在"); }为啥不能改成
shopJSON == "",因为==比对的是地址,""和Redis返回的空串是两个不同的对象,地址不一样
2.8缓存雪崩问题
缓存雪崩指在同一时间段大量的缓存key同时失效或者Redis宕机,导致大量请求到达数据库,带来巨大压力

解决方案:
- 给不同的key的TTL添加随机值
- 利用Redis集群提供服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
2.9缓存击穿问题
缓存击穿问题也叫热点key问题,就是一个被高并发访问 并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大冲击

解决方案:
-
互斥锁

-
逻辑过期


2.10利用互斥锁解决缓存击穿
需求:修改根据id查询商铺的业务,基于互斥锁方式解决缓存击穿问题

可以使用Redis的setnx实现互斥锁,(如果键不存在才设置)
java
/**
* 根据id查询
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
//缓存穿透
// Shop shop = queryWithPassThrough(id);
//缓存击穿
//互斥锁
Shop shop = queryWithMutex(id);
if(shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
/**
* 解决缓存穿透
* @param id
* @return
*/
public Shop queryWithPassThrough(Long id){
//1.从Redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.若存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//判断命中的是否为空值
if(shopJson != null){
return null;
}
//4.若不存在根据id查询数据库
Shop shop = getById(id);
//5.若不存在返回错误
if(shop == null){
//将空值写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id,"",30,TimeUnit.MINUTES);
return null;
}
//6.若存在返回数据并将其存入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),2, TimeUnit.MINUTES);
return shop;
}
/**
* 互斥锁解决缓存击穿
* @param id
* @return
*/
public Shop queryWithMutex(Long id){
//1.从Redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.若存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//判断命中的是否为空值
if(shopJson != null){
return null;
}
//实现缓存重建
//1)获取互斥锁
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
//2)判断是否获取成功
if(!isLock){
//3)失败则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//4.若不存在根据id查询数据库
shop = getById(id);
//5.若不存在返回错误
if(shop == null){
//将空值写入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id,"",30,TimeUnit.MINUTES);
return null;
}
//6.若存在返回数据并将其存入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),2, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//释放互斥锁
unLock(lockKey);
}
//返回
return shop;
}
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
2.11利用逻辑过期解决缓存击穿问题

java
/**
* 逻辑过期解决缓存击穿
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id){
//1.从Redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
//2.判断是否存在
if(StrUtil.isBlank(shopJson)){
//3.若不存在,直接返回
return null;
}
//4.命中,把JSON反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
//需要先转成JSONObject再反序列化,否则可能无法正确映射Shop的字段
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//5.1未过期直接返回信息
return shop;
}
//5.2过期 缓冲重建
//5.2.1判断是否获取锁成功
String lockKey = "lock:shop:" + id;
boolean isLock = tryLock(lockKey);
if(isLock){
//5.2.2成功,开启独立线程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存
this.saveShopToCache(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unLock(lockKey);
}
});
}
//5.2.3失败,返回过期的店铺信息
//6.若存在返回数据并将其存入Redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),2, TimeUnit.MINUTES);
//返回
return shop;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 将数据保存到缓存中
* @param id 商铺id
* @param expireSeconds 逻辑过期时间
*/
public void saveShopToCache(Long id, Long expireSeconds) {
// 从数据库中查询店铺数据
Shop shop = this.getById(id);
// 封装逻辑过期数据
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 将逻辑过期数据存入Redis中
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(redisData));
}
