这里有新鲜出炉的 Redis 教程, 程序狗速度看过来!
Redis Key-Value 数据库
Redis 是一个开源的使用 ANSI C 语言编写, 支持网络, 可基于内存亦可持久化的日志型, Key-Value 数据库, 并提供多种语言的 API.
这篇文章主要介绍了 Spring AOP 实现 Redis 缓存数据库查询的相关内容, 源码部分还是不错的, 需要的朋友可以参考下.
应用场景
我们希望能够将数据库查询结果缓存到 Redis 中, 这样在第二次做同样的查询时便可以直接从 redis 取结果, 从而减少数据库读写次数.
需要解决的问题
操作缓存的代码写在哪? 必须要做到与业务逻辑代码完全分离.
如何避免脏读? 从缓存中读出的数据必须与数据库中的数据一致.
如何为一个数据库查询结果生成一个唯一的标识? 即通过该标识 (Redis 中为 Key), 能唯一确定一个查询结果, 同一个查询结果, 一定能映射到同一个 key. 只有这样才能保证缓存内容的正确性
如何序列化查询结果? 查询结果可能是单个实体对象, 也可能是一个 List.
解决方案避免脏读
我们缓存了查询结果, 那么一旦数据库中的数据发生变化, 缓存的结果就不可用了. 为了实现这一保证, 可以在执行相关表的更新查询 (update, delete, insert) 查询前, 让相关的缓存过期. 这样下一次查询时程序就会重新从数据库中读取新数据缓存到 redis 中. 那么问题来了, 在执行一条 insert 前我怎么知道应该让哪些缓存过期呢? 对于 Redis, 我们可以使用 Hash Set 数据结构, 让一张表对应一个 Hash Set, 所有在这张表上的查询都保存到该 Set 下. 这样当表数据发生变动时, 直接让 Set 过期即可. 我们可以自定义一个注解, 在数据库查询方法上通过注解的属性注明这个操作与哪些表相关, 这样在执行过期操作时, 就能直接从注解中得知应该让哪些 Set 过期了.
为查询生成唯一标识
对于 MyBatis, 我们可以直接使用 SQL 字符串做为 key. 但是这样就必须编写基于 MyBatis 的拦截器, 从而使你的缓存代码与 MyBatis 紧紧耦合在一起. 如果哪天更换了持久层的框架, 你的缓存代码就白写了, 所以这个方案并不完美.
仔细想一想, 其实如果两次查询调用的类名, 方法名和参数值相同, 我们就可以确定这两次查询结果一定是相同的 (在数据没有变动的前提下). 因此, 我们可以将这三个元素组合成一个字符串做为 key, 就解决了标识问题.
序列化查询结果
最方便的序列化方式就是使用 JDK 自带的 ObjectOutputStream 和 ObjectInputStream. 优点是几乎任何一个对象, 只要实现了 Serializable 接口, 都用同一套代码能被序列化和反序列化. 但缺点也很致命, 那就是序列化的结果容量偏大, 在 redis 中会消耗大量内存 (是对应 JSON 格式的 3 倍左右). 那么我们只剩下 JSON 这一个选择了.
JSON 的优点是结构紧凑, 可读性强, 但美中不足的是, 反序列化对象时必须提供具体的类型参数 (Class 对象), 如果是 List 对象, 还必须提供 List 和 List 中的元素类型两种信息, 才能被正确反序列化. 这样就增加了代码的复杂度. 不过这些困难都是可以克服的, 所以我们还是选择 JSON 作为序列化存储方式.
代码写在哪
毫无疑问, 该 AOP 上场了. 在我们的例子中, 持久化框架使用的是 MyBatis, 因此我们的任务就是拦截 Mapper 接口方法的调用, 通过 Around(环绕通知) 编写以下逻辑:
方法被调用之前, 根据类名, 方法名和参数值生成 Key
通过 Key 向 Redis 发起查询
如果缓存命中, 则将缓存结果反序列化作为方法调用的返回值 , 并阻止被代理方法的调用.
如果缓存未命中, 则执行代理方法, 得到查询结果, 序列化, 用当前的 Key 将序列化结果放入 redis 中.
代码实现
因为我们要拦截的是 Mapper 接口方法, 因此必须命令 spring 使用 JDK 的动态代理而不是 cglib 的代理. 为此, 我们需要做以下配置:
<!-- 当 proxy-target-class 为 false 时使用 JDK 动态代理 -->
<!-- 为 true 时使用 cglib -->
<!-- cglib 无法拦截接口方法 -->
<aop:aspectj-autoproxy proxy-target-class="false" />
然后定义两个标注在接口方法上的注解, 用于传递类型参数:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisCache {
Class type();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisEvict {
Class type();
}
注解的使用方式如下:
// 表示该方法需要执行 (缓存是否命中 ? 返回缓存并阻止方法调用 : 执行方法并缓存结果) 的缓存逻辑
@RedisCache(type = JobPostModel.class)
JobPostModel selectByPrimaryKey(Integer id);
// 表示该方法需要执行清除缓存逻辑
@RedisEvict(type = JobPostModel.class)
int deleteByPrimaryKey(Integer id);
AOP 的代码如下:
@Aspect
@Component
public class RedisCacheAspect {
public static final Logger infoLog = LogUtils.getInfoLogger();
@Qualifier("redisTemplateForString")
@Autowired
StringRedisTemplate rt;
/**
* 方法调用前, 先查询缓存. 如果存在缓存, 则返回缓存数据, 阻止方法调用;
* 如果没有缓存, 则调用业务方法, 然后将结果放到缓存中
* @param jp
* @return
* @throws Throwable
*/
@Around("execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.select*(..))" +
"|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.get*(..))" +
"|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.find*(..))" +
"|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.search*(..))")
public Object cache(ProceedingJoinPoint jp) throws Throwable {
// 得到类名, 方法名和参数
String clazzName = jp.getTarget().getClass().getName();
String methodName = jp.getSignature().getName();
Object[] args = jp.getArgs();
// 根据类名, 方法名和参数生成 key
String key = genKey(clazzName, methodName, args);
if (infoLog.isDebugEnabled()) {
infoLog.debug("生成 key:{}", key);
}
// 得到被代理的方法
Method me = ((MethodSignature) jp.getSignature()).getMethod();
// 得到被代理的方法上的注解
Class modelType = me.getAnnotation(RedisCache.class).type();
// 检查 redis 中是否有缓存
String value = (String)rt.opsForHash().get(modelType.getName(), key);
// result 是方法的最终返回结果
Object result = null;
if (null == value) {
// 缓存未命中
if (infoLog.isDebugEnabled()) {
infoLog.debug("缓存未命中");
}
// 调用数据库查询方法
result = jp.proceed(args);
// 序列化查询结果
String json = serialize(result);
// 序列化结果放入缓存
rt.opsForHash().put(modelType.getName(), key, json);
} else {
// 缓存命中
if (infoLog.isDebugEnabled()) {
infoLog.debug("缓存命中, value = {}", value);
}
// 得到被代理方法的返回值类型
Class returnType = ((MethodSignature) jp.getSignature()).getReturnType();
// 反序列化从缓存中拿到的 json
result = deserialize(value, returnType, modelType);
if (infoLog.isDebugEnabled()) {
infoLog.debug("反序列化结果 = {}", result);
}
}
return result;
}
/**
* 在方法调用前清除缓存, 然后调用业务方法
* @param jp
* @return
* @throws Throwable
*/
@Around("execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.insert*(..))" +
"|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.update*(..))" +
"|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.delete*(..))" +
"|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.increase*(..))" +
"|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.decrease*(..))" +
"|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.complaint(..))" +
"|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.set*(..))")
public Object evictCache(ProceedingJoinPoint jp) throws Throwable {
// 得到被代理的方法
Method me = ((MethodSignature) jp.getSignature()).getMethod();
// 得到被代理的方法上的注解
Class modelType = me.getAnnotation(RedisEvict.class).type();
if (infoLog.isDebugEnabled()) {
infoLog.debug("清空缓存:{}", modelType.getName());
}
// 清除对应缓存
rt.delete(modelType.getName());
return jp.proceed(jp.getArgs());
}
/**
* 根据类名, 方法名和参数生成 key
* @param clazzName
* @param methodName
* @param args 方法参数
* @return
*/
protected String genKey(String clazzName, String methodName, Object[] args) {
StringBuilder sb = new StringBuilder(clazzName);
sb.append(Constants.DELIMITER);
sb.append(methodName);
sb.append(Constants.DELIMITER);
for (Object obj : args) {
sb.append(obj.toString());
sb.append(Constants.DELIMITER);
}
return sb.toString();
}
protected String serialize(Object target) {
return JSON.toJSONString(target);
}
protected Object deserialize(String jsonString, Class clazz, Class modelType) {
// 序列化结果应该是 List 对象
if (clazz.isAssignableFrom(List.class)) {
return JSON.parseArray(jsonString, modelType);
}
// 序列化结果是普通对象
return JSON.parseObject(jsonString, clazz);
}
}
这样我们就完成了数据库查询缓存的实现.
UPDATE:
最好为 Hash Set 设置一个过期时间, 这样即使缓存策略有误 (导致读出脏数据), 过期时间到了以后依然可以与数据库保持同步:
// 序列化结果放入缓存
rt.execute(new RedisCallback < Object > () {@Override public Object doInRedis(RedisConnection redisConn) throws DataAccessException {
// 配置文件中指定了这是一个 String 类型的连接
// 所以这里向下强制转换一定是安全的
StringRedisConnection conn = (StringRedisConnection) redisConn;
// 判断 hash 名是否存在
// 如果不存在, 创建该 hash 并设置过期时间
if (false == conn.exists(hashName)) {
conn.hSet(hashName, key, json);
conn.expire(hashName, Constants.HASH_EXPIRE_TIME);
} else {
conn.hSet(hashName, key, json);
}
return null;
}
});
总结
本文关于 Spring AOP 实现 Redis 缓存数据库查询源码的介绍就到这里, 希望对大家有所帮助. 感兴趣的朋友可以参阅本站其他相关专题, 在此非常感谢大家对 PHPERZ 的支持!
来源: http://www.phperz.com/article/18/0130/353337.html