前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >玩转 Spring Boot 应用篇(引入Redis解决店铺高并发读的问题)

玩转 Spring Boot 应用篇(引入Redis解决店铺高并发读的问题)

作者头像
一猿小讲
发布2022-05-31 09:44:54
4631
发布2022-05-31 09:44:54
举报
文章被收录于专栏:一猿小讲一猿小讲

0.

0.0. 历史文章整理

玩转 Spring Boot 入门篇

玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)

玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)

玩转 Spring Boot 集成篇(Redis)

玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)

玩转 Spring Boot 集成篇(RabbitMQ)

玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)

玩转 Spring Boot 集成篇(任务动态管理代码篇)

玩转 Spring Boot 集成篇(定时任务框架Quartz)

玩转 Spring Boot 原理篇(源码环境搭建)

玩转 Spring Boot 原理篇(核心注解知多少)

玩转 Spring Boot 原理篇(自动装配前凑之自定义Starter)

玩转 Spring Boot 原理篇(自动装配源码剖析)

玩转 Spring Boot 原理篇(启动机制源码剖析)

玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)

玩转 Spring Boot 应用篇(搭建菜菜的店铺)

玩转 Spring Boot 应用篇(解决菜菜店铺商品超卖问题)

0.1. 回顾(菜菜的店铺目前存在的问题)

为了大家能够熟练应用 Spring Boot 相关技术,前几天菜菜同学基于 Spring Boot 快速搭建了一个商品售卖网站(V1),然后一起演示了商品超卖问题(V2),并对其进行分析,引入了悲观锁、乐观锁、可重入锁来解决商品超卖的问题,并借机提了提 CAS 的概念,以及 CAS 带来的 ABA 问题的解决方案。

菜菜的店铺技术实现很简单,基于 MySQL 进行增删改查而已,而此时的架构在面对高并发查询商品列表的情况下,势必会对数据库带来一定的查询压力,况且数据库操作是一个对磁盘的操作过程,性能上会存在一定的问题,悲观一点就是当有大量的并发读写操作时,会使系统服务或者数据库出现故障或者宕机。

那么,该如何环节数据库的压力,而且提升性能呢?是时候引入 Redis 啦,直面内存操作性能会高不少。

1. 菜菜的店铺技术升级:实现集成 Redis

有关 Spring Boot 集成 Redis 的详细操作步骤,可以参考历史文章《玩转 Spring Boot 集成篇(Redis)》,本次只是集成 Redis 来解决商品缓存的问题。

  • 引入依赖
代码语言:javascript
复制
<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 配置
代码语言:javascript
复制
### 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 从缓存中获取商品列表的方法实现
代码语言:javascript
复制
@Override
public 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
复制
@Controller
public 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 查询来解决数据库高并发读的问题。

但是,此时的架构,依然还是架不住粉丝的购买热情呀,会导致瞬间过高的请求进行数数据库创建商品购买记录,而且是瞬间的,何解?所以还需要想点办法改进改进,且听下次分享。

不愿迈出前行的脚步,就无法到达最美的远方;不肯跳出眼前的安逸,就无法感受生活的多彩。

参考资料:

https://spring.io/

https://start.spring.io/

https://spring.io/projects/spring-boot

https://github.com/spring-projects/spring-boot

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/

https://stackoverflow.com/questions/tagged/spring-boot

《Spring Boot实战》《深入浅出Spring Boot 2.x》

《一步一步学Spring Boot:微服务项目实战(第二版)》

《Spring Boot揭秘:快速构建微服务体系》

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-04-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一猿小讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档