简述 {#简述}
在高并发场景下查询缓存时很容易出现缓存击穿(本文针对单机没有使用分布式锁),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,因此查询缓存需要进行加锁,但这种代码每次写多了很烦,而且容易写错,因此本文采用模板方法模式简化缓存查询及并发处理
快速开始 {#快速开始}
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 &lt;T&gt; T findCache(String key, Class&lt;T&gt; clazz, LoadCallback&lt;T&gt; 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, &quot;null&quot;, 60L); } else { // 否则放入缓存,过期时间为10分钟 redisService.set(key, JSON.toJSONString(dataOfMysql)); }
return dataOfMysql; } return JSON.parseObject(cacheValueString, clazz); }
} else { return JSON.parseObject(cacheValueString, clazz); }
}
public &lt;T&gt; T findCache(String key, TypeReference&lt;T&gt; typeReference, LoadCallback&lt;T&gt; 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 &lt;T&gt; T findCache(String key, long expire, TypeReference&lt;T&gt; typeReference, LoadCallback&lt;T&gt; 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, &amp;quot;null&amp;quot;, 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, ()-&gt;{ // 从数据库查询数据的逻辑,这里可以简化为一行写 return super.getBy(status, slug); }); }
}