Autoload cache framework

Autoload cache framework


For the code , please visit github for more details, the updated content QQ exchange group: 429274886, the version update will be notified in the group, and you can learn about the latest developments

Version 0.5 is already a stable version, and you can use it with confidence.

There are many caching technologies used now, such as Redis , Memcache , EhCache , etc., and even use ConcurrentHashMap or HashTable to implement caching. However, in the use of cache, everyone has their own implementation methods, most of which are directly bound to the business code. With the change of business, it is very troublesome to replace the cache scheme. Next, we will use AOP + Annotation to solve this problem, and use the automatic loading mechanism to achieve data " resident memory ".

Spring AOP has been very popular in recent years, and it is used more and more, but I personally suggest that AOP should only be used to handle some auxiliary functions (for example: the cache we will talk about next), and cannot use AOP to implement business logic, especially is in an environment where "transactions" are required.

As shown below:Alt cache framework

After AOP intercepts the request:

  1. Generate the key according to the request parameters, and we will further explain the rules for generating the key later;
  2. If it is AutoLoad, the relevant parameters are requested, encapsulated in AutoLoadTO, and placed in AutoLoadHandler.
  3. According to the key, the data is retrieved from the cache server. If the data is retrieved, the data is returned. If the data is not retrieved, the method in the DAO is executed to retrieve the data and put the data in the cache at the same time. If it is AutoLoad, update the last loading time to AutoLoadTO, and finally return the data; if it is an AutoLoad request, every time it is requested, the last request time in AutoLoadTO will be updated.
  4. In order to reduce concurrency, increase the waiting mechanism: if multiple users fetch a piece of data at the same time, let the first user fetch data from DAO first, and other users will wait for it to return, and then fetch it from the cache. Get it, and then go to DAO to get the data.

The main thing that AutoLoadHandler does: when the cache is about to expire, execute the DAO method, get the data, and put the data in the cache. In order to prevent the auto-loading queue from being too large, a capacity limit is set; at the same time, those that have not been requested by users for more than a certain period of time will also be removed from the auto-loading queue, releasing server resources to those that are really needed.

Purpose of using self-loading:

  1. Avoid unbearable database pressure due to cache invalidation during peak requests;
  2. Some time-consuming business can be realized.
  3. Use automatic loading for some very frequently used data, because when the cache of such data is invalid, it is most likely to cause excessive pressure on the server.

Distributed autoload

If the application is deployed on multiple servers, it can theoretically be considered that the automatic loading queue is performed by these servers together to complete the automatic loading task. For example, the application is deployed on two servers, A and B, and server A automatically loads data D (because the automatic loading queues of the two servers are independent, so the loading order is the same), and then a user requests data from server B D. At this time, the last loading time of data D will be updated to server B, so that server B will not load data D repeatedly.

##Usage ###1. Implement com.jarvis.cache.CacheGeterSeter Here is an example of using Redis as a cache server:

package com.jarvis.example.cache;
import ... ...
/**
 * 缓存切面,用于拦截数据并调用Redis进行缓存操作
 */
@Aspect
public class CachePointCut implements CacheGeterSeter<Serializable> {

    private static final Logger logger=Logger.getLogger(CachePointCut.class);

    private AutoLoadHandler<Serializable> autoLoadHandler;

    private static List<RedisTemplate<String, Serializable>> redisTemplateList;

    public CachePointCut() {
        autoLoadHandler=new AutoLoadHandler<Serializable>(10, this, 20000);
    }

    @Pointcut(value="execution(public !void com.jarvis.example.dao..*.*(..)) && @annotation(cache)", argNames="cache")
    public void daoCachePointcut(Cache cache) {
        logger.info("----------------------init daoCachePointcut()--------------------");
    }

    @Around(value="daoCachePointcut(cache)", argNames="pjp, cache")
    public Object controllerPointCut(ProceedingJoinPoint pjp, Cache cache) throws Exception {
        return CacheUtil.proceed(pjp, cache, autoLoadHandler, this);
    }

    public static RedisTemplate<String, Serializable> getRedisTemplate(String key) {
        if(null == redisTemplateList || redisTemplateList.isEmpty()) {
            return null;
        }
        int hash=Math.abs(key.hashCode());
        Integer clientKey=hash % redisTemplateList.size();
        RedisTemplate<String, Serializable> redisTemplate=redisTemplateList.get(clientKey);
        return redisTemplate;
    }

    @Override
    public void setCache(final String cacheKey, final CacheWrapper<Serializable> result, final int expire) {
        try {
            final RedisTemplate<String, Serializable> redisTemplate=getRedisTemplate(cacheKey);
            redisTemplate.execute(new RedisCallback<Object>() {

                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    byte[] key=redisTemplate.getStringSerializer().serialize(cacheKey);
                    JdkSerializationRedisSerializer serializer=(JdkSerializationRedisSerializer)redisTemplate.getValueSerializer();
                    byte[] val=serializer.serialize(result);
                    connection.set(key, val);
                    connection.expire(key, expire);
                    return null;
                }
            });
        } catch(Exception ex) {
            logger.error(ex.getMessage(), ex);
        }
    }

    @Override
    public CacheWrapper<Serializable> get(final String cacheKey) {
        CacheWrapper<Serializable> res=null;
        try {
            final RedisTemplate<String, Serializable> redisTemplate=getRedisTemplate(cacheKey);
            res=redisTemplate.execute(new RedisCallback<CacheWrapper<Serializable>>() {

                @Override
                public CacheWrapper<Serializable> doInRedis(RedisConnection connection) throws DataAccessException {
                    byte[] key=redisTemplate.getStringSerializer().serialize(cacheKey);
                    byte[] value=connection.get(key);
                    if(null != value && value.length > 0) {
                        JdkSerializationRedisSerializer serializer=
                            (JdkSerializationRedisSerializer)redisTemplate.getValueSerializer();
                        @SuppressWarnings("unchecked")
                        CacheWrapper<Serializable> res=(CacheWrapper<Serializable>)serializer.deserialize(value);
                        return res;
                    }
                    return null;
                }
            });
        } catch(Exception ex) {
            logger.error(ex.getMessage(), ex);
        }
        return res;
    }

    /**
     * 删除缓存
     * @param cs Class
     * @param method
     * @param arguments
     * @param subKeySpEL
     * @param deleteByPrefixKey 是否批量删除
     */
    public static void delete(@SuppressWarnings("rawtypes") Class cs, String method, Object[] arguments, String subKeySpEL,
        boolean deleteByPrefixKey) {
        try {
            if(deleteByPrefixKey) {
                final String cacheKey=CacheUtil.getDefaultCacheKeyPrefix(cs.getName(), method, arguments, subKeySpEL) + "*";
                for(final RedisTemplate<String, Serializable> redisTemplate : redisTemplateList){
                    redisTemplate.execute(new RedisCallback<Object>() {
                        @Override
                        public Object doInRedis(RedisConnection connection) throws DataAccessException {
                            byte[] key=redisTemplate.getStringSerializer().serialize(cacheKey);
                            Set<byte[]> keys=connection.keys(key);
                            if(null != keys && keys.size() > 0) {
                                byte[][] keys2=new byte[keys.size()][];
                                keys.toArray(keys2);
                                connection.del(keys2);
                            }
                            return null;
                        }
                    });
                }

            } else {
                final String cacheKey=CacheUtil.getDefaultCacheKey(cs.getName(), method, arguments, subKeySpEL);
                final RedisTemplate<String, Serializable> redisTemplate=getRedisTemplate(cacheKey);
                redisTemplate.execute(new RedisCallback<Object>() {

                    @Override
                    public Object doInRedis(RedisConnection connection) throws DataAccessException {
                        byte[] key=redisTemplate.getStringSerializer().serialize(cacheKey);

                        connection.del(key);

                        return null;
                    }
                });
            }
        } catch(Exception ex) {
            logger.error(ex.getMessage(), ex);
        }
    }

    public AutoLoadHandler<Serializable> getAutoLoadHandler() {
        return autoLoadHandler;
    }

    public void destroy() {
        autoLoadHandler.shutdown();
        autoLoadHandler=null;
    }

    public List<RedisTemplate<String, Serializable>> getRedisTemplateList() {
        return redisTemplateList;
    }

    public void setRedisTemplateList(List<RedisTemplate<String, Serializable>> redisTemplateList) {
        CachePointCut.redisTemplateList=redisTemplateList;
    }

}

As can be seen from the above code, the operation of the cache is still implemented by the business system itself. We only do some processing on the ProceedingJoinPoint intercepted by AOP.

After the java code is implemented, the next step is to perform related configuration in spring:

<aop:aspectj-autoproxy proxy-target-class="true"/>
<bean id="cachePointCut" class="com.jarvis.example.cache.CachePointCut" destroy-method="destroy">
    <property name="redisTemplateList">
        <list>
            <ref bean="redisTemplate1"/>
            <ref bean="redisTemplate2"/>
        </list>
    </property>
</bean>

Since version 0.4, the implementation of Redis' PointCut has been added, which can be used directly in Spring with aop:config :

  <bean id="autoLoadConfig" class="com.jarvis.cache.to.AutoLoadConfig">
    <property name="threadCnt" value="10" />
    <property name="maxElement" value="20000" />
    <property name="printSlowLog" value="true" />
    <property name="slowLoadTime" value="1000" />
</bean>
<bean id="cachePointCut" class="com.jarvis.cache.redis.CachePointCut" destroy-method="destroy">
  <constructor-arg ref="autoLoadConfig" />
  <property name="redisTemplateList">
    <list>
      <ref bean="redisTemplate100" />
      <ref bean="redisTemplate2" />
    </list>
  </property>
</bean>

<aop:config>
  <aop:aspect id="aa" ref="cachePointCut">
    <aop:pointcut id="daoCachePointcut" expression="execution(public !void com.jarvis.cache_example.dao..*.*(..)) &amp;&amp; @annotation(cache)" />
    <aop:around pointcut-ref="daoCachePointcut" method="controllerPointCut" />
  </aop:aspect>
</aop:config>

Through Spring configuration, it can better support the situation that different data use different cache servers.

example code

Memcache example:

<bean id="memcachedClient" class="net.spy.memcached.spring.MemcachedClientFactoryBean">
    <property name="servers" value="192.138.11.165:11211,192.138.11.166:11211" />
    <property name="protocol" value="BINARY" />
    <property name="transcoder">
        <bean class="net.spy.memcached.transcoders.SerializingTranscoder">
            <property name="compressionThreshold" value="1024" />
        </bean>
    </property>
    <property name="opTimeout" value="2000" />
    <property name="timeoutExceptionThreshold" value="1998" />
    <property name="hashAlg">
        <value type="net.spy.memcached.DefaultHashAlgorithm">KETAMA_HASH</value>
    </property>
    <property name="locatorType" value="CONSISTENT" />
    <property name="failureMode" value="Redistribute" />
    <property name="useNagleAlgorithm" value="false" />
</bean>

<bean id="cachePointCut" class="com.jarvis.cache.memcache.CachePointCut" destroy-method="destroy">
  <constructor-arg value="10" /><!-- 线程数量 -->
  <constructor-arg value="20000" /><!-- 自动加载队列容量 -->
  <property name="memcachedClient", ref="memcachedClient" />
</bean>

###2. Add the @Cache annotation before the method that needs to use the cache

package com.jarvis.example.dao;
import ... ...
public class UserDAO {
    @Cache(expire=600, autoload=true, requestTimeout=72000)
    public List<UserTO> getUserList(... ...) {
        ... ...
    }
}

##Cache Key generation

  1. Custom cache Key using Spring EL expressions: CacheUtil.getDefinedCacheKey(String keySpEL, Object[] arguments)

    例如: @Cache(expire=600, key="'goods'+#args[0]")

  2. The default method for generating a cache key: CacheUtil.getDefaultCacheKey(String className, String method, Object[] arguments, String subKeySpEL)

  • className class name

  • method method name

  • arguments parameter

  • subKeySpEL SpringEL expression

    The format of the generated Key is: {class name}.{method name}{.SpringEL expression operation result}:{Hash string of parameter value}.

    When the key value is not set in @Cache, use the default method to generate the cache key

It is recommended to use the default method of generating cache keys, which can reduce some maintenance work.

###subKeySpEL Instructions for use

According to the needs of the business, the cache keys are grouped. For example, a list of product reviews:

package com.jarvis.example.dao;
import ... ...
public class GoodsCommentDAO{
    @Cache(expire=600, subKeySpEL="#args[0]", autoload=true, requestTimeout=18000)
    public List<CommentTO> getCommentListByGoodsId(Long goodsId, int pageNo, int pageSize) {
        ... ...
    }
}

If the product ID is: 100, then the format of the generated cache key is: com.jarvis.example.dao.GoodsCommentDAO.getCommentListByGoodsId.100:xxxx In Redis, the comment list with the product ID of 100 can be deleted precisely, just execute the command: del com.jarvis.example.dao.GoodsCommentDAO.getCommentListByGoodsId.100:*

SpringEL expressions are really very convenient to use. If necessary, the expire, requestTimeout and autoload parameters in @Cache can be dynamically set with SpringEL expressions, but it becomes complicated to use, so we did not do this.

###Data real-time

In the example of the product review above, if the user posts a review, what should be done immediately?

The simpler method is to clear the data in the cache immediately after the comment is successfully posted, and that's it.

package com.jarvis.example.dao;
import ... ...
public class GoodsCommentDAO{
    @Cache(expire=600, subKeySpEL="#args[0]", autoload=true, requestTimeout=18000)
    public List<CommentTO> getCommentListByGoodsId(Long goodsId, int pageNo, int pageSize) {
        ... ...
    }
    public void addComment(Long goodsId, String comment) {
        ... ...// 省略添加评论代码
        deleteCache(goodsId);
    }
    private void deleteCache(Long goodsId) {
        Object arguments[]=new Object[]{goodsId};
        CachePointCut.delete(this.getClass(), "getCommentListByGoodsId", arguments, "#args[0]", true);
    }
}

###@Cache

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cache {

    /**
     * 缓存的过期时间,单位:秒
     */
    int expire();

    /**
     * 自定义缓存Key,如果不设置使用系统默认生成缓存Key的方法
     * @return
     */
    String key() default "";
    
    /**
     * 是否启用自动加载缓存
     * @return
     */
    boolean autoload() default false;

    /**
     * 当autoload为true时,缓存数据在 requestTimeout 秒之内没有使用了,就不进行自动加载数据,如果requestTimeout为0时,会一直自动加载
     * @return
     */
    long requestTimeout() default 36000L;
    
    /**
     * 使用SpEL,将缓存key,根据业务需要进行二次分组
     * @return
     */
    String subKeySpEL() default "";
    /**
     * 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存,例如:"#args[0]==1",当第一个参数值为1时,才进缓存。
     * @return
     */
    String condition() default "";
}

##Precautions

###1. When autoload in @Cache is set to true , the parameters of the corresponding method must be Serializable. The parameters after deep copying need to be cached in AutoLoadHandler .

###2. Only the necessary attribute values ​​are set in the parameters, and the attribute values ​​that are not used in DAO should not be set as much as possible, so as to avoid the generation of different cache keys and reduce the utilization rate of the cache. E.g:

    public CollectionTO<AccountTO> getAccountByCriteria(AccountCriteriaTO criteria)  {
        List<AccountTO> list=null;
        PaginationTO paging=criteria.getPaging();
        if(null != paging && paging.getPageNo() > 0 && paging.getPageSize() > 0) {// 如果需要分页查询,先查询总数
            criteria.setPaging(null);// 减少缓存KEY的变化,在查询记录总数据时,不用设置分页相关的属性值
            Integer recordCnt=accountDAO.getAccountCntByCriteria(criteria);
            if(recordCnt > 0) {
                criteria.setPaging(paging);
                paging.setRecordCnt(recordCnt);
                list=accountDAO.getAccountByCriteria(criteria);
            }
            return new CollectionTO<AccountTO>(list, recordCnt, criteria.getPaging().getPageSize());
        } else {
            list=accountDAO.getAccountByCriteria(criteria);
            return new CollectionTO<AccountTO>(list, null != list ? list.size() : 0, 0);
        }
    }

###3. Be aware of cases where AOP fails; for example:

    TempDAO {

        public Object a() {
            return b().get(0);
        }

        @Cache(expire=600)
        public List<Object> b(){
            return ... ...;
        }
    }

When the b method is called through new TempDAO().a(), AOP is invalid, and cache related operations cannot be performed.

###4. When automatically loading the cache, query parameter values ​​cannot be superimposed within the cache method; for example:

    @Cache(expire=600, autoload=true)
    public List<AccountTO> getDistinctAccountByPlayerGet(AccountCriteriaTO criteria) {
        List<AccountTO> list;
        int count=criteria.getPaging().getThreshold() ;
        // 查预设查询数量的10倍
        criteria.getPaging().setThreshold(count * 10);
        … …
    }

Because AutoLoadHandler caches query parameters during automatic loading, when executing automatic loading, the threshold will be multiplied by 10 each time it is executed, so that the value of the threshold will become larger and larger.

###5. What if the method return type changes?

During code refactoring, there may be cases where the return value type of the method is changed, but the parameters remain unchanged. When deploying online, data of the old data type may be retrieved from the cache, which can be handled by the following methods:

  • After going online, quickly clean up the data in the cache;
  • Add a version uniformly to the implementation class of CacheGeterSeter;
  • Add version to @Cache (not implemented).

###6. Try to use automatic loading for some time-consuming methods.

###7. Do not use the automatic loading mechanism for query conditions that change drastically. For example, the method of searching data based on the keywords entered by the user is not recommended to use automatic loading.

##How to reduce "dirty reads" in a transactional environment

  1. Do not fetch data from the cache and apply it to SQL statements that modify the data

  2. After the transaction is completed, delete the relevant cache

At the beginning of the transaction, use a ThreadLocal to record a HashSet. When the update data method is executed, encapsulate the relevant parameters to delete the cache into a Bean and put them in the HashSet. When the transaction is completed, traverse the HashSet, and then Delete the relevant cache.

In most cases, you only need to do point 1, because ensuring that the data in the database is accurate is the most important thing. Because this "dirty read" situation can only reduce the probability of occurrence, and cannot complete the solution. It is generally only possible in very high concurrency situations. Just like 12306, it tells you that there are still tickets when you check, but you may not have them when you pay at the end.

##Terms of Use

  1. Take data from the interface or database and encapsulate it in the DAO layer . There cannot be a method for interface adjustment anywhere.
  2. When the cache is automatically loaded, the query condition value cannot be superimposed (or subtracted) within the cache method, but it is allowed to set the value.
  3. Inside the DAO layer, if the @Cache method is not used, the method with @Cache cannot be called to avoid AOP failure.
  4. Because the cached key is obtained by converting the method parameters into strings, in order to avoid the generated keys being different, try to set only the necessary parameters and attributes , which is also convenient for reverse positioning .
  5. For a relatively large system, a modular design should be carried out , so that the automatic loading can be equally divided into each module.

##Why use autoloading mechanism?

First, let's think about where is the bottleneck of the system?

  1. In the case of high concurrency, the database performance is extremely poor, even if the performance of the query statement is very high; if there is no automatic loading mechanism, when the cache expires and the access peak arrives, it is easy to greatly increase the data pressure.

  2. Writing data to the cache is also much less efficient than reading data from the cache, because operations such as memory allocation are required when writing to the cache. Using automatic loading can reduce the situation of writing data to the cache at the same time, and also improve the throughput of the cache server.

  3. There are also some more time-consuming operations.

##How to reduce DAO layer concurrency

  1. use cache;
  2. Use the autoload mechanism; "writing" data tends to be less performant than reading data, and using autoload can also reduce write concurrency.
  3. When loading data from the DAO layer, a waiting mechanism is added (using the doctrine): if there are multiple requests requesting the same data at the same time, one of the requests will be asked to fetch the data first, and the other requests will wait for its data.

##Extensibility and maintainability

  1. The decoupling of cache and business logic is achieved through AOP; if you want to display data in real time, it will still be a bit coupled.
  2. It is very convenient to replace the cache server or cache implementation (for example: from Memcache to Redis);
  3. It is very convenient to increase or decrease the cache server (eg: increase the number of Redis nodes);
  4. It is very convenient to add or remove the cache, which is convenient for troubleshooting during testing;
{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324230382&siteId=291194637