【案例实战】SpringBoot整合Redis实现缓存分页数据查询

这篇具有很好参考价值的文章主要介绍了【案例实战】SpringBoot整合Redis实现缓存分页数据查询。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

正式观看本文之前,设想一个问题,高并发情况下,首页列表数据怎么做?

【案例实战】SpringBoot整合Redis实现缓存分页数据查询

类似淘宝首页,这些商品是从数据库中查出来的吗?答案肯定不是,在高并发的情况下,数据库是扛不住的,那么我们要怎么去扛住C端大并发量呢,这块我们可以借助Redis,我们知道Redis是一个基于内存的NoSQL数据库。学过操作系统我们都知道,内存要比磁盘的效率大的多,那Redis就是基于内存的,而数据库是基于磁盘的。

还有类似天猫聚划算商品类表。
【案例实战】SpringBoot整合Redis实现缓存分页数据查询

我们现在知道要用Redis去做首页数据的分页,那么我们应该用Redis的那种数据结构来做呢。

Redis有5种基本的数据结构,我们这里用list类型做分页。

在 Redis 中,List(列表)类型是按照元素的插入顺序排序的字符串列表。你可以在列表的头部(左边)或者尾部(右部)添加新的元素。

ok,那么接下来我们就通过一个案例实操一下,首页热点数据怎么放到Redis中去查询。

SpringBoot整合RedisTemplate这里就不做过多介绍啦,大家可以网上找篇博文 整合一下。

<!-- 创建SpringBoot项目加入redis的starter依赖 -->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

编写ProductService,定于数据分页方法。

public interface ProductService {

    Map<String,Object> productListPage(int current, int size) throws InterruptedException;

}

编写ProductServiceImpl实现类。

/**
 * @author lixiang
 * @date 2023/6/18 21:01
 */
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {

    private static final String PRODUCT_LIST_KEY = "product:list";

    private static final List<Product> PRODUCT_LIST;

    //模拟从数据库中查出来的数据
    static {
        PRODUCT_LIST = new ArrayList<>();
        for (int i = 1; i <= 100; i++) {
            Product product = new Product();
            product.setId(UUID.randomUUID().toString().replace("-", ""));
            product.setName("商品名称:" + i);
            product.setDesc("商品描述:" + i);
            product.setPrice(new BigDecimal(i));
            product.setInventory(2);
            PRODUCT_LIST.add(product);
        }
    }

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public Map<String, Object> productListPage(int current, int size) throws InterruptedException {

        //从缓存中拿到分页数据
        List<Product> productList = getProductListByRedis(current, size);

        if (productList == null || productList.size() == 0) {
            log.info("当前缓存中无分页数据,当前页:" + current + ",页大小:" + size);
            //从数据库中拿到分页数据
            productList = getProductListByDataSource(current, size);
        }
        Map<String, Object> resultMap = new HashMap<>();
        //计算当前总页数
        int totalPage = (PRODUCT_LIST.size() + size - 1) / size;
        resultMap.put("total", PRODUCT_LIST.size());
        resultMap.put("data", productList);
        resultMap.put("pages", totalPage);
        return resultMap;
    }

    private List<Product> getProductListByRedis(int current, int size) {
        log.info("从Redis取出商品信息列表,当前页:" + current + ",页大小:" + size);
        // 计算总页数
        int pages = pages(size);
        // 起始位置
        int start = current <= 0 ? 0 : (current > pages ? (pages - 1) * size : (current - 1) * size);
        // 终止位置
        int end = start+size-1;
        List<Product> list = redisTemplate.opsForList().range(PRODUCT_LIST_KEY, start, end);
        List<Product> productList = list;
        return productList;
    }

    /**
     * 获取商品信息集合
     *
     * @return
     */
    private List<Product> getProductListByDataSource(int current, int size) throws InterruptedException {
        //模拟从DB查询需要300ms
        Thread.sleep(300);
        log.info("从数据库取出商品信息列表,当前页:" + current + ",页大小:" + size);
        // 计算总页数
        int pages = pages(size);
        // 起始位置
        int start = current <= 0 ? 0 : (current > pages ? (pages - 1) * size : (current - 1) * size);
        //数据缓存到redis中
        redisTemplate.opsForList().rightPushAll(PRODUCT_LIST_KEY, PRODUCT_LIST);
        //设置当前key过期时间为1个小时
        redisTemplate.expire(PRODUCT_LIST_KEY,1000*60*60, TimeUnit.MILLISECONDS);
        return PRODUCT_LIST.stream().skip(start).limit(size).collect(Collectors.toList());
    }

    /**
     *  获取总页数
     * @param size
     * @return
     */
    private Integer pages(int size){
        int pages = PRODUCT_LIST.size() % size == 0 ? PRODUCT_LIST.size() / size : PRODUCT_LIST.size() / size + 1;
        return pages;
    }
}

ok,然后编写controller,进行测试。

@RestController
@RequestMapping("/api/v1/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/page")
    public Map<String,Object> page(@RequestParam("current") int current,@RequestParam("size") int size){
        Map<String, Object> stringObjectMap;
        try {
            stringObjectMap = productService.productListPage(current, size);
        } catch (InterruptedException e) {
            stringObjectMap = new HashMap<>();
        }
        return stringObjectMap;
    }
}

当第一次访问的时候,先去Redis中查询,发现没有,然后就去查DB,将要缓存的数据页放到Redis中。
【案例实战】SpringBoot整合Redis实现缓存分页数据查询
【案例实战】SpringBoot整合Redis实现缓存分页数据查询
【案例实战】SpringBoot整合Redis实现缓存分页数据查询

第二次访问的时候。就直接访问Redis啦

【案例实战】SpringBoot整合Redis实现缓存分页数据查询
【案例实战】SpringBoot整合Redis实现缓存分页数据查询
【案例实战】SpringBoot整合Redis实现缓存分页数据查询

通过Redis和DB查询的对比,我们发现从Redis中拿出来只用了18ms,从公DB中需要300ms,由此可见Redis的一个强大之处。

那么我们观察一下查询逻辑,会不会有什么问题。

    public Map<String, Object> productListPage(int current, int size) throws InterruptedException {

        //从缓存中拿到分页数据
        List<Product> productList = getProductListByRedis(current, size);

        if (productList == null || productList.size() == 0) {
            log.info("当前缓存中无分页数据,当前页:" + current + ",页大小:" + size);
            //从数据库中拿到分页数据
            productList = getProductListByDataSource(current, size);
        }
    }

设想,假如某一时刻,Redis中的缓存失效啦,大量的请求,全部查到DB上,也会带来一个灾难。所以这快又涉及到一个缓存击穿的问题。

解决缓存击穿

  • 方案一:永不过期
    • 提前把热点数据不设置过期时间,后台异步更新缓存。
  • 方案二:加互斥锁或队列
    • 其实我理解缓存击穿和缓存穿透差不多,所以加一个互斥锁,让一个线程正常请求数据库,其他线程等待即可(这里可以使用线程池来处理),都创建完缓存,让其他线程请求缓存即可。

在这里我们采用第一种方式,让key永远不过期。

那可能有的人会说了,这很简单啊,那我就设置一个定时任务定时的去刷新key就可以了啊。于是写出了如下的定时作业代码。

// 定时任务,每隔30分钟,从数据库中读取商品列表,存储到缓存里面
priviate static final String PRODUCT_LIST_KEY = "product:list";

@Scheduled(cron = "0 */30 * * * ?")
public void loadActivityProduct() {
  //从数据库中查询参加活动的商品列表
  List<Product> productList = productMapper.queryAcitvityProductList();
  //删除旧的
  redisTemplate.delete(PRODUCT_LIST_KEY);
  //存储新的
  redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)
}

但是,不知道大家有没发现,我们即使加了定时任务的代码也会发生缓存击穿的问题。因为删除旧的数据 和 存储新的数据两个命令非原子操作,存在时间间隔。如果改用string结构存储,可以直接覆盖旧值,则没有原子性问题,但是业务需求需要支持分页,只能用list结构。

	//就在我删除旧的key的时候,这会还没有往redis中放入,大的并发量进来导致请求都跑到了数据库上,造成缓存击穿。
	//删除旧的
  redisTemplate.delete(PRODUCT_LIST_KEY);
  //存储新的
  redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)

解决方案

  • 业务架构里面强调降级,兜底数据,那缓存击穿是不是也可以考虑这个方案,空间换时间

  • 缓存两份数据,一份是List结构(先删除,再设置新值), 一份是String结构(直接覆盖旧值)

    • 查询的时候优先查询list结构,如果没有则解析String结构成为list,进行内存分页,一般数据量不大
// 定时任务,每隔30分钟,从数据库中读取商品列表,存储到缓存里面
priviate static final String PRODUCT_LIST_KEY = "product:list";
priviate static final String PRODUCT_LIST_KEY_STR = "product:liststr";

@Scheduled(cron = "0 */30 * * * ?")
public void loadActivityProduct() {
  //从数据库中查询参加活动的商品列表
  List<Product> productList = productMapper.queryAcitvityProductList();
  
  //先缓存一份String类型的数据,直接set,如果要分页则解析成list再返回
  redis.opsForValue.set(PRODUCT_LIST_KEY_STR, JSON.toString(productList))
  
  //删除旧的
  redisTemplate.delete(PRODUCT_LIST_KEY);
  //存储新的
  redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)
}

查询的时候,先去查list结构,list结构如果没有数据,则查String类型的数据。

priviate static final String PRODUCT_LIST_KEY = "product:list";
priviate static final String PRODUCT_LIST_KEY_STR = "product:liststr";

// 将商品列表从 Redis 缓存中读取
public List<Product> getProductListFromCache(int begin, int end) {
    List<Product> list = new ArrayList();

    //从缓存里分页获取
    list = redisTemplate.opsForList().range(PRODUCT_LIST_KEY, begin,end)
    if (productListStr != null) {
      return list;
    } else {
        // 缓存A中不存在商品列表,则从缓存B读取
        String productStrList = redis.opsForValue.get(PRODUCT_LIST_KEY_STR);
        // 缓存中存在商品列表,将 JSON 字符串转换为对象
        List<Product> productList = JSON.parseArray(productStrList, Product.class);
        //分页计算
        list = CommonUtil.pageList(productList,begin, end);
       return list;
    }
}

OK,整篇的案例整合 我们就到这里,觉得博主写的不错的,记得给个三连哦!!!

【案例实战】SpringBoot整合Redis实现缓存分页数据查询文章来源地址https://www.toymoban.com/news/detail-491649.html

到了这里,关于【案例实战】SpringBoot整合Redis实现缓存分页数据查询的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SpringBoot 整合 Redis 缓存

    Spring Boot提供了对Spring Cache抽象的支持,可以很容易地与Redis集成。 在pom.xml文件中添加Spring Boot Starter Redis依赖: 在application.properties或application.yml中配置Redis连接信息: 在Spring Boot应用的主类(通常是带有@SpringBootApplication注解的类)上添加@EnableCaching注解,启用缓存支持: 在

    2024年01月16日
    浏览(53)
  • Redis实战案例4-缓存更新策略

    缓存中的数据一致性问题(数据库更新数据,而Redis存的是旧数据) 内存淘汰策略:当内存很充足时,很长时间无法淘汰数据,所以很难控制淘汰,一致性差; 超时剔除:取决于TTL大小,可以达到控制目的,但是在TTL时间内也可能存在数据库更新从而Redis中变成旧数据; 主动

    2024年02月10日
    浏览(42)
  • SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】

    上一篇实现了单体应用下如何上锁,这一篇主要说明如何在分布式场景下上锁 上一篇地址:加锁 需要注意的点是: 在上锁和释放锁的过程中要保证 原子性操作 核心是上锁和解锁的过程 关于解锁使用脚本参考:SET key value [EX seconds] [PX milliseconds] [NX|XX] 3.1 一个服务按照多个端口同时

    2023年04月10日
    浏览(52)
  • Springboot整合Redis集群实战详解

    Springboot 整合 Redis 集群,实现 Redis 分布式方案详解 前言 准备工作 Redis 集群环境搭建 Redis 集群故障转移(主从复制) Redis 集群扩展与收缩节点 Redis 集群扩展节点(添加节点) Redis 集群收缩节点(移除节点) Springboot 整合 Redis 集群 Redis Sentinel安装与部署,实现redis的高可用

    2024年02月09日
    浏览(42)
  • mall整合Redis实现缓存功能

    本文主要讲解mall整合Redis的过程,以短信验证码的存储验证为例。 Redis是用C语言开发的一个高性能键值对数据库,可用于数据缓存,主要用于处理大量数据的高访问负载。 下载Redis,下载地址:github.com/MicrosoftAr… 下载完后解压到指定目录 在当前地址栏输入cmd后,执行redis的启

    2024年01月19日
    浏览(43)
  • SpringBoot整合ElasticSearch实现分页查询

    本文使用SpringBoot整合ElasticSearch实现分页查询 还是继续使用spring-boot-starter-data-elasticsearch来实现分页查询操作 数据准备 使用ElasticsearchRestTemplate来实现 程序结果 使用ElasticsearchOperations来实现 程序结果 本文记录了SpringBoot整合ElasticSearch来实现分页查询的两种方式

    2024年01月25日
    浏览(52)
  • 【案例实战】SpringBoot整合阿里云文件上传OSS

    1.需求背景 C端业务用户头像上传 海量图片音频、视频存储 用户行为日志存储 (1)阿里云OSS介绍 对象存储OSS(Object Storage Service)是阿里云提供的海量、安全、低成本、高持久的云存储服务。其数据设计持久性不低于99.9999999999%(12个9),服务设计可用性不低于99.995%。 OSS具

    2024年02月06日
    浏览(55)
  • java springboot整合MyBatis实现分页查询以及带条件的分页查询

    之前的文章 java springboot整合MyBatis做数据库查询操作操作了springboot整合MyBatis,然后简单做了个按id查询的操作 那么 我们按上文搭建起的环境继续 我们直接在staffDao接口中声明一个分页函数 这里 我们直接在 sql语句中写limit 分页逻辑 参数是方法接收的 这个函数接收两个参数

    2024年02月10日
    浏览(46)
  • SpringBoot 整合ElasticSearch实现模糊查询,批量CRUD,排序,分页,高亮

    准备一个空的SpringBoot项目 写入依赖 注意你的SpringBoot和你的es版本,一定要对应,如果不知道的可以查看这篇文章:https://blog.csdn.net/u014641168/article/details/130386872 我的版本是2.2.6,所以用的ES版本是 6.8.12,安装es请看这篇文章:https://blog.csdn.net/u014641168/article/details/130622430 查看

    2024年02月08日
    浏览(53)
  • RabbitMq整合Springboot超全实战案例+图文演示+源码自取

    目录 介绍 简单整合 简单模式 定义 代码示例  work模式 定义 代码示例 pubsub模式 定义 代码示例 routing模式  定义 代码示例  top模式 定义 代码 下单付款加积分示例 介绍 代码  可靠性投递示例 介绍 代码 交换机投递确认回调  队列投递确认回调  ​延迟消息场景示例 介绍 代

    2024年02月03日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包