正式觀看本文之前,設想一個問題,高并發(fā)情況下,首頁列表數(shù)據(jù)怎么做?
類似淘寶首頁,這些商品是從數(shù)據(jù)庫中查出來的嗎?答案肯定不是,在高并發(fā)的情況下,數(shù)據(jù)庫是扛不住的,那么我們要怎么去扛住C端大并發(fā)量呢,這塊我們可以借助Redis,我們知道Redis是一個基于內(nèi)存的NoSQL數(shù)據(jù)庫。學過操作系統(tǒng)我們都知道,內(nèi)存要比磁盤的效率大的多,那Redis就是基于內(nèi)存的,而數(shù)據(jù)庫是基于磁盤的。
還有類似天貓聚劃算商品類表。
我們現(xiàn)在知道要用Redis去做首頁數(shù)據(jù)的分頁,那么我們應該用Redis的那種數(shù)據(jù)結構來做呢。
Redis有5種基本的數(shù)據(jù)結構,我們這里用list類型做分頁。
在 Redis 中,List(列表)類型是按照元素的插入順序排序的字符串列表。你可以在列表的頭部(左邊)或者尾部(右部)添加新的元素。
ok,那么接下來我們就通過一個案例實操一下,首頁熱點數(shù)據(jù)怎么放到Redis中去查詢。
SpringBoot整合RedisTemplate這里就不做過多介紹啦,大家可以網(wǎng)上找篇博文 整合一下。
<!-- 創(chuàng)建SpringBoot項目加入redis的starter依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
編寫ProductService,定于數(shù)據(jù)分頁方法。
public interface ProductService {
Map<String,Object> productListPage(int current, int size) throws InterruptedException;
}
編寫ProductServiceImpl實現(xiàn)類。
/**
* @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;
//模擬從數(shù)據(jù)庫中查出來的數(shù)據(jù)
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 {
//從緩存中拿到分頁數(shù)據(jù)
List<Product> productList = getProductListByRedis(current, size);
if (productList == null || productList.size() == 0) {
log.info("當前緩存中無分頁數(shù)據(jù),當前頁:" + current + ",頁大小:" + size);
//從數(shù)據(jù)庫中拿到分頁數(shù)據(jù)
productList = getProductListByDataSource(current, size);
}
Map<String, Object> resultMap = new HashMap<>();
//計算當前總頁數(shù)
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);
// 計算總頁數(shù)
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("從數(shù)據(jù)庫取出商品信息列表,當前頁:" + current + ",頁大小:" + size);
// 計算總頁數(shù)
int pages = pages(size);
// 起始位置
int start = current <= 0 ? 0 : (current > pages ? (pages - 1) * size : (current - 1) * size);
//數(shù)據(jù)緩存到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());
}
/**
* 獲取總頁數(shù)
* @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;
}
}
當?shù)谝淮卧L問的時候,先去Redis中查詢,發(fā)現(xiàn)沒有,然后就去查DB,將要緩存的數(shù)據(jù)頁放到Redis中。
第二次訪問的時候。就直接訪問Redis啦
通過Redis和DB查詢的對比,我們發(fā)現(xiàn)從Redis中拿出來只用了18ms,從公DB中需要300ms,由此可見Redis的一個強大之處。
那么我們觀察一下查詢邏輯,會不會有什么問題。
public Map<String, Object> productListPage(int current, int size) throws InterruptedException {
//從緩存中拿到分頁數(shù)據(jù)
List<Product> productList = getProductListByRedis(current, size);
if (productList == null || productList.size() == 0) {
log.info("當前緩存中無分頁數(shù)據(jù),當前頁:" + current + ",頁大小:" + size);
//從數(shù)據(jù)庫中拿到分頁數(shù)據(jù)
productList = getProductListByDataSource(current, size);
}
}
設想,假如某一時刻,Redis中的緩存失效啦,大量的請求,全部查到DB上,也會帶來一個災難。所以這快又涉及到一個緩存擊穿的問題。
解決緩存擊穿
-
方案一:永不過期
- 提前把熱點數(shù)據(jù)不設置過期時間,后臺異步更新緩存。
-
方案二:加互斥鎖或隊列
- 其實我理解緩存擊穿和緩存穿透差不多,所以加一個互斥鎖,讓一個線程正常請求數(shù)據(jù)庫,其他線程等待即可(這里可以使用線程池來處理),都創(chuàng)建完緩存,讓其他線程請求緩存即可。
在這里我們采用第一種方式,讓key永遠不過期。
那可能有的人會說了,這很簡單啊,那我就設置一個定時任務定時的去刷新key就可以了啊。于是寫出了如下的定時作業(yè)代碼。
// 定時任務,每隔30分鐘,從數(shù)據(jù)庫中讀取商品列表,存儲到緩存里面
priviate static final String PRODUCT_LIST_KEY = "product:list";
@Scheduled(cron = "0 */30 * * * ?")
public void loadActivityProduct() {
//從數(shù)據(jù)庫中查詢參加活動的商品列表
List<Product> productList = productMapper.queryAcitvityProductList();
//刪除舊的
redisTemplate.delete(PRODUCT_LIST_KEY);
//存儲新的
redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)
}
但是,不知道大家有沒發(fā)現(xiàn),我們即使加了定時任務的代碼也會發(fā)生緩存擊穿的問題。因為刪除舊的數(shù)據(jù) 和 存儲新的數(shù)據(jù)兩個命令非原子操作,存在時間間隔。如果改用string結構存儲,可以直接覆蓋舊值,則沒有原子性問題,但是業(yè)務需求需要支持分頁,只能用list結構。
//就在我刪除舊的key的時候,這會還沒有往redis中放入,大的并發(fā)量進來導致請求都跑到了數(shù)據(jù)庫上,造成緩存擊穿。
//刪除舊的
redisTemplate.delete(PRODUCT_LIST_KEY);
//存儲新的
redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)
解決方案
-
業(yè)務架構里面強調降級,兜底數(shù)據(jù),那緩存擊穿是不是也可以考慮這個方案,空間換時間
-
緩存兩份數(shù)據(jù),一份是List結構(先刪除,再設置新值), 一份是String結構(直接覆蓋舊值)
- 查詢的時候優(yōu)先查詢list結構,如果沒有則解析String結構成為list,進行內(nèi)存分頁,一般數(shù)據(jù)量不大
// 定時任務,每隔30分鐘,從數(shù)據(jù)庫中讀取商品列表,存儲到緩存里面
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() {
//從數(shù)據(jù)庫中查詢參加活動的商品列表
List<Product> productList = productMapper.queryAcitvityProductList();
//先緩存一份String類型的數(shù)據(jù),直接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結構如果沒有數(shù)據(jù),則查String類型的數(shù)據(jù)。
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,整篇的案例整合 我們就到這里,覺得博主寫的不錯的,記得給個三連哦?。。?mark hidden color="red">文章來源:http://www.zghlxwxcb.cn/news/detail-491649.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-491649.html
到了這里,關于【案例實戰(zhàn)】SpringBoot整合Redis實現(xiàn)緩存分頁數(shù)據(jù)查詢的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!