当前位置: 首页 > news >正文

JAVA-实战8 Redis实战项目—雷神点评(2)商户查询缓存

素敵だった未来に繋がった夢

雷神点评——商户查询缓存

由于商店数量较少,所以多次查询返回结果相同,且实际操作中往往查询操作占大多数,考虑使用缓存优化查询操作,避免多次访问数据库

缓存概念

缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高

缓存优缺点如下:

优点:

降低后端负载
提高读写效率,降低响应时间

缺点:

数据一致性成本
代码维护成本
运维成本

添加Redis缓存

Redis缓存模型作用示意如下:
image

具体流程示意图如下:
image

根据ID查询商店

控制层方法

@RestController
@RequestMapping("/shop")
public class ShopController {@Autowiredprivate ShopService shopService;@GetMapping(value="/{id}")public ResultData GetShopByID(@PathVariable("id") Long ShopId) {ShopData Shop = shopService.SelectShopById(ShopId);if(Shop==null) return ResultData.error("Shop dose not exist!");else return ResultData.success(Shop);}
}

服务层方法

@Service
public class ShopServiceImpl implements ShopService {@Autowiredprivate ShopMapper shopMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic ShopData SelectShopById(Long ShopId) {// 查询Redis,查看缓存中是否存在,有则直接返回String ShopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+ShopId);if(!StringUtil.isNullOrEmpty(ShopJson)) {return JSONUtil.toBean(ShopJson,ShopData.class);}// 缓存中没有,则查询数据库,并更新缓存LambdaQueryWrapper<ShopData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(ShopData::getId,ShopId);ShopData Shop = shopMapper.selectOne(lambdaWrapper);if(Shop!=null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,JSONUtil.toJsonStr(Shop));}return Shop;}
}

常量封装工具类:

public class RedisConstants {public static final Long CACHE_NULL_TTL = 2L;public static final String CACHE_NULL_VALUE = "";public static final Long CACHE_SHOP_TTL = 30L;public static final String CACHE_SHOP_KEY = "cache:shop:";public static final String LOCK_SHOP_KEY = "lock:shop:";public static final Long LOCK_SHOP_TTL = 10L;
}

测试效果如下:

第一次查询,走数据库查询并且缓存Redis更新
image
image

第二次同样的查询,没有走数据库
image

缓存更新策略

Redis如果缓存过量的话就需要及时汰换,这就是缓存更新策略
image

业务场景:

低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

读操作

缓存命中直接返回
缓存未命中则查询数据并写入缓存

写操作

先写数据库再删除缓存
确保数据库操作和缓存操作的原子性

根据ID插入或更新餐厅

// 控制层方法
@PutMapping
public ResultData UpdateShop(@RequestBody ShopData NewShop) {shopService.UpdateShop(NewShop);return ResultData.success();
}// 服务层方法
@Override
@Transactional // 添加业务注解保证操作原子性
public void UpdateShop(ShopData NewShop) {shopMapper.updateById(NewShop);stringRedisTemplate.delete(CACHE_SHOP_KEY+NewShop.getId());return;
}

缓存穿透和解决方法

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力

常见的解决方案有两种:

缓存空对象

优点:实现简单易于维护
缺点:①额外的内存消耗 ②可能造成短期的数据不一致

布隆过滤器

优点:内存占用较少,没有多余Key
缺点:①实现复杂 ②存在误判可能

除此之外,还有以下方法应对缓存穿透:

增强id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流

这里采用缓存空对象方法来应对缓存穿透
image

代码如下:

@Override
public ShopData SelectShopById(Long ShopId) {String ShopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+ShopId);if(!StringUtil.isNullOrEmpty(ShopJson)) {return JSONUtil.toBean(ShopJson,ShopData.class);}if (ShopJson != null && ShopJson.equals(CACHE_NULL_VALUE)) {// 判断是否是缓存中的空对象return null;}LambdaQueryWrapper<ShopData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(ShopData::getId,ShopId);ShopData Shop = shopMapper.selectOne(lambdaWrapper);// 如果为空则将空对象插入Redisif(Shop==null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,CACHE_NULL_VALUE,CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,JSONUtil.toJsonStr(Shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);return Shop;
}

测试效果如下:
image
image

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

给不同的Key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:

互斥锁:当缓存失效时,只有一个线程能获取到分布式锁去查询数据库并重建缓存,其他未获取到锁的线程则等待一段时间后重试,或直接返回降级数据。

①查询缓存,若命中则直接返回。
②若未命中,尝试获取分布式锁(如使用 Redis 的 SETNX 命令,需设置过期时间防止死锁)。
③双重检查‌:获取锁成功后,再次检查缓存是否已有数据(防止其他线程在等待期间已重建完成)。若有则直接返回。
④若仍无数据,则查询数据库,将结果写入缓存,并释放锁。
⑤未获取到锁的线程可短暂休眠后重试查询缓存,或快速失败返回提示。

逻辑过期:缓存中不设置物理过期时间(TTL),而是在数据内部包含一个逻辑过期时间字段。

①查询缓存时,如果缓存中不存在数据(极少见,如首次加载),则直接查库并写入缓存(此时可加锁保证唯一性)。
②如果缓存存在,查看逻辑过期时间是否到期
③若未到期,直接返回数据。
④若已到期,不立即阻塞当前请求,而是返回旧数据(或空数据),同时启动一个异步线程去查询数据库并更新缓存。
⑤后续请求会逐渐获取到新数据。

流程示意图如下:
image

优缺点对比如下:
image

互斥锁应对缓存穿透

这里基于互斥锁方法来应对缓存穿透,流程示意图如下:
image

使用Redis中的setnx操作来模拟互斥锁

// 加锁操作
private boolean TryLock(String Key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(Key,"1",LOCK_SHOP_TTL,TimeUnit.MINUTES);return BooleanUtil.isTrue(flag);
}
// 去锁操作
private void Unlock(String Key) {stringRedisTemplate.delete(Key);
}private ShopData SelectShopByIdWithMutex(Long ShopId) throws InterruptedException {String ShopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+ShopId);if(!StringUtil.isNullOrEmpty(ShopJson)) {return JSONUtil.toBean(ShopJson,ShopData.class);}if (ShopJson != null && ShopJson.equals(CACHE_NULL_VALUE)) {return null;}// 设置加锁的键String LockKey = LOCK_SHOP_KEY+ShopId;ShopData Shop = null;try {boolean IsLock = TryLock(LockKey);if(!IsLock) {// 模拟循环等待Thread.sleep(50);return SelectShopByIdWithMutex(ShopId);}LambdaQueryWrapper<ShopData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(ShopData::getId,ShopId);Shop = shopMapper.selectOne(lambdaWrapper);Thread.sleep(200); // 模拟重建延迟if(Shop==null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,CACHE_NULL_VALUE,CACHE_NULL_TTL, TimeUnit.MINUTES);}stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,JSONUtil.toJsonStr(Shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (Exception e) {throw new RuntimeException(e);} finally {// 保证无论如何都能去锁Unlock(LockKey);}return Shop;
}

逻辑过期应对缓存穿透

由于查询缓存时缓存中不存在数据情况极为少见,所以这里设定缓存中已存在数据(讨论情况为内容是否为空以及时间是否过期)

实现流程图如下:
image

实现代码如下:

// 用于封装过期时间的RedisData
public class RedisData {private LocalDateTime expireTime;private Object data;
}
// 服务层方法
@Service
public class ShopServiceImpl implements ShopService {@Autowiredprivate ShopMapper shopMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 设置线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);// 从数据库中读取最新信息并更新过期时间private void SaveShopToRedis(Long ShopId,Long ExpireSeconds) {LambdaQueryWrapper<ShopData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(ShopData::getId,ShopId);ShopData Shop = shopMapper.selectOne(lambdaWrapper);RedisData redisData = new RedisData();redisData.setData(Shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(ExpireSeconds));stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,JSONUtil.toJsonStr(redisData));}private ShopData SelectShopByIdWithLogicExpire(Long ShopId) {String ShopKey = CACHE_SHOP_KEY+ShopId;String ShopJson = stringRedisTemplate.opsForValue().get(ShopKey);// 查看是否为空,如果为空表示不存在直接返回if(StrUtil.isBlank(ShopJson)) {return null;}//从Redis缓存中获取Shop数据,由于RedisData中封装数据类型为Object,需要进行格式转化RedisData redisData = JSONUtil.toBean(ShopJson,RedisData.class);ShopData Shop = JSONUtil.toBean(JSONUtil.toJsonStr(redisData.getData()), ShopData.class);LocalDateTime ExpireTime = redisData.getExpireTime();// 查看是否过期,如果没过期直接返回if(ExpireTime.isAfter(LocalDateTime.now())) {return Shop;}// 如果过期的话那么查看是否已加锁,获取锁成功则开启异步线程开始更新缓存数据String LockKey = LOCK_SHOP_KEY + ShopId;boolean IsLock = TryLock(LockKey);if(IsLock) {CACHE_REBUILD_EXECUTOR.submit(() -> {try{this.SaveShopToRedis(ShopId,20L);} catch (Exception e) {throw new RuntimeException(e);}finally {// 无论是否成功都要去锁Unlock(LockKey);}});}return Shop;}
}
http://www.jsqmd.com/news/747311/

相关文章:

  • Betaflight敏捷开发终极指南:开源飞控项目的10个高效实践
  • 避开这些坑!AUTOSAR RTM集成时关于CPU负载计算的几个关键点
  • AutoSar项目里,OS Timer选PIT还是HRT?一个配置不当,CPU负载直接翻倍
  • 如何快速掌握Hpple:Objective-C XML/HTML解析器的完整指南
  • 自感的物质重塑与唯物主义的本体论重构 ——岐金兰论AI时代“唯心恐惧症”的终结
  • 终极验证码识别技术对决:CNN与CTC方法性能全面评测
  • 2026年工厂短视频IP拍摄代运营服务商选择指南:深圳昊客网络等行业头部机构深度评测 - 深圳昊客网络
  • Phi-mini-MoE-instruct行业方案:教育科技公司AI助教系统集成实录
  • 别再只懂TF-IDF了!Elasticsearch 8.x 默认的BM25评分算法,调参实战指南(附k1、b值详解)
  • 训练资源大揭秘:静态语料库与动态环境的完美结合
  • Faster-Whisper-GUI终极指南:3分钟实现专业级语音转文字
  • 何帆律师:二审策略助保险拒赔案全面逆转 - 测评者007
  • 前法官何帆:以双重视角构筑保险拒赔维权防线 - 测评者007
  • Html Agility Pack终极指南:如何快速解析任意HTML文档的10个技巧
  • AnythingtoRealCharacters2511保姆级教学:ComfyUI节点配置、图片预处理与后处理建议
  • 基于多源API的音乐歌词智能提取与处理系统架构解析
  • real-anime-z效果可视化:t-SNE聚类分析生成图特征空间分布图谱
  • CH341SER驱动安装指南:5分钟解决Linux串口设备识别难题
  • FOSRestBundle安全最佳实践:API认证与授权完整解决方案
  • MusicFree插件终极指南:打造你的全能免费音乐播放器
  • 终极Html Agility Pack实战指南:5种轻松解决HTML乱码的高效方法
  • RWKV7-1.5B-worldGPU算力优化:Triton 3.2内核加速线性注意力实测报告
  • 如何在Windows系统上快速安装APK应用:终极免费指南
  • 六自由度串联机械臂运动规划及跟踪动力学【附代码】
  • 如何在国服英雄联盟中免费解锁所有皮肤:R3nzSkin国服特供版终极指南
  • Library Compiler:时序弧建模与约束全解析(一)
  • 终极指南:5个可复用转录UI组件,快速构建实时语音识别界面
  • Word分节符实战:搞定复杂页码、页眉页脚独立设置的终极指南
  • 使用 minimax-pdf 技能生成专业PDF文档
  • 为什么你的遥感模型总过拟合?Python解译中被忽视的3类空间自相关陷阱及scikit-learn+spatialEco双校正方案