正式观看本文之前,设想一个问题,高并发情况下,首页列表数据怎么做?
类似淘宝首页,这些商品是从数据库中查出来的吗?答案肯定不是,在高并发的情况下,数据库是扛不住的,那么我们要怎么去扛住C端大并发量呢,这块我们可以借助Redis,我们知道Redis是一个基于内存的NoSQL数据库。学过操作系统我们都知道,内存要比磁盘的效率大的多,那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中。
第二次访问的时候。就直接访问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,整篇的案例整合 我们就到这里,觉得博主写的不错的,记得给个三连哦!!!文章来源:https://www.toymoban.com/news/detail-491649.html
文章来源地址https://www.toymoban.com/news/detail-491649.html
到了这里,关于【案例实战】SpringBoot整合Redis实现缓存分页数据查询的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!