Application Scenarios
We want to be able to cache database query results into Redis so that the results can be taken directly from Redis when the same query is made the second time, thus reducing the number of database reads and writes.
Issues that need to be addressed
- Where is the code where the operation cache is written? Must be completely detached from the business logic code.
- How to avoid dirty reading? The data that is read from the cache must be consistent with the data in the database.
- How do I generate a unique identity for a database query result? That is, the identity (in Redis
Key
), can uniquely determine a query result, the same query results, must be able to map to the same key
. Only in this way can we guarantee the correctness of the cached content
- How do I serialize the results of a query? The result of the query may be a single Entity object, or it may be one
List
.
Solution to avoid dirty reads
We cache the results of the query, so once the data in the database changes, the cached results are not available. To achieve this guarantee, you can update
delete
have the relevant cache expire before you perform an update query (,) query on the related table insert
. The next time you query, the program will re-read the new data cache from the database into Redis. So the question is, insert
how do I know which cache should expire before executing one? For Redis, we can use Hash Set
data structures to have one table corresponding to one Hash Set
, and all queries on this table are saved under that set. This allows the set to expire directly when the table data changes. We can customize an annotation to indicate which tables are related to this action on the database query method through the attributes of the annotation, so that when you perform an expiration operation, you will be able to tell directly from the annotations which set should expire.
Generate a unique identity for a query
For MyBatis
, we can use SQL
strings directly as key
. However, you must write an interceptor based on it so that MyBatis
your cache code is MyBatis
tightly coupled. If you change the frame of the persistence layer one day, your cache code will be written in white, so this scheme is not perfect.
Think about it, in fact, if the class name, method name, and parameter value of the two query call are the same, we can make sure that the results of these two queries must be the same (if the data is not changed). Therefore, we can combine these three elements into a single string as key, which solves the identity problem.
Serialization of query Results
The most convenient way to serialize is to use the and of the JDK itself ObjectOutputStream
ObjectInputStream
. The advantage is that almost any object, as long as the interface is implemented Serializable
, can be serialized and deserialized using the same set of code . But the disadvantage is also fatal, that is, serialization results in large capacity, in Redis will consume a lot of memory (the corresponding JSON format of about 3 times times). So we only have the JSON option left.
The advantage of JSON is that it is compact and readable, but the drawback is that when deserializing an object, you must provide a specific type parameter ( Class
object), and if it is an List
object, you must also provide two types of List
information, the element type in the list, to be correctly deserialized. This increases the complexity of the code. However, these difficulties can be overcome, so we still choose JSON as a serialized storage method.
Where is the code written?
No doubt it AOP
's time to play. In our case, the persistence framework uses MyBatis
that, so our task is to intercept Mapper
the invocation of the interface method by writing the Around(环绕通知)
following logic:
- method is called before it is generated according to the class name, method name, and parameter value.
Key
- by
Key
Redis
initiating a query to
- If the cache is hit, the cached result is deserialized as the return value of the method call, and the call to the proxy method is blocked.
- If the cache misses, execute the proxy method, get the query results, serialize, and put the
Key
serialized results into Redis with the current.
Code implementation
Because we want to intercept the Mapper
interface method, we must command spring to use the dynamic proxy of the JDK instead cglib
of the proxy. To do this, we need to make the following configuration:
<!-- 当proxy-target-class为false时使用JDK动态代理 --><!-- 为true时使用cglib --><!-- cglib无法拦截接口方法 --><aop:aspectj-autoproxy proxy-target-class="false" />
Then define two annotations on the interface method to pass the type parameters:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documentedpublic @interface RedisCache { Class type();}
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface RedisEvict { Class type();}
Annotations are used in the following ways:
// 表示该方法需要执行 (缓存是否命中 ? 返回缓存并阻止方法调用 : 执行方法并缓存结果)的缓存逻辑@RedisCache(type = JobPostModel.class)JobPostModel selectByPrimaryKey(Integer id);
// 表示该方法需要执行清除缓存逻辑@RedisEvict(type = JobPostModel.class)int deleteByPrimaryKey(Integer id);
The code for AOP is as follows:
@Aspect@Component Public class rediscacheaspect { Public Static FinalLogger InfoLog = Logutils.getinfologger ();@Qualifier("Redistemplateforstring")@AutowiredStringredistemplate RT;/** * Before the method call, query the cache first. If there is a cache, the cached data is returned and the method call is blocked; * If there is no cache, call the business method and put the result in the cache * @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* (..)) ") PublicObjectCache(Proceedingjoinpoint JP)throwsThrowable {//Get the class name, method name, and parametersString clazzname = Jp.gettarget (). GetClass (). GetName (); String methodName = Jp.getsignature (). GetName (); object[] args = Jp.getargs ();generate key based on class name, method name, and parameterString key = Genkey (Clazzname, methodName, args);if(Infolog.isdebugenabled ()) {Infolog.debug ("Generate key:{}", key); }//Get the Proxy methodMethod me = ((methodsignature) jp.getsignature ()). GetMethod ();//Get an annotation on the method being proxiedClass Modeltype = me.getannotation (Rediscache.class). Type ();//Check if there is a cache in RedisString value = (string) rt.opsforhash (). Get (Modeltype.getname (), key);//result is the result of the method's final returnObject result =NULL;if(NULL= = value) {//Cache misses if(Infolog.isdebugenabled ()) {Infolog.debug ("Cache Misses"); }//Call database Query methodresult = Jp.proceed (args);//Serialization of query resultsString JSON = serialize (result);//Serialize results into cacheRt.opsforhash (). Put (Modeltype.getname (), key, JSON); }Else{//Cache hit if(Infolog.isdebugenabled ()) {Infolog.debug ("Cache hit, value = {}", value); }//Get the return value type of the proxy methodClass returntype = ((methodsignature) jp.getsignature ()). Getreturntype ();//Deserialize JSON obtained from the cacheresult = Deserialize (value, ReturnType, Modeltype);if(Infolog.isdebugenabled ()) {Infolog.debug ("deserialization result = {}", result); } }returnResult }/** * Clears the cache before the method call and then calls the business method * @param JP * @return * @throws throwable */< /c0> @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* (..)) ") PublicObjectEvictcache(Proceedingjoinpoint JP)throwsThrowable {//Get the Proxy methodMethod me = ((methodsignature) jp.getsignature ()). GetMethod ();//Get an annotation on the method being proxiedClass Modeltype = me.getannotation (Redisevict.class). Type ();if(Infolog.isdebugenabled ()) {Infolog.debug ("Empty cache: {}", Modeltype.getname ()); }//clear the corresponding cacheRt.delete (Modeltype.getname ());returnJp.proceed (Jp.getargs ()); }/** * Generates key based on class name, method name, and parameter * @param clazzname * @param methodName * @param args Method parameter * @return * / protectedStringGenkey(String clazzname, String methodName, object[] args) {StringBuilder SB =NewStringBuilder (Clazzname); Sb.append (Constants.delimiter); Sb.append (MethodName); Sb.append (Constants.delimiter); for(Object Obj:args) {Sb.append (obj.tostring ()); Sb.append (Constants.delimiter); }returnSb.tostring (); }protectedStringSerialize(Object target) {returnJson.tojsonstring (target); }protectedObjectDeserialize(String jsonstring, Class Clazz, class Modeltype) {//Serialization result should be a list object if(Clazz.isassignablefrom (List.class)) {returnJson.parsearray (jsonstring, Modeltype); }//Serialization result is a normal object returnJson.parseobject (jsonstring, clazz); }}
This completes the implementation of the database query cache.
Copyright NOTICE: This article for Bo Master original article, without Bo Master permission not reproduced.
Spring AOP + Redis Cache database Query