0.
0.1. 回顾(菜菜的店铺目前存在的问题)
为了大家能够熟练应用 Spring Boot 相关技术,前几天菜菜同学基于 Spring Boot 快速搭建了一个商品售卖网站(V1),然后一起演示了商品超卖问题(V2),并对其进行分析,引入了悲观锁、乐观锁、可重入锁来解决商品超卖的问题,并借机提了提 CAS 的概念,以及 CAS 带来的 ABA 问题的解决方案。
菜菜的店铺技术实现很简单,基于 MySQL 进行增删改查而已,而此时的架构在面对高并发查询商品列表的情况下,势必会对数据库带来一定的查询压力,况且数据库操作是一个对磁盘的操作过程,性能上会存在一定的问题,悲观一点就是当有大量的并发读写操作时,会使系统服务或者数据库出现故障或者宕机。
那么,该如何环节数据库的压力,而且提升性能呢?是时候引入 Redis 啦,直面内存操作性能会高不少。
1. 菜菜的店铺技术升级:实现集成 Redis
有关 Spring Boot 集成 Redis 的详细操作步骤,可以参考历史文章《玩转 Spring Boot 集成篇(Redis)(四)》,本次只是集成 Redis 来解决商品缓存的问题。
- 引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </exclusion> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions></dependency>
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId></dependency>
- 添加 redis 配置
### Redis 缓存配置信息# 主机名称spring.redis.host=127.0.0.1# 端口号spring.redis.port=6379# 认证密码spring.redis.password=# 连接超时时间spring.redis.timeout=500# 默认数据库spring.redis.database=0
2. 查询商品优先从 Redis 返回
- GoodsService 商品 Service 功能扩展
考虑到不对原有基于 DB 查询功能的影响,本次扩展一个缓存查询商品的功能 findAllCacheGoods,GoodsService 中添加从缓存中获取商品列表方法定义如下。
代码语言:javascript复制/**获取缓存的商品列表*/public List<Goods> findAllCacheGoods();
- GoodsServiceImpl 从缓存中获取商品列表的方法实现
@Overridepublic Collection<Goods> findAllCacheGoods() { // 从缓存中查询商品数据 Map<Integer, Goods> goodsMap = redisTemplate.opsForHash().entries(GOODS_LIST_CACHE_KEY); // 缓存的商品列表为空,则从库中加载然后放入缓存中(此操作可以前置,在商品管理时加入到缓存中) if (MapUtils.isEmpty(goodsMap)) { logger.info("商品缓存列表为空,先从库中查询商品列表信息"); // 查询库中的所有商品 List<Goods> goodsList = findAllGoods(); // 将商品 list 转换为商品 map goodsMap = convertToMap(goodsList); logger.info("从库中查询商品条数为:" goodsMap.size()); // 把商品信息缓存到 redis 中 redisTemplate.opsForHash().putAll(GOODS_LIST_CACHE_KEY, goodsMap); // 设置商品缓存数据的过期时间为 10 秒 redisTemplate.expire(GOODS_LIST_CACHE_KEY, 10000, TimeUnit.MILLISECONDS); logger.info("商品信息放入 redis 中完毕"); } // 返回商品列表 return goodsMap.values();}
/** * 商品列表转成Map<Integer,Goods> */private Map<Integer, Goods> convertToMap(Collection<Goods> goodsList) { if (CollectionUtils.isEmpty(goodsList)) { return Collections.EMPTY_MAP; }
Map<Integer, Goods> goodsMap = new HashMap<>(goodsList.size()); for (Goods goods : goodsList) { goodsMap.put(goods.getId(), goods); } return goodsMap;}
- IndexController 的修改
查询商品信息,由数据库查询 findAllGoods 直接变更为优先从缓存中查询 findAllCacheGoods,变更如下。
代码语言:javascript复制@Controllerpublic class IndexController {
@Resource private GoodsService goodsService;
@RequestMapping({"","/","/index"}) public String index(Model model) { // v1:从数据库中加载商品列表 // model.addAttribute("goodsList", goodsService.findAllGoods()); // v3:优先从 redis 中获取商品列表 model.addAttribute("goodsList", goodsService.findAllCacheGoods()); return "index"; }}
3. 菜菜店铺升级验证
- 服务端控制输出
首次查询时会把数据库的商品缓存至 redis(如上图示意),后续的商品查询都优先从 redis 中返回,此时店铺页面购买访问正常,一定程度上减轻了数据库的访问压力。
但是,仔细的同学会发现一个问题页面展示的库存与数据库的记录居然不一致。
- 页面的库存数量
- 数据库的库存记录
看到上面截图铁证如山,当用户购买成功后,店铺首先展示的库存并没有变化,依然缓存的是旧值,导致与数据库记录不一致,该咋办?
- 解决 Redis 中缓存旧值的问题
当数据库扣减库存成功后,则更新 redis 缓存的商品信息。
更新缓存中的商品信息,核心代码如下:
代码语言:javascript复制//扣减库存成功,则更新 redis 中缓存的商品信息redisTemplate.opsForHash().put(GOODS_LIST_CACHE_KEY, goodsId, goodsDao.getGoodsById(goodsId));logger.info("更新缓存中的商品信息:" goodsId "成功");
修改完毕后,重新运行菜菜的店铺,运行效果良好。
4. 例行回顾
本文主要是对菜菜的店铺中的高并发读带来的数据库查询压力进行环节,主要引入基于内存操作的 Redis 来解决商品高并发查询的问题。
感兴趣的同学,可以把商品 购买流程全部搬到 Redis Lua 中进行实现,那样查询效率(内存) 扣减库存(原子性)将不再是个头疼的问题。
通过本次集成 Redis 技术组件,架构演变如上图示意,让请求不再直接查询数据库,而是优先从 Redis 查询来解决数据库高并发读的问题。
但是,此时的架构,依然还是架不住粉丝的购买热情呀,会导致瞬间过高的请求进行数数据库创建商品购买记录,而且是瞬间的,何解?所以还需要想点办法改进改进,且听下次分享。
不愿迈出前行的脚步,就无法到达最美的远方;不肯跳出眼前的安逸,就无法感受生活的多彩。