模板方法模式实现Redis缓存查询简化
简述
在高并发场景下查询缓存时很容易出现缓存击穿(本文针对单机没有使用分布式锁),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,因此查询缓存需要进行加锁,但这种代码每次写多了很烦,而且容易写错,因此本文采用模板方法模式简化缓存查询及并发处理
快速开始
Redis Dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis Service
public interface RedisService {
/**
* 存储数据
*/
void set(String key, String value);
/**
* SET key value EX seconds,设置key-value和超时时间(秒)
* 存储数据并设置过期时间
*
* @param key cache key
* @param value cache value
* @param expire expire time,time unit is seconds
*/
void set(String key, String value, long expire);
/**
* 存储数据并设置过期时间通知指定过期时间的单位
* @param key 数据的键
* @param value 值
* @param expire 过期时间
* @param timeUnit 过期时间的单位
*/
void set(String key, String value, long expire, TimeUnit timeUnit);
/**
* 获取数据
*/
String get(String key);
/**
* 设置超期时间
* @param key
* @param expire 过期时间
* @return 设置成功返回true, 否则返回false
*/
boolean expire(String key, long expire);
/**
* 删除数据
*/
void remove(String key);
/**
* 自增操作
* @param delta 自增步长
*/
Long increment(String key, long delta);
}
Redis Service Impl
@Service
public class RedisServiceImpl implements RedisService {
private static final long DEFAULT_EXPIRE_SECONDS = 10;
private final StringRedisTemplate redisTemplate;
@Autowired
public RedisServiceImpl(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value, DEFAULT_EXPIRE_SECONDS, TimeUnit.MINUTES);
}
@Override
public void set(String key, String value, long expire) {
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
@Override
public void set(String key, String value, long expire, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, expire, timeUnit);
}
@Override
public String get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
public boolean expire(String key, long expire) {
Boolean result = redisTemplate.expire(key, expire, TimeUnit.SECONDS);
if(result == null) {
return false;
}
return result;
}
@Override
public void remove(String key) {
redisTemplate.delete(key);
}
@Override
public Long increment(String key, long delta) {
return redisTemplate.opsForValue().increment(key,delta);
}
}
Redis Key Util:
public class RedisUtils {
/**
* 主数据系统标识
*/
private static final String KEY_PREFIX = "halo";
/**
* 分割字符,默认[:],使用:可用于rdm分组查看
*/
private static final String KEY_SPLIT_CHAR = ":";
/**
* redis的key键规则定义
* @param module 模块名称
* @param service service名称
* @param args 参数..
* @return key
*/
public static String keyBuilder(String module, String service, String... args) {
return keyBuilder(null, module, service, args);
}
/**
* redis的key键规则定义
* @param module 模块名称
* @param service service名称
* @param objStr 对象.toString()
* @return key
*/
public static String keyBuilder(String module, String service, String objStr) {
return keyBuilder(null, module, service, new String[]{objStr});
}
/**
* redis的key键规则定义
* @param prefix 项目前缀
* @param module 模块名称
* @param service service名称
* @param objStr 对象.toString()
* @return key
*/
public static String keyBuilder(String prefix, String module, String service, String objStr) {
return keyBuilder(prefix, module, service, new String[]{objStr});
}
/**
* redis的key键规则定义
* @param prefix 项目前缀
* @param module 模块名称
* @param service 方法名称
* @param args 参数..
* @return key
*/
public static String keyBuilder(String prefix, String module, String service, String... args) {
// 项目前缀
if (prefix == null) {
prefix = KEY_PREFIX;
}
StringBuilder key = new StringBuilder(prefix);
// KEY_SPLIT_CHAR 为分割字符
key.append(KEY_SPLIT_CHAR).append(module).append(KEY_SPLIT_CHAR).append(service);
for (String arg : args) {
key.append(KEY_SPLIT_CHAR).append(arg);
}
return key.toString();
}
/**
* redis的key键规则定义
* @param cacheEnum 枚举对象
* @param objStr 对象.toString()
* @return key
*/
public static String keyBuilder(CacheEnum cacheEnum, String objStr) {
return keyBuilder(cacheEnum.getPrefix(), cacheEnum.getModule(), cacheEnum.getService(), objStr);
}
}
Cache Enum
@Data
@AllArgsConstructor
public enum CacheEnum {
CMS_POST("halo", "cms", "PostService", "文章缓存");
//other enum....
/**
* key前缀
*/
private String prefix;
/**
* 模块名称
*/
private String module;
/**
* service名称
*/
private String service;
/**
* 描述
*/
private String desc;
}
CacheTemplate:
@Component
public class CacheTemplate {
@Autowired
private RedisService redisService;
public <T> T findCache(String key, Class<T> clazz, LoadCallback<T> loadCallback) {
String cacheValueString = redisService.get(key);
if(StringUtils.isEmpty(cacheValueString)) {
synchronized (this) {
cacheValueString = redisService.get(key);
if(ObjectUtils.isEmpty(cacheValueString)) {
T dataOfMysql = loadCallback.load();
// 如果为null也放入缓存,过期时间为1分钟
if (dataOfMysql == null) {
redisService.set(key, "null", 60L);
} else {
// 否则放入缓存,过期时间为10分钟
redisService.set(key, JSON.toJSONString(dataOfMysql));
}
return dataOfMysql;
}
return JSON.parseObject(cacheValueString, clazz);
}
} else {
return JSON.parseObject(cacheValueString, clazz);
}
}
public <T> T findCache(String key, TypeReference<T> typeReference, LoadCallback<T> loadCallback) {
String cacheValueString = redisService.get(key);
if(StringUtils.isEmpty(cacheValueString)) {
synchronized (this) {
cacheValueString = redisService.get(key);
if(ObjectUtils.isEmpty(cacheValueString)) {
T dataOfMysql = loadCallback.load();
// 如果为null也放入缓存,过期时间为1分钟
if (dataOfMysql == null) {
redisService.set(key, "null", 60L);
} else {
// 否则放入缓存,过期时间为10分钟
redisService.set(key, JSON.toJSONString(dataOfMysql));
}
return dataOfMysql;
}
return JSON.parseObject(cacheValueString, typeReference);
}
} else {
return JSON.parseObject(cacheValueString, typeReference);
}
}
public <T> T findCache(String key, long expire, TypeReference<T> typeReference, LoadCallback<T> loadCallback) {
String cacheValueString = redisService.get(key);
if(StringUtils.isEmpty(cacheValueString)) {
synchronized (this) {
cacheValueString = redisService.get(key);
if(ObjectUtils.isEmpty(cacheValueString)) {
T dataOfMysql = loadCallback.load();
// 如果为null也放入缓存,过期时间为1分钟
if (dataOfMysql == null) {
redisService.set(key, "null", 60L);
} else {
// 否则放入缓存,过期时间为10分钟
redisService.set(key, JSON.toJSONString(dataOfMysql), expire);
}
return dataOfMysql;
}
return JSON.parseObject(cacheValueString, typeReference);
}
} else {
return JSON.parseObject(cacheValueString, typeReference);
}
}
}
LoadCallback
@FunctionalInterface
public interface LoadCallback<T> {
/**
* 从数据库查询数据逻辑,查询后的结果会被优先放入缓存
* @return 返回查询结果对象
*/
T load();
}
Example:
@Slf4j
@Service
public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostService {
private final PostRepository postRepository;
private final CacheTemplate cacheTemplate;
@Autowired
public PostServiceImpl(PostRepository postRepository,
CacheTemplate cacheTemplate) {
this.postRepository = postRepository;
this.cacheTemplate = cacheTemplate;
}
public Post getBy(PostStatus status, String slug) {
// 构建缓存key
String cacheKey = RedisUtils.keyBuilder(CacheEnum.CMS_POST, "status:" + status.getValue() + ":slug:" + slug);
return cacheTemplate.findCache(cacheKey, Post.class, ()->{
// 从数据库查询数据的逻辑,这里可以简化为一行写
return super.getBy(status, slug);
});
}
}