文章目录
前言
该工具类包含以下功能:
1.将任意对象存储在 hash 类型的 key 中,并可以设置 TTL
2.将任意对象存储在 hash 类型的 key 中,并且可以设置逻辑过期时间
3.将空对象存入 hash 类型的 key 中,并且可以设置超时时间
4.缓存空对象解决缓存穿透
5.布隆过滤器解决缓存穿透
6.布隆过滤器+缓存空对象解决缓存穿透
7.互斥锁解决缓存击穿
8.逻辑过期解决缓存击穿
以下是关键代码
IBloomFilter
IBloomFilter 是自定义的布隆过滤器接口。这里使用接口的原因在于,每个业务使用的布隆过滤器可能是不一样的,因此为了让工具类能兼容所有的布隆过滤器,这里添加接口,并使用泛型表示布隆过滤器内部存储数据的类型
public interface IBloomFilter<T> {
// 添加
void add(T id);
// 判断是否存在
boolean mightContain(T id);
}
ObjectMapUtils
ObjectMapUtils 是对象与 Map 类型的相互转换,可以让对象转换为 Map 集合,也可以让 Map 集合转回对象
public class ObjectMapUtils {
// 将对象转为 Map
public static Map<String, String> obj2Map(Object obj) throws IllegalAccessException {
Map<String, String> result = new HashMap<>();
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 如果为 static 且 final 则跳过
if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
continue;
}
field.setAccessible(true); // 设置为可访问私有字段
Object fieldValue = field.get(obj);
if (fieldValue != null) {
result.put(field.getName(), field.get(obj).toString());
}
}
return result;
}
// 将 Map 转为对象
public static<R> R map2Obj(Map<Object, Object> map, Class<R> clazz) throws Exception {
R obj = clazz.getDeclaredConstructor().newInstance();
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object fieldName = entry.getKey();
Object fieldValue = entry.getValue();
Field field = clazz.getDeclaredField(fieldName.toString());
field.setAccessible(true); // 设置为可访问私有字段
String fieldValueStr = fieldValue.toString();
// 根据字段类型进行转换
fillField(obj, field, fieldValueStr);
}
return obj;
}
// 将 Map 转为对象(含排除字段)
public static<R> R map2Obj(Map<Object, Object> map, Class<R> clazz, String... excludeFields) throws Exception {
R obj = clazz.getDeclaredConstructor().newInstance();
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object fieldName = entry.getKey();
if(Arrays.asList(excludeFields).contains(fieldName)) {
continue;
}
Object fieldValue = entry.getValue();
Field field = clazz.getDeclaredField(fieldName.toString());
field.setAccessible(true); // 设置为可访问私有字段
String fieldValueStr = fieldValue.toString();
// 根据字段类型进行转换
fillField(obj, field, fieldValueStr);
}
return obj;
}
// 填充字段
private static void fillField(Object obj, Field field, String value) throws IllegalAccessException {
if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {
field.set(obj, Integer.parseInt(value));
} else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
field.set(obj, Boolean.parseBoolean(value));
} else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {
field.set(obj, Double.parseDouble(value));
} else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {
field.set(obj, Long.parseLong(value));
} else if (field.getType().equals(String.class)) {
field.set(obj, value);
} else if(field.getType().equals(LocalDateTime.class)) {
field.set(obj, LocalDateTime.parse(value));
}
// 如果有需要可以继续添加...
}
}
CacheClient
CacheClient 就是缓存工具类,包含了之前提到的所有功能
@Component
@Slf4j
public class CacheClient {
@Autowired
private StringRedisTemplate redisTemplate;
// 重建缓存线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 将任意对象存储在 hash 类型的 key 中,并可以设置 TTL
public void setByHash(String key, Object value, Long time, TimeUnit unit) throws IllegalAccessException {
redisTemplate.opsForHash().putAll(key, ObjectMapUtils.obj2Map(value));
redisTemplate.expire(key, time, unit);
}
// 将任意对象存储在 hash 类型的 key 中,并且可以设置逻辑过期时间
public void setWithLogicalExpireByHash(String key, Object value, Long time, TimeUnit unit) throws IllegalAccessException {
Map<String, String> map = ObjectMapUtils.obj2Map(value);
// 添加逻辑过期时间
map.put(RedisConstants.EXPIRE_KEY, LocalDateTime.now().plusSeconds(unit.toSeconds(time)).toString());
redisTemplate.opsForHash().putAll(key, map);
}
// 将空对象存入 hash 类型的 key 中,并且可以设置超时时间
public void setNullByHash(String key, Long time, TimeUnit unit) {
redisTemplate.opsForHash().put(key, "", "");
redisTemplate.expire(key, time, unit);
}
// 尝试加锁
private boolean tryLock(String key, Long time, TimeUnit unit) {
Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(key,"1", time, unit);
return Boolean.TRUE.equals(isLocked);
}
// 解锁
private void unlock(String key) {
redisTemplate.delete(key);
}
// 缓存空对象解决缓存穿透
public<R, ID> R queryWithCacheNull(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
Long time, TimeUnit unit) {
// 从 redis 查询
String key = keyPrefix + id;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
// 缓存命中
if (!entries.isEmpty()) {
try {
// 如果是空对象,表示一定不存在数据库中,直接返回(解决缓存穿透)
if (entries.containsKey("")) {
log.info("redis查询到id={}的空对象,", id);
return null;
}
// 刷新有效期
redisTemplate.expire(key, time, unit);
R r = ObjectMapUtils.map2Obj(entries, clazz);
log.info("缓存命中,结果为:{}", r);
return r;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 查询数据库
R r = dbFallback.apply(id);
if (r == null) {
log.info("id={}不存在于数据库,存入redis", id);
// 存入空值
setNullByHash(key, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
// 不存在,直接返回
return null;
}
// 存在,写入 redis
try {
setByHash(key, r, time, unit);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
log.info("查询数据库,获取结果:{}", r);
return r;
}
// 布隆过滤器解决缓存穿透
public<R, ID> R queryWithBloom(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
Long time, TimeUnit unit, IBloomFilter<ID> bloomFilter) {
// 如果不在布隆过滤器中,直接返回
if (!bloomFilter.mightContain(id)) {
log.info("id={}不存在于布隆过滤器", id);
return null;
}
// 从 redis 查询
String key = keyPrefix + id;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
// 缓存命中
if (!entries.isEmpty()) {
try {
// 刷新有效期
redisTemplate.expire(key, time, unit);
R r = ObjectMapUtils.map2Obj(entries, clazz);
log.info("缓存命中,结果为:{}", r);
return r;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 查询数据库
R r = dbFallback.apply(id);
if (r == null) {
log.info("id={}不存在于数据库", id);
// 不存在,直接返回
return null;
}
// 存在,写入 redis
try {
setByHash(key, r, time, unit);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
log.info("查询数据库,获取结果:{}", r);
return r;
}
// 布隆过滤器+缓存空对象解决缓存穿透
public<R, ID> R queryWithBloomAndCacheNull(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
Long time, TimeUnit unit, IBloomFilter<ID> bloomFilter) {
// 如果不在布隆过滤器中,直接返回
if (!bloomFilter.mightContain(id)) {
log.info("id={}不存在于布隆过滤器", id);
return null;
}
// 从 redis 查询
String key = keyPrefix + id;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
// 缓存命中
if (!entries.isEmpty()) {
try {
// 如果是空对象,表示一定不存在数据库中,直接返回(解决缓存穿透)
if (entries.containsKey("")) {
log.info("redis查询到id={}的空对象,", id);
return null;
}
// 刷新有效期
redisTemplate.expire(key, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
R r = ObjectMapUtils.map2Obj(entries, clazz);
log.info("缓存命中,结果为:{}", r);
return r;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 查询数据库
R r = dbFallback.apply(id);
if (r == null) {
log.info("id={}不存在于数据库,存入redis", id);
// 存入空值
setNullByHash(key, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
// 不存在,直接返回
return null;
}
// 存在,写入 redis
try {
setByHash(key, r, time, unit);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
log.info("查询数据库,获取结果:{}", r);
return r;
}
// 互斥锁解决缓存击穿
public<R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
Long cacheTime, TimeUnit cacheUnit, String lockKeyPrefix,
Long lockTime, TimeUnit lockUnit) {
String key = keyPrefix + id;
String lockKey = lockKeyPrefix + id;
boolean flag = false;
int cnt = 10; // 重试次数
try {
do {
// 从 redis 查询
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
// 缓存命中
if (!entries.isEmpty()) {
try {
// 刷新有效期
redisTemplate.expire(key, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
R r = ObjectMapUtils.map2Obj(entries, clazz);
log.info("缓存命中,结果为:{}", r);
return r;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 缓存未命中,尝试获取互斥锁
log.info("缓存未命中,尝试获取互斥锁 id={}", id);
flag = tryLock(lockKey, lockTime, lockUnit);
if (flag) { // 获取成功,进行下一步
log.info("成功获取互斥锁 id={}", id);
break;
}
// 获取失败,睡眠后重试
Thread.sleep(50);
} while ((--cnt) != 0); // 未获取到锁,休眠后重试
if(!flag) { // 重试次数到达上限
log.info("重试次数达到上限 id={}", id);
return null;
}
// 查询数据库
R r = dbFallback.apply(id);
if (r == null) {
log.info("id={}不存在于数据库", id);
// 不存在,直接返回
return null;
}
// 存在,写入 redis
try {
setByHash(key, r, cacheTime, cacheUnit);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
log.info("查询数据库,获取结果:{}", r);
return r;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
if (flag) { // 获取了锁需要释放
log.info("解锁id={}", id);
unlock(lockKey);
}
}
}
// 逻辑过期解决缓存击穿
public<R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
Long expireTime, TimeUnit expireUnit,
String lockKeyPrefix, Long lockTime, TimeUnit lockUnit) {
String key = keyPrefix + id;
String lockKey = lockKeyPrefix + id;
// 从 redis 查询
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
// 缓存未命中,返回空
if(entries.isEmpty()) {
log.info("缓存未命中,返回空 id={}", id);
return null;
}
try {
R r = ObjectMapUtils.map2Obj(entries, clazz, RedisConstants.EXPIRE_KEY);
LocalDateTime expire = LocalDateTime.parse(entries.get(RedisConstants.EXPIRE_KEY).toString());
// 判断缓存是否过期
if(expire.isAfter(LocalDateTime.now())) {
// 未过期则直接返回
log.info("缓存未过期,结果为;{}", r);
return r;
}
// 过期需要先尝试获取互斥锁
log.info("尝试获取互斥锁 id={}", id);
if(tryLock(lockKey, lockTime, lockUnit)) {
log.info("获得到互斥锁 id={}", id);
// 获取成功
// 双重检验
entries = redisTemplate.opsForHash().entries(key);
r = ObjectMapUtils.map2Obj(entries, clazz, RedisConstants.EXPIRE_KEY);
expire = LocalDateTime.parse(entries.get(RedisConstants.EXPIRE_KEY).toString());
if(expire.isAfter(LocalDateTime.now())) {
// 未过期则直接返回
log.info("缓存未过期,结果为;{}", r);
log.info("解锁 id={}", id);
unlock(lockKey);
return r;
}
// 通过线程池完成重建缓存任务
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
log.info("进行重建缓存任务 id={}", id);
setWithLogicalExpireByHash(key, dbFallback.apply(id), expireTime, expireUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
log.info("解锁 id={}", id);
unlock(lockKey);
}
});
}
log.info("返回结果:{}", r);
return r;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
使用示例
具体业务的布隆过滤器
public class ShopBloomFilter implements IBloomFilter<Long> {
private BloomFilter<Long> bloomFilter;
public ShopBloomFilter(ShopMapper shopMapper) {
// 初始化布隆过滤器,设计预计元素数量为100_0000L,误差率为1%
bloomFilter = BloomFilter.create(Funnels.longFunnel(), 100_0000, 0.01);
// 将数据库中已有的店铺 id 加入布隆过滤器
List<Shop> shops = shopMapper.selectList(null);
for (Shop shop : shops) {
bloomFilter.put(shop.getId());
}
}
public void add(Long id) {
bloomFilter.put(id);
}
public boolean mightContain(Long id){
return bloomFilter.mightContain(id);
}
}
控制层
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryShopById(id);
}
服务层
@Override
public Result queryShopById(Long id) {
// 缓存空对象解决缓存穿透
/*Shop shop = cacheClient.queryWithCacheNull(RedisConstants.CACHE_SHOP_KEY,
id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);*/
// 布隆过滤器解决缓存穿透
/*Shop shop = cacheClient.queryWithBloom(RedisConstants.CACHE_SHOP_KEY,
id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES, shopBloomFilter);*/
// 布隆过滤器+缓存空对象解决缓存穿透
/*Shop shop = cacheClient.queryWithBloomAndCacheNull(RedisConstants.CACHE_SHOP_KEY,
id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES, shopBloomFilter);*/
// 互斥锁解决缓存击穿
/*Shop shop = cacheClient.queryWithMutex(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById,
RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES,
RedisConstants.LOCK_SHOP_KEY, RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);*/
// 逻辑过期解决缓存击穿
Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById,
RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES,
RedisConstants.LOCK_SHOP_KEY, RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
if(shop == null) {
return Result.fail("商铺不存在");
}
return Result.ok(shop);
}
标签:缓存,return,log,Redis,击穿,field,key,id
From: https://blog.csdn.net/Vendetta_A_A/article/details/143258243