功能02-商鋪查詢緩存03 3.功能02-商鋪查詢緩存 3.6封裝redis工具類 3.6.1需求說明 基於StringRedisTemplate封裝一個工具列,滿足下列需求: 方法1:將任意Java對象序列化為json,並存儲在string類型的key中,並且可以設置TTL過期時間 方法2:將任意 ...
功能02-商鋪查詢緩存03
3.功能02-商鋪查詢緩存
3.6封裝redis工具類
3.6.1需求說明
基於StringRedisTemplate封裝一個工具列,滿足下列需求:
方法1:將任意Java對象序列化為json,並存儲在string類型的key中,並且可以設置TTL過期時間
方法2:將任意Java對象序列化為json,並存儲在string類型的key中,並且可以設置邏輯過期時間,用戶處理緩存擊穿問題(針對熱點key)
方法3:根據指定的key查詢緩存,並反序列化為指定類型,利用緩存空值的方式解決緩存穿透問題
方法4:根據指定的key查詢緩存,並反序列化為指定類型,需要利用邏輯過期解決緩存擊穿問題(針對熱點key)
3.6.2代碼實現
(1)創建redis工具類,封裝上述方法
package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.hmdp.utils.RedisConstants.*;
/**
* @author 李
* @version 1.0
* 封裝redis工具類
*/
@Component
@Slf4j
public class CacheClient {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 將任意Java對象序列化為json,並存儲在string類型的key中,並且可以設置TTL過期時間
*
* @param key 緩存的key值
* @param value 緩存的value值
* @param time 過期時間值
* @param unit 過期的時間單位
*/
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue()
.set(key, JSONUtil.toJsonStr(value), time, unit);
}
/**
* 將任意Java對象序列化為json,並存儲在string類型的key中,
* 並且可以設置邏輯過期時間,用戶處理緩存擊穿問題(針對熱點key)
*
* @param key 緩存的key值
* @param value 緩存的value值
* @param time 過期時間值
* @param unit 過期的時間單位
*/
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
//設置邏輯過期時間
RedisData redisData = new RedisData();
redisData.setData(value);
//邏輯過期時間=當前時間+指定的時間
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* 根據指定的key查詢緩存,並反序列化為指定類型,利用緩存空值的方式解決緩存穿透問題
*
* @param keyPrefix 查詢的key值的首碼
* @param id 查詢的key值的尾碼
* @param type 要轉換的Class類型
* @param dbFallback 傳入的函數
* @param time 過期時間值
* @param unit 時間單位
* @param <R> 泛型
* @param <ID> 泛型
* @return 返回指定的類型對象
*/
public <R, ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
//redis查詢緩存
String json = stringRedisTemplate.opsForValue().get(key);
//判斷json是否存在
if (StrUtil.isNotBlank(json)) {
//存在,轉為java對象並返回
return JSONUtil.toBean(json, type);
}
//判斷是否為"",如果是,說明該key是為瞭解決緩存穿透設置的空值
if ("".equals(json)) {
//返回錯誤信息
return null;
}
//不存在,根據id查詢資料庫——使用函數式編程
R r = dbFallback.apply(id);
if (r == null) {//說明資料庫中沒有該數據
//緩存空值,應對緩存穿透
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
//返回錯誤信息
return null;
}
//r存在,則將其寫入redis
this.set(key, r, time, unit);
return r;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR =
Executors.newFixedThreadPool(10);
/**
* 根據指定的key查詢緩存,並反序列化為指定類型,需要利用邏輯過期解決緩存擊穿問題(針對熱點key)
* @param keyPrefix 查詢的key值的首碼
* @param id 查詢的key值的尾碼
* @param type 要轉換的Class類型
* @param dbFallback 傳入的函數
* @param time 過期時間值
* @param unit 時間單位
* @param <R> 泛型
* @param <ID> 泛型
* @return 返回指定的類型對象
*/
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type,
Function<ID, R> dbFallback, Long time,
TimeUnit unit) {
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
//這裡不再考慮緩存穿透問題,因為key永不過期
if (StrUtil.isBlank(json)) {
//如果未命中,說明不是熱點key,直接返回null
return null;
}
//如果命中
//先把json反序列化為對象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
//判斷是否邏輯過期
if (expireTime.isAfter(LocalDateTime.now())) {
//未過期,直接返回信息
return r;
}
//過期,獲取互斥鎖
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock) {//成功獲取互斥鎖
//開啟獨立線程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建緩存
//先查詢資料庫
R apply = dbFallback.apply(id);
//再存入reids緩存
this.setWithLogicalExpire(key, apply, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}
//釋放互斥鎖
unLock(lockKey);
});
}
//如果未獲取互斥鎖,直接返回舊數據
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
}
(2)修改ShopServiceImpl,調用封裝好的方法,簡化代碼
package com.hmdp.service.impl;
import ...
/**
* 服務實現類
*
* @author 李
* @version 1.0
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop>
implements IShopService {
@Resource
StringRedisTemplate stringRedisTemplate;
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
//緩存穿透
//Shop shop =
// cacheClient.queryWithPassThrough
// (CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
//緩存擊穿方案(邏輯過期)
Shop shop = cacheClient.queryWithLogicalExpire
(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.MINUTES);
if (shop == null) {
return Result.fail("店鋪不存在!");
}
return Result.ok(shop);
}
@Override
@Transactional
public Result update(Shop shop) {
...
}
}