一、封装redis工具类
基于stringredistemplate封装一个缓存工具类,满足下列需求:
- 方法1:将任意java对象序列化为json并存储在string类型的key中,并且可以设置ttl过期时间
- 方法2:将任意java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
- 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
- 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
- 方法5:根据指定的key查询缓存,并反序列化为指定类型,需要利用互斥锁解决缓存击穿问题
1. 使用构造方法注入stringredistemplate
@slf4j
@component
public class cacheclient {
private final stringredistemplate stringredistemplate;
// 构造方法注入stringredistemplate
public cacheclient(stringredistemplate stringredistemplate) {
this.stringredistemplate = stringredistemplate;
}
}
2. 方法1
将任意java对象序列化为json并存储在string类型的key中,并且可以设置ttl过期时间
public void set(string key, object value, long time, timeunit unit) {
stringredistemplate.opsforvalue().set(key, jsonutil.tojsonstr(value), time, unit);
}
3. 方法2
将任意java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
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));
}
4. 方法3
根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
/**
* @param keyprefix 业务的缓存key前缀,例如:"cache:shop:"
* @param id
* @param type 对象类型,例如:user.class、shop.class
* @param dbfallback 传入查询数据库的方法,例如:getbyid(id);
* @param time
* @param unit
* @param <r> 返回值类型
* @param <id> id的类型,string、integer、long。。。
* @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;
// 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;
}
5. 方法4
根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
@data
public class redisdata {
// 逻辑过期时间
private localdatetime expiretime;
// 缓存预热对象
private object data;
}
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;
}
6. 方法5
根据指定的key查询缓存,并反序列化为指定类型,需要利用互斥锁解决缓存击穿问题
/**
* 自定义互斥锁,利用redis的setnx方法来表示获取锁
* @param key
* @return
*/
private boolean trylock(string key) {
boolean flag = stringredistemplate.opsforvalue().setifabsent(key, "1", lock_shop_ttl, timeunit.seconds);
return booleanutil.istrue(flag);
}
/**
* 释放互斥锁
* @param key
*/
private void unlock(string key) {
stringredistemplate.delete(key);
}
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;
}
二、调用redis工具类
- 在shopserviceimpl中,调用redis工具类
@service
public class shopserviceimpl extends serviceimpl<shopmapper, shop> implements ishopservice {
@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("店铺不存在!");
}
// 返回
return result.ok(shop);
}
}
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论