基於StringRedisTemplate封裝一個快取工具類,滿足下列需求:
- 方法1:將任意Java物件序列化為json並儲存在string型別的key中,並且可以設定TTL過期時間
- 方法2:將任意Java物件序列化為json並儲存在string型別的key中,並且可以設定邏輯過期時間,用於處理緩
存擊穿問題
- 方法3:根據指定的key查詢快取,並反序列化為指定型別,利用快取空值的方式解決快取穿透問題
- 方法4:根據指定的key查詢快取,並反序列化為指定型別,需要利用邏輯過期解決快取擊穿問題
將邏輯進行封裝
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, 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)));
// 寫入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.從redis查詢商鋪快取
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判斷是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判斷命中的是否是空值
if (json != null) {
// 返回一個錯誤資訊
return null;
}
// 4.不存在,根據id查詢資料庫
R r = dbFallback.apply(id);
// 5.不存在,返回錯誤
if (r == null) {
// 將空值寫入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回錯誤資訊
return null;
}
// 6.存在,寫入redis
this.set(key, r, time, unit);
return r;
}
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.從redis查詢商鋪快取
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判斷是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化為物件
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判斷是否過期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未過期,直接返回店鋪資訊
return r;
}
// 5.2.已過期,需要快取重建
// 6.快取重建
// 6.1.獲取互斥鎖
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判斷是否獲取鎖成功
if (isLock){
// 6.3.成功,開啟獨立執行緒,實現快取重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查詢資料庫
R newR = dbFallback.apply(id);
// 重建快取
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 釋放鎖
unlock(lockKey);
}
});
}
// 6.4.返回過期的商鋪資訊
return r;
}
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.從redis查詢商鋪快取
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判斷是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判斷命中的是否是空值
if (shopJson != null) {
// 返回一個錯誤資訊
return null;
}
// 4.實現快取重建
// 4.1.獲取互斥鎖
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判斷是否獲取成功
if (!isLock) {
// 4.3.獲取鎖失敗,休眠並重試
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.獲取鎖成功,根據id查詢資料庫
r = dbFallback.apply(id);
// 5.不存在,返回錯誤
if (r == null) {
// 將空值寫入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回錯誤資訊
return null;
}
// 6.存在,寫入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.釋放鎖
unlock(lockKey);
}
// 8.返回
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);
}
}
在ShopServiceImpl 中
@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
// .queryWithMutex(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.SECONDS);
if (shop == null) {
return Result.fail("店鋪不存在!");
}
// 7.返回
return Result.ok(shop);
}