通用双缓存服务使用说明
简介
DualCacheService
是一个通用的缓存服务实现,采用短TTL+长TTL的双缓存策略,结合本地缓存、逻辑过期和异步刷新机制,实现高性能且防止缓存击穿的缓存方案。
特性
- 三级缓存:本地缓存(Caffeine) -> Redis短TTL缓存 -> Redis长TTL缓存(带逻辑过期)
- 防止缓存击穿:使用Redisson分布式锁
- 防止缓存穿透:使用空值缓存
- 防止缓存雪崩:使用不同的TTL和随机延迟
- 异步刷新:逻辑过期后异步刷新缓存,提高用户体验
- 错误重试:加入重试机制提高系统容错性
- 降级策略:缓存操作异常时直接查询数据库
- 批量预热:支持批量缓存预热,降低系统启动压力
- 通用性:支持任意类型的数据缓存
使用方法
1. 注入DualCacheService
在你的服务中注入DualCacheService
:
java
@Autowired
private DualCacheService<ID, T> dualCacheService;
其中ID
是数据的ID类型(如Long
、String
等),T
是要缓存的数据类型。
2. 使用getById方法获取数据
java
public T getDataById(ID id) {
// keyPrefix是缓存键的前缀,用于区分不同的业务数据
// dbFallback是查询数据库的回调函数
return dualCacheService.getById(id, "yourKeyPrefix", this::queryFromDatabase);
}
// 数据库查询方法
private T queryFromDatabase(ID id) {
return repository.findById(id);
}
3. 更新缓存
当新增或更新数据时,使用setCache
方法更新缓存:
java
public T saveData(T data) {
// 保存到数据库
T savedData = repository.save(data);
// 更新缓存
dualCacheService.setCache(savedData.getId(), savedData, "yourKeyPrefix");
return savedData;
}
4. 删除缓存
当删除数据时,使用deleteCache
方法删除缓存:
java
public void deleteData(ID id) {
// 从数据库删除
repository.deleteById(id);
// 删除缓存
dualCacheService.deleteCache(id, "yourKeyPrefix");
}
5. 缓存预热
系统启动或定时任务时,可以使用批量预热缓存:
java
// 批量预热缓存
public void preloadAllData() {
// 获取所有需要预热的ID列表
List<ID> allIds = getAllIds();
// 批量预热
dualCacheService.preloadCache(allIds, "yourKeyPrefix", this::batchQueryFromDatabase);
}
// 批量查询数据库
private List<T> batchQueryFromDatabase(List<ID> ids) {
return repository.findAllByIdIn(ids);
}
处理空值
DualCacheService
会自动处理数据库返回的null值:
- 当查询数据库返回null时,会在缓存中设置一个空值标记("NULL")
- 空值有较短的过期时间(默认2分钟),避免长时间缓存无效数据
- 当查询到空值标记时,会直接返回null,不会再查询数据库
java
// 设置空值的例子
dualCacheService.setCache(id, null, "yourKeyPrefix");
适用场景
- 读多写少的高并发场景
- 对数据一致性要求不是特别高的场景
- 需要防止缓存击穿和雪崩的场景
- 需要防止缓存穿透的场景
参数说明
- SHORT_TTL:短TTL缓存的过期时间,默认5分钟
- LONG_TTL:长TTL缓存的过期时间,默认12小时
- LOGICAL_EXPIRE:逻辑过期时间,默认10分钟
- NULL_VALUE_TTL:空值缓存过期时间,默认2分钟
- MAX_RETRY_TIMES:最大重试次数,默认3次
- RETRY_WAIT_TIME:重试等待时间,默认100毫秒
错误处理
DualCacheService
内置了以下错误处理机制:
- 重试机制:获取锁失败时会进行重试,最多重试3次
- 降级策略:当缓存操作发生异常时,会尝试直接查询数据库作为降级措施
- 异常日志:详细记录各种异常情况,便于排查问题
案例
参考DualCacheProductServiceImpl
类中的实现方式。
java
package com.example.cache.service.cache;
import com.example.cache.model.CacheData;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* 通用双缓存服务
* 采用短TTL+长TTL的双缓存策略,结合逻辑过期和异步刷新,实现无锁的缓存防护机制
* @param <ID> ID类型
* @param <T> 缓存数据类型
*/
@Slf4j
@Component
public class DualCacheService<ID, T> {
// 短时间缓存前缀,物理过期TTL短
private static final String SHORT_KEY_PREFIX = "short:";
// 长时间缓存前缀,物理TTL长,但有逻辑过期时间
private static final String LONG_KEY_PREFIX = "long:";
// 分布式锁前缀
private static final String LOCK_KEY_PREFIX = "dual:lock:";
// 空值缓存标记
private static final String NULL_VALUE_KEY = "NULL";
// 短TTL缓存物理过期时间(秒)
private static final long SHORT_TTL = 60 * 5; // 5分钟
// 长TTL缓存物理过期时间(秒)
private static final long LONG_TTL = 60 * 60 * 12; // 12小时
// 逻辑过期时间(秒)
private static final long LOGICAL_EXPIRE = 60 * 10; // 10分钟
// 空值缓存过期时间(秒)
private static final long NULL_VALUE_TTL = 60 * 2; // 2分钟
// 锁等待时间(秒)
private static final int LOCK_WAIT_TIME = 10;
// 最大重试次数
private static final int MAX_RETRY_TIMES = 3;
// 重试等待时间(毫秒)
private static final int RETRY_WAIT_TIME = 100;
// 刷新随机延迟最大值(毫秒),防止同时刷新
private static final int REFRESH_DELAY_MAX = 1000;
// 本地缓存,作为第一级缓存
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 根据ID获取数据,使用双缓存策略
* @param id 对象ID
* @param keyPrefix 缓存键前缀
* @param dbFallback 数据库查询回调函数
* @return 查询到的对象
*/
public T getById(ID id, String keyPrefix, Function<ID, T> dbFallback) {
if (id == null) {
return null;
}
String cacheKeyPrefix = keyPrefix + ":";
String shortKey = SHORT_KEY_PREFIX + cacheKeyPrefix + id;
String longKey = LONG_KEY_PREFIX + cacheKeyPrefix + id;
String lockKey = LOCK_KEY_PREFIX + cacheKeyPrefix + id;
// 1. 查询本地缓存
Object localCacheResult = localCache.getIfPresent(shortKey);
if (localCacheResult != null) {
log.info("【双缓存】本地缓存命中,key: {}", shortKey);
// 如果是空值标记,返回null
if (NULL_VALUE_KEY.equals(localCacheResult)) {
return null;
}
return (T) localCacheResult;
}
try {
// 2. 查询Redis短TTL缓存
Object shortTTLResult = redisTemplate.opsForValue().get(shortKey);
if (shortTTLResult != null) {
log.info("【双缓存】短TTL缓存命中,key: {}", shortKey);
// 如果是空值标记,返回null
if (NULL_VALUE_KEY.equals(shortTTLResult)) {
// 更新本地缓存
localCache.put(shortKey, NULL_VALUE_KEY);
return null;
}
T data = (T) shortTTLResult;
// 更新本地缓存
localCache.put(shortKey, data);
return data;
}
// 3. 查询Redis长TTL缓存
Object longTTLResult = redisTemplate.opsForValue().get(longKey);
if (longTTLResult != null) {
// 长TTL缓存命中
CacheData<T> cacheData = (CacheData<T>) longTTLResult;
T data = cacheData.getData();
if (cacheData.isExpired()) {
// 逻辑过期,返回旧数据,并异步刷新缓存
log.info("【双缓存】长TTL缓存命中但逻辑过期,返回旧数据并异步刷新,key: {}", longKey);
asyncRefreshCache(id, cacheKeyPrefix, dbFallback);
} else {
log.info("【双缓存】长TTL缓存命中且未过期,key: {}", longKey);
}
// 无论是否过期,先将长TTL缓存的数据写入短TTL缓存
redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
// 更新本地缓存
localCache.put(shortKey, data);
return data;
}
// 4. 缓存全部未命中,使用Redisson分布式锁防止并发查询数据库
return queryWithLock(id, keyPrefix, dbFallback, shortKey, longKey, lockKey);
} catch (Exception e) {
log.error("【双缓存】获取数据异常,id: {}, 尝试走降级策略", id, e);
// 降级策略:尝试直接查询数据库
try {
return dbFallback.apply(id);
} catch (Exception ex) {
log.error("【双缓存】降级查询数据库也失败,id: {}", id, ex);
throw new RuntimeException("获取数据失败", e);
}
}
}
/**
* 使用分布式锁查询并更新缓存
*/
private T queryWithLock(ID id, String keyPrefix, Function<ID, T> dbFallback,
String shortKey, String longKey, String lockKey) {
log.info("【双缓存】缓存全部未命中,尝试获取分布式锁,key: {}", lockKey);
RLock lock = redissonClient.getLock(lockKey);
// 使用循环代替递归,避免栈溢出风险
int retryTimes = 0;
while (retryTimes < MAX_RETRY_TIMES) {
try {
// 尝试获取锁,最多等待10秒,锁自动释放时间为30秒
boolean locked = lock.tryLock(LOCK_WAIT_TIME, 30, TimeUnit.SECONDS);
if (!locked) {
// 获取锁失败,说明有其他线程正在更新缓存,等待一会再查Redis
log.info("【双缓存】获取分布式锁失败,等待{}ms后重试,key: {}, 重试次数: {}",
RETRY_WAIT_TIME, lockKey, retryTimes + 1);
try {
TimeUnit.MILLISECONDS.sleep(RETRY_WAIT_TIME);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 重新查询Redis,可能其他线程已经更新了缓存
// 检查短TTL缓存
Object retryShortResult = redisTemplate.opsForValue().get(shortKey);
if (retryShortResult != null) {
log.info("【双缓存】重试查询短TTL缓存命中,key: {}", shortKey);
// 如果是空值标记,返回null
if (NULL_VALUE_KEY.equals(retryShortResult)) {
localCache.put(shortKey, NULL_VALUE_KEY);
return null;
}
T data = (T) retryShortResult;
localCache.put(shortKey, data);
return data;
}
retryTimes++;
continue;
}
try {
// 双重检查,获取锁后再次查询缓存
// 检查短TTL缓存
Object shortTTLResult = redisTemplate.opsForValue().get(shortKey);
if (shortTTLResult != null) {
log.info("【双缓存】获取锁后再次检查-短TTL缓存命中,key: {}", shortKey);
// 如果是空值标记,返回null
if (NULL_VALUE_KEY.equals(shortTTLResult)) {
localCache.put(shortKey, NULL_VALUE_KEY);
return null;
}
T data = (T) shortTTLResult;
localCache.put(shortKey, data);
return data;
}
// 检查长TTL缓存
Object longTTLResult = redisTemplate.opsForValue().get(longKey);
if (longTTLResult != null) {
CacheData<T> cacheData = (CacheData<T>) longTTLResult;
T data = cacheData.getData();
if (cacheData.isExpired()) {
log.info("【双缓存】获取锁后再次检查-长TTL缓存命中但逻辑过期,key: {}", longKey);
// 这里不需要异步刷新,因为接下来会直接查询数据库并更新缓存
} else {
log.info("【双缓存】获取锁后再次检查-长TTL缓存命中且未过期,key: {}", longKey);
// 更新短TTL缓存和本地缓存
redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
localCache.put(shortKey, data);
return data;
}
}
// 5. 查询数据库
log.info("【双缓存】获取锁后查询数据库,id: {}", id);
T data = dbFallback.apply(id);
if (data != null) {
// 数据存在,更新缓存
// 更新短TTL缓存
redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
// 更新长TTL缓存
CacheData<T> cacheData = CacheData.create(data, LOGICAL_EXPIRE);
redisTemplate.opsForValue().set(longKey, cacheData, LONG_TTL, TimeUnit.SECONDS);
// 更新本地缓存
localCache.put(shortKey, data);
} else {
// 数据不存在,缓存空值
log.info("【双缓存】数据库查询结果为空,缓存空值,id: {}", id);
redisTemplate.opsForValue().set(shortKey, NULL_VALUE_KEY, NULL_VALUE_TTL, TimeUnit.SECONDS);
localCache.put(shortKey, NULL_VALUE_KEY);
}
return data;
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("【双缓存】释放分布式锁,key: {}", lockKey);
}
}
} catch (InterruptedException e) {
log.error("【双缓存】获取锁被中断,key: {}", lockKey, e);
Thread.currentThread().interrupt();
throw new RuntimeException("获取数据被中断", e);
} catch (Exception e) {
log.error("【双缓存】锁操作异常,key: {}, 重试次数: {}", lockKey, retryTimes, e);
retryTimes++;
if (retryTimes >= MAX_RETRY_TIMES) {
throw e;
}
try {
TimeUnit.MILLISECONDS.sleep(RETRY_WAIT_TIME * retryTimes);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
// 所有重试都失败,直接查询数据库作为最后手段
log.warn("【双缓存】所有重试都失败,直接查询数据库,id: {}", id);
return dbFallback.apply(id);
}
/**
* 异步刷新缓存
* @param id 对象ID
* @param keyPrefix 缓存键前缀
* @param dbFallback 数据库查询回调函数
*/
@Async
public void asyncRefreshCache(ID id, String keyPrefix, Function<ID, T> dbFallback) {
// 随机延迟,避免集中刷新
try {
int randomDelay = ThreadLocalRandom.current().nextInt(REFRESH_DELAY_MAX);
Thread.sleep(randomDelay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("【双缓存】异步刷新缓存,id: {}", id);
// 从数据库查询最新数据
T data = dbFallback.apply(id);
String shortKey = SHORT_KEY_PREFIX + keyPrefix + ":" + id;
String longKey = LONG_KEY_PREFIX + keyPrefix + ":" + id;
try {
if (data != null) {
// 更新短TTL缓存
redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
// 更新长TTL缓存
CacheData<T> cacheData = CacheData.create(data, LOGICAL_EXPIRE);
redisTemplate.opsForValue().set(longKey, cacheData, LONG_TTL, TimeUnit.SECONDS);
// 更新本地缓存
localCache.put(shortKey, data);
log.info("【双缓存】异步刷新缓存完成,id: {}", id);
} else {
// 数据库不存在,缓存空值
log.info("【双缓存】异步刷新缓存,数据不存在,缓存空值,id: {}", id);
redisTemplate.opsForValue().set(shortKey, NULL_VALUE_KEY, NULL_VALUE_TTL, TimeUnit.SECONDS);
localCache.put(shortKey, NULL_VALUE_KEY);
// 删除长TTL缓存
redisTemplate.delete(longKey);
}
} catch (Exception e) {
log.error("【双缓存】异步刷新缓存异常,id: {}", id, e);
}
}
/**
* 设置缓存
* @param id 对象ID
* @param data 对象数据
* @param keyPrefix 缓存键前缀
*/
public void setCache(ID id, T data, String keyPrefix) {
if (id == null) {
return;
}
String shortKey = SHORT_KEY_PREFIX + keyPrefix + ":" + id;
String longKey = LONG_KEY_PREFIX + keyPrefix + ":" + id;
if (data != null) {
// 更新短TTL缓存
redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
// 更新长TTL缓存
CacheData<T> cacheData = CacheData.create(data, LOGICAL_EXPIRE);
redisTemplate.opsForValue().set(longKey, cacheData, LONG_TTL, TimeUnit.SECONDS);
// 更新本地缓存
localCache.put(shortKey, data);
} else {
// 缓存空值
redisTemplate.opsForValue().set(shortKey, NULL_VALUE_KEY, NULL_VALUE_TTL, TimeUnit.SECONDS);
localCache.put(shortKey, NULL_VALUE_KEY);
// 删除长TTL缓存
redisTemplate.delete(longKey);
}
}
/**
* 删除缓存
* @param id 对象ID
* @param keyPrefix 缓存键前缀
*/
public void deleteCache(ID id, String keyPrefix) {
if (id == null) {
return;
}
String shortKey = SHORT_KEY_PREFIX + keyPrefix + ":" + id;
String longKey = LONG_KEY_PREFIX + keyPrefix + ":" + id;
redisTemplate.delete(shortKey);
redisTemplate.delete(longKey);
localCache.invalidate(shortKey);
}
/**
* 批量预热缓存
* @param ids ID列表
* @param keyPrefix 缓存键前缀
* @param batchDbFallback 批量查询数据库的回调函数
*/
public void preloadCache(List<ID> ids, String keyPrefix, Function<List<ID>, List<T>> batchDbFallback) {
if (ids == null || ids.isEmpty()) {
return;
}
log.info("【双缓存】开始批量预热缓存,keyPrefix: {}, 数量: {}", keyPrefix, ids.size());
try {
// 批量查询数据库
List<T> dataList = batchDbFallback.apply(ids);
if (dataList != null && !dataList.isEmpty()) {
// 这里假设T类型对象有getId()方法,实际使用时可能需要提供一个Function来获取ID
for (T data : dataList) {
// 获取对象的ID,这里简化处理,实际应该通过反射或Function获取
ID dataId = getIdFromData(data);
if (dataId != null) {
setCache(dataId, data, keyPrefix);
}
}
log.info("【双缓存】批量预热缓存完成,keyPrefix: {}, 实际预热数量: {}", keyPrefix, dataList.size());
} else {
log.info("【双缓存】批量预热缓存,没有数据,keyPrefix: {}", keyPrefix);
}
} catch (Exception e) {
log.error("【双缓存】批量预热缓存异常,keyPrefix: {}", keyPrefix, e);
}
}
/**
* 从数据对象中获取ID
* 这是一个示例方法,实际使用时应该根据具体的数据类型实现
* 可以考虑使用反射或在使用时提供一个Function
*/
@SuppressWarnings("unchecked")
private ID getIdFromData(T data) {
try {
// 这里假设数据对象有getId方法
return (ID) data.getClass().getMethod("getId").invoke(data);
} catch (Exception e) {
log.error("【双缓存】从数据对象获取ID失败", e);
return null;
}
}
}
java
package com.example.cache.service.impl;
import com.example.cache.model.CacheData;
import com.example.cache.model.Product;
import com.example.cache.repository.ProductRepository;
import com.example.cache.service.ProductService;
import com.example.cache.service.cache.DualCacheService;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* 采用双缓存策略的产品服务实现
* 采用短TTL+长TTL的双缓存策略,结合逻辑过期和异步刷新,实现无锁的缓存防护机制
*/
@Slf4j
@Service("dualCacheProductService")
public class DualCacheProductServiceImpl implements ProductService {
// 缓存键前缀
private static final String PRODUCT_KEY_PREFIX = "product";
// 列表缓存键
private static final String PRODUCT_LIST_KEY = "dual:product:list";
// 短TTL缓存物理过期时间(秒)
private static final long SHORT_TTL = 60 * 5; // 5分钟
@Autowired
private ProductRepository productRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private DualCacheService<Long, Product> dualCacheService;
/**
* 应用启动时预热缓存
*/
@PostConstruct
@Override
public void preloadCache() {
log.info("【双缓存】开始预热产品缓存...");
try {
List<Product> products = productRepository.findAll();
// 批量缓存所有产品
for (Product product : products) {
dualCacheService.setCache(product.getId(), product, PRODUCT_KEY_PREFIX);
}
// 缓存产品列表
redisTemplate.opsForValue().set(PRODUCT_LIST_KEY, products, SHORT_TTL, TimeUnit.SECONDS);
log.info("【双缓存】缓存预热完成,共预热{}个产品", products.size());
} catch (Exception e) {
log.error("【双缓存】缓存预热失败", e);
}
}
/**
* 根据ID获取产品,使用双缓存策略
* 当缓存全部未命中查询数据库时,使用Redisson分布式锁防止并发查询
*/
@Override
public Product getProductById(Long id) {
return dualCacheService.getById(id, PRODUCT_KEY_PREFIX, productRepository::findById);
}
/**
* 获取所有产品,使用双缓存策略
*/
@Override
public List<Product> getAllProducts() {
// 查询Redis短TTL缓存
Object result = redisTemplate.opsForValue().get(PRODUCT_LIST_KEY);
if (result != null) {
log.info("【双缓存】产品列表缓存命中");
return (List<Product>) result;
}
// 缓存未命中,查询数据库
log.info("【双缓存】产品列表缓存未命中,查询数据库");
List<Product> products = productRepository.findAll();
// 更新缓存
redisTemplate.opsForValue().set(PRODUCT_LIST_KEY, products, SHORT_TTL, TimeUnit.SECONDS);
return products;
}
/**
* 保存产品并更新缓存
*/
@Override
public Product saveProduct(Product product) {
if (product == null) {
return null;
}
// 保存到数据库
Product savedProduct = productRepository.save(product);
Long id = savedProduct.getId();
// 更新缓存
dualCacheService.setCache(id, savedProduct, PRODUCT_KEY_PREFIX);
// 删除列表缓存
redisTemplate.delete(PRODUCT_LIST_KEY);
return savedProduct;
}
/**
* 更新产品并更新缓存
*/
@Override
public Product updateProduct(Product product) {
if (product == null || product.getId() == null) {
return null;
}
// 更新数据库
Product updatedProduct = productRepository.save(product);
Long id = updatedProduct.getId();
// 更新缓存
dualCacheService.setCache(id, updatedProduct, PRODUCT_KEY_PREFIX);
// 删除列表缓存
redisTemplate.delete(PRODUCT_LIST_KEY);
return updatedProduct;
}
/**
* 删除产品并更新缓存
*/
@Override
public void deleteProduct(Long id) {
if (id == null) {
return;
}
// 从数据库删除
productRepository.deleteById(id);
// 删除缓存
dualCacheService.deleteCache(id, PRODUCT_KEY_PREFIX);
// 删除列表缓存
redisTemplate.delete(PRODUCT_LIST_KEY);
}
}