缓存分页(转)

构建高性能ASP.NET站点之三 细节决定成败

 

  前言:曾经就因为一个小小的疏忽,从而导致了服务器崩溃了,后来才发现:原来就是因为一个循环而导致的,所以,对“注意细节“这一说法是深有感触。

 

  系列文章连接:     

  构建高性能ASP.NET站点 开篇

  构建高性能ASP.NET站点之一 剖析页面的处理过程(前端)

  构建高性能ASP.NET站点之二 优化HTTP请求(前端)

 

本篇的议题如下:

问题的描述

细节的重要性

 

  问题的描述

 

  首先,描述一下故事的背景:(希望大家耐心的故事读完)

  在网站中,网页中的分页控件每次显示10条数据,每次点击下一页,就再次去取下一个10条数据。至于分页的方法怎样做,方法有很多,相信这点大家都知道。

 

  过程是这样的:在用户请求数据的时候(考虑到了用户的操作和网站的访问量)我会第一次取出500条数据,然后把数据放在缓存中,也就是说,我取出了50页的数据,放在缓存中,这样如果,以后用户请求第一页到第49页的时候,就直接从缓存中拿数据。

  如下图:

  

  第一个数据块:

 

  采用键值对的形式:字典保存

 

 

  如果用户请求到了49页以后,那么就再次从数据库中取出下一个数据块(包含5011000数据),然后,现在内存中就有了1000条数据。

 

  至于缓存多久,数据什么失效,失效后怎么做,这里暂不谈论。(网站在这种缓存策略下运行的很好)。 

  代码如下:

 

List < Product >  products = GetDataFromCacheOrDatabase(condition,pageIndex,count….);

 

 

  代码的意思很清楚,从缓存中拿数据,如果缓存中没有对应的数据,那么就先从数据库中拿500条数据,然后放在缓存中,最后返回10条数据。

 

  后来,因为某些功能的需要,需要返回当前页的前6页数据和后6页的数据,例如:如果当前页是第12页,那么就要返回12页之前6Product(也就是第6,7,8,9,10,11页的数据),和第12页后的页的Product(第13,14,15,16,17,18页的数据)。 

  如下:

  

 

 

  当然,如果当前页是第5页,那么就把之前所有5页的数据都返回,另外再加上第5页之后的6页数据。

 

  这里就可能涉及到跨块获取数据,如:

  如果当前页是第48页的时候,那么返回前6页数据是没有什么问题的,那么后6页的数据就不足了,因为49,40也得数据可以从缓存的数据块中取到,至于51,52,53,54页的数据,就需要再次从数据库中读取,然后再次缓存(如果事先没有被缓存)。

 

 

  最后在缓存中的数据如下:

 

   

  然后调用方法:(伪码)

 

  

List < Product >  products = GetDataFromCacheOrDatabase(condition, 42 126 ….); 

 

 

  上面传入的是从第42页开始的数据,也就是第48页的前6页和后6页的数据。

  这个方法的内部实现是这样的:

    1.    首先从第一个数据块中取出42页到50页的数据

    取出数据后保存在一个List<Product> firstProductList;

    2.    从第二个数据块中取出从51页到54页(如果第二个数据块在缓存不存在,就去数据库中取501-1000条,然后再放在缓存的第二个数据块中)。

    保存在第二个List<Product> secondProductList

    3. 然后把两个list合并,返回结果。例如

           secondProductList.Foreach(u=>firstProductList.Add(u));

 

  基本的实现就是这样,看起来还行,也比较的合理,但是就是因为这个操作,从而导致服务器内存溢出。

  大家想想看是什么原因。

 

  细节的重要性

 

  其实缓存的数据不是很多,是不足以让服务器内存溢出的,但是服务器还是出现了out of memory的异常。之前一直跑的很好,就是改了代码之后才出现问题的。

 

  其实这就是由于一个最基本的错误产生的:引用类型。

  下面就来分析下:

  首先是从第一个数据块中取出数据,然后用

    List<Product> firstProductList 引用指向取出的数据

  然后从第二个数据块中取出数据,用

    List<Product> secondProductList指向数据的引用

 

  如下图

 

 

  在第三步中采用

 

    secondProductList.Foreach(u=>firstProductList.Add(u));

  把secondProductList中的数据加入到firstProductList中,就因为是引用类型,其实实际操作的结果是:不断的在改变第一个数据块中的数据,使得第一个数据块中的数据逐渐的变多。

  现在当前页是48页,采用上面的操作,致使第一个数据块中的数据增加了60条,

  如果用户再次翻页,到了49页,那么第一个数据块中的数据又增多了60

  依此类推,最后导致了服务器内存的不足,致使服务器崩溃了。原本的“功臣”----缓存却成为了罪魁祸首。

 

  其实这个问题的解决,只要改变一点点的代码就行了:

    List<Product> firstProductList

    List<Product> secondProductList

  然后

  List<Product> resultProductList=new List<Product>();然后分别把firstProductListsecondProductList遍历,加入到resultProductList就行了。

就这么简单。

 

  一个小的细节,导致了大的问题。 

  

  不要忽视每一个细节。不要做没有比较的循环等操作,一定要审视代码,重构代码。 

  今天就写到了这里,啰嗦了这么多,希望对大家有一点点的帮助。

  如有不正确,或者认为不妥的地方,希望大家斧正!而且留下高见!谢谢!

 

版权为小洋和博客园所有,欢迎转载,转载请标明出处给作者。

   http://www.cnblogs.com/yanyangtian

 

 

 
<think>好的,我现在需要帮助用户解决在Spring Boot中如何实现缓存分页功能的结合。用户还要求最佳实践和代码示例。首先,我得回忆一下Spring Boot的缓存机制和分页的实现方式,然后思考如何将两者结合起来。 首先,缓存通常用于存储经常访问的数据,减少数据库压力。分页则是处理大量数据时的常见需求,将数据分成多个页面显示。结合这两者,用户可能希望缓存分页后的结果,这样在用户请求相同页码时可以直接从缓存获取,而不必每次都查询数据库。 接下来,我需要考虑几个问题: 1. **缓存分页数据的键设计**:不同的分页请求(如不同页码、不同排序条件)会产生不同的数据,因此缓存键必须包含这些参数,比如页码、每页大小、排序字段等。例如,使用`page::1::size::10`这样的格式作为键。 2. **数据更新时的缓存处理**:当数据发生变化时,所有相关的分页缓存可能都需要清除或更新,否则用户可能看到过时的数据。例如,新增一条数据后,原来的第一页可能不再准确,需要清除所有以`page::*`开头的缓存。 3. **分页查询的缓存策略**:确定何时应该缓存分页结果。通常,对于读多写少的数据缓存分页结果更有效。对于频繁变动的数据,可能需要更谨慎的缓存策略,比如较短的过期时间。 然后,参考用户提供的引用内容,特别是引用[3]提到的缓存穿透、雪崩、击穿的解决方案,以及引用[5]中的分页实现代码。用户之前的问题可能涉及这些方面,需要综合考虑。 例如,引用[5]中使用了PageHelper和PageInfo来实现分页,而引用[3]提到了使用Redis进行缓存,并处理缓存异常情况。结合这些,最佳实践可能是在Service层进行分页查询,并将结果缓存到Redis中,同时在数据更新时清除相关缓存。 接下来,代码示例部分需要展示如何整合缓存分页。比如,在查询方法上添加@Cacheable注解,并指定动态生成的缓存键。在更新数据的方法上使用@CacheEvict来清除缓存。 需要注意的是,分页查询的结果可能依赖于不同的参数,如页码、每页大小、排序方式,这些都需要包含在缓存键中,否则会导致不同查询结果被错误地缓存。此外,分页数据的总页数和总条目数也需要考虑是否需要缓存,或者是否每次查询都需要重新计算。 可能的问题包括:如何动态生成缓存键,如何处理分页参数的变化,以及如何批量清除相关缓存。例如,当数据新增时,所有页码的缓存都可能失效,这时候可能需要使用通配符来清除所有相关的缓存键,但Redis的keys命令在生产环境中不推荐使用,可以考虑使用Redis的SCAN命令或者维护一个缓存键的集合。 另外,考虑到缓存击穿的问题,当缓存过期时,大量请求同时到达数据库,可以使用互斥锁或者逻辑过期的方法来处理。引用[3]中提到了这两种方法,可以应用到分页缓存中,比如在缓存未命中时,使用锁机制确保只有一个线程去数据库加载数据,其他线程等待。 总结步骤: 1. 在Service层实现分页查询,使用PageHelper进行分页。 2. 使用@Cacheable注解缓存分页结果,缓存键包含分页参数。 3.数据更新的方法上使用@CacheEvict清除所有相关的分页缓存。 4. 处理缓存异常情况,如穿透、击穿、雪崩,采用合适的策略。 5. 提供代码示例,展示如何配置缓存分页,以及如何注解方法。 需要验证缓存键的设计是否正确,确保不同的分页参数生成不同的键。例如,页码和每页大小作为键的一部分。同时,清除缓存时可能需要模式匹配,如使用`allPages*`来删除所有相关缓存。 最后,回答用户的问题时,需要结构清晰,分步骤说明,并提供代码示例。同时,生成相关问题,帮助用户进一步了解缓存分页的结合使用。</think>在Spring Boot中实现缓存分页功能的结合,需要综合考虑分页参数动态性和缓存数据一致性。以下是具体实现方案: --- ### 一、实现思路 1. **缓存键设计**:以分页参数(页码、每页大小、排序条件)作为缓存键组成部分,例如:`page::1::size::10` 2. **缓存更新策略**:数据变更时清除相关分页缓存,避免脏读 3. **性能优化**:结合Redis实现分布式缓存,使用逻辑过期解决缓存击穿问题 --- ### 二、代码实现示例 #### 1. 添加依赖 ```xml <!-- Spring Data Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- PageHelper --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.6</version> </dependency> ``` #### 2. Service层实现 ```java @Service @CacheConfig(cacheNames = "productPage") public class ProductServiceImpl implements ProductService { @Autowired private ProductMapper productMapper; @Cacheable(key = "'page::' + #pageNum + '::size::' + #pageSize") @Override public PageInfo<Product> getProducts(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<Product> list = productMapper.selectAll(); return new PageInfo<>(list); } @CacheEvict(allEntries = true) @Override public void addProduct(Product product) { productMapper.insert(product); } } ``` #### 3. 缓存配置类 ```java @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } } ``` --- ### 三、最佳实践 1. **动态缓存键**:使用SpEL表达式组合分页参数生成唯一键 ```java @Cacheable(key = "T(String).format('page:%d:size:%d:sort:%s', #pageNum, #pageSize, #sortField)") ``` 2. **批量清除策略**:使用`@CacheEvict(allEntries = true)`清空整个缓存空间 3. **防缓存穿透**:对空结果设置短时间缓存 ```java if (pageList.isEmpty()) { redisTemplate.opsForValue().set(cacheKey, Collections.emptyList(), 30, TimeUnit.SECONDS); } ``` 4. **逻辑过期方案**:在缓存值中存储过期时间字段,异步刷新缓存[^3] --- ### 四、注意事项 1. **缓存雪崩防护**:对不同的分页结果设置随机过期时间 2. **数据一致性**:使用`@Transactional`保证数据库操作与缓存操作的原子性 3. **性能监控**:通过`spring-boot-starter-actuator`监控缓存命中率 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值