Spring AOP + Redis cache database query, aopredis
Application scenarios
We hope to cache the database query results to Redis so that the results can be directly obtained from redis for the second query, thus reducing the number of database reads and writes.
Solutions to problems to be solved avoid dirty reading
The query results are cached. Once the data in the database changes, the cached results are unavailable. To ensure this, You can execute the update query of the relevant table (update
,delete
,insert
) Before the query, make the related cache expire. In this way, the program will re-read the new data from the database and cache it to redis during the next query. The problem arises.insert
How do I know which caches should expire before? For Redis, we can useHash Set
Data structure, so that a table corresponds toHash Set
All queries on this table are saved to this Set. In this way, when the table data changes, the Set can expire directly. We can customize an annotation to indicate the tables associated with the operation through the annotation attribute in the database query method, so that when performing the expiration operation, you can directly find out which sets should expire from the annotations.
Generate a unique identifier for the query
ForMyBatis
, We can directly useSQL
Stringkey
. However, you must writeMyBatis
So that your cache code andMyBatis
Tightly coupled. If you change the persistence layer framework one day, your cache code will be written in white, so this solution is not perfect.
If the class name, method name, and parameter value of the two queries are the same, we can determine that the results of these two queries must be the same (without any data changes ). Therefore, we can combine these three elements into a string as the key to solve the identification problem.
Serialized query results
The most convenient serialization method is to use the built-in JDKObjectOutputStream
AndObjectInputStream
. The advantage is that almost any object, as long asSerializable
Interface, allSame set of codeCan be serialized or deserialized. But the disadvantage is also fatal, that is, the serialization result capacity is too large, which will consume a large amount of memory in redis (about three times the corresponding JSON format ). Then we only have the JSON option.
The advantage of JSON is its compact structure and high readability, but the disadvantage is that the specific type parameters must be provided for deserialization objects (Class
Object ).List
Object, you must also provideList
And List element types in order to be correctly deserialized. This increases the complexity of the Code. However, these difficulties can be overcome, so we still choose JSON as the serialization storage method.
Code written?
There is no doubt thatAOP
Playing now. In our example, the persistence framework usesMyBatis
Therefore, our task is to interceptMapper
Interface method call, throughAround (surround notification)
Write the following logic:
Code Implementation
Because what we want to intercept isMapper
Therefore, you must use the dynamic proxy insteadcglib
. To do this, we need to make the following Configuration:
<! -- Use JDK dynamic proxy when proxy-target-class is false --> <! -- Use cglib --> <! -- Cglib cannot intercept interface methods --> <aop: aspectj-autoproxy proxy-target-class = "false"/>
Define two annotations on the Interface Method to pass type parameters:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documentedpublic @interface RedisCache { Class type();}
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface RedisEvict { Class type();}
The usage of annotations is as follows:
// Indicates that this method needs to be executed (is the cache hit? Return the cache and block method calls: Execute the method and cache the result.) The cache logic @ RedisCache (type = JobPostModel. class) JobPostModel selectByPrimaryKey (Integer id );
// Indicates that the method needs to clear the cache logic @ RedisEvict (type = JobPostModel. class) int deleteByPrimaryKey (Integer id );
The AOP code is as follows:
@ Aspect @ Componentpublic class RedisCacheAspect {public static final Logger infoLog = LogUtils. getInfoLogger (); @ Qualifier ("redisTemplateForString") @ Autowired StringRedisTemplate rt;/*** query the cache before calling the method. If a cache exists, the cached data is returned to prevent method calls. * if no cache exists, the business method is called, then 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 *(..)) ") public Object cache (ProceedingJoinPoint jp) throws Throwable {// obtain the class name, method name, and parameter String clazzName = jp. getTarget (). getClass (). getName (); String methodName = jp. getSignature (). getName (); Object [] args = jp. getArgs (); // generate key String key = genKey (clazzName, methodName, args) based on the class name, method name, and parameter; if (infoLog. isDebugEnabled () {infoLog. debug ("generate key :{}", key) ;}// obtain the Method me = (MethodSignature) jp. getSignature ()). getMethod (); // obtain the Class modelType = me. getAnnotation (RedisCache. class ). type (); // check whether redis has cached String value = (String) rt. opsForHash (). get (modelType. getName (), key); // result is the final result returned by the method. Object result = null; if (null = value) {// if (infoLog. isDebugEnabled () {infoLog. debug ("cache miss");} // call the database query method result = jp. proceed (args); // serialize the query result String json = serialize (result); // put the serialization result into the cache rt. opsForHash (). put (modelType. getName (), key, json);} else {// cache hit if (infoLog. isDebugEnabled () {infoLog. debug ("cache hit, value = {}", value) ;}// Class returnType = (MethodSignature) jp. getSignature ()). getReturnType (); // deserialization the json result obtained from the cache = deserialize (value, returnType, modelType); if (infoLog. isDebugEnabled () {infoLog. debug ("deserialization result = {}", result) ;}} return result;}/*** clear the cache before calling the method, then call the Business Method * @ 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 {// obtain the proxy Method me = (MethodSignature) jp. getSignature ()). getMethod (); // obtain the Class modelType = me. getAnnotation (RedisEvict. class ). type (); if (infoLog. isDebugEnabled () {infoLog. debug ("Clear cache: {}", modelType. getName ();} // clear the corresponding cache rt. delete (modelType. getName (); return jp. proceed (jp. getArgs ());} /*** generate key * @ param clazzName * @ param methodName * @ param args method parameter * @ 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) {// the serialization result should be List Object if (clazz. isAssignableFrom (List. class) {return JSON. parseArray (jsonString, modelType);} // The serialized result is a normal object return JSON. parseObject (jsonString, clazz );}}
This completes the implementation of database query cache.
Copyright Disclaimer: This article is an original article by the blogger and cannot be reproduced without the permission of the blogger.