通过Mybatis的拦截机制实现自动缓存

        目前项目中用到Spring的地方很多,很多功能都能在sping中找到解决方案,正如我现在想要说的缓存实现,Spring Cache已经为我们提供了很好的解决方案,并且提供了默认实现,增加几个注解立刻就能使用,确实挺好,但是在实际使用过程中还是觉得不太方便,主要就是因为要保持缓存注解方法间的名称保持一致,在@CacheEvict中需要指定所有需要清除的缓存信息(通过name,key等属性),方法比较多、比较分散的时候维护难度就会随之提高,稍有不慎就会导致数据的不一致;故此引出今天分享讨论的一种缓存实现:“通过Mybatis的拦截机制实现自动缓存”。

        该实现的逻辑就是参考mybatis的二级缓存,针对使用sql语句查询的dao层,思路就是拦截所有的DAO层方法,解析方法对应的sql语句,对sql语句进行分类处理,分类很简单就两类,query类和update类,query主要是指select语句,update值得就是insert、delete喝update语句了;

        对于query类,方法第一次执行的时候查询数据库,将查询结果缓存到cache中,之后在此调用该方法的时候直接从cache中查询;对于update类方法,就是解析sql中的表名,当方法执行成功后,根据表名将cache中所有涉及到该表的所有存储都清除,这样当有query类的方法sql中包含该表的缓存就不存在了,需要重新从数据库查询,然后再缓存,这样也就保证了数据的一致性。

        逻辑很简单,下面就说一下实现:

        下面是通过xml方式配置sqlSessionFactory的xml片段,注释部分是常规的方式,我们不用,改为指向我们自定义的Factory类:MyBatisCacheSqlSessionFactory,指定工厂方法为:getSqlSessionFactory,这个方法需要两个参数,一个就是常规方法定义的DataSource,再有一个就是cacheService,也就是一个单独的缓存服务,我这里用的redis。

    
<!-- 工厂类
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <property name="mapperLocations">
        <array>
            <value>classpath*:mapper/*.xml</value>
        </array>
    </property>
    <property name="typeAliasesPackage" value="com.xbniao.uc.dao.po"/>
    <property name="plugins">
        <array>
            <bean class="com.github.pagehelper.PageHelper">
                <property name="properties">
                    <value>
                        dialect=mysql
                        reasonable=true
                    </value>
                </property>
            </bean>
        </array>
    </property>
</bean> -->
<bean id="sqlSessionFactory" class="com.mingjia.dao.mybatisCache.MyBatisCacheSqlSessionFactory" factory-method="getSqlSessionFactory">
    <constructor-arg name="datasource" ref="dataSource"></constructor-arg>
    <constructor-arg name="cacheService" ref="mybatisCacheService"></constructor-arg>
</bean>
<bean id="mybatisCacheService" class="com.mingjia.dao.mybatisCache.MybatisCacheService" init-method="init" >
    <property name="cacheOpen" value="${cache.isOpen}"></property>
    <property name="sentinelIp" value="${cache.redis.sentinelIp}"></property>
    <property name="sentinelMaster" value="${cache.redis.sentinelMaster}"></property>
    <property name="masterConnectionPoolSize" value="${cache.redis.masterConnectionPoolSize}"></property>
    <property name="slaveConnectionPoolSize" value="${cache.redis.slaveConnectionPoolSize}"></property>
    <property name="masterConnectionMinimumIdleSize" value="${cache.redis.masterConnectionMinimumIdleSize}"></property>
    <property name="slaveConnectionMinimumIdleSize" value="${cache.redis.slaveConnectionMinimumIdleSize}"></property>
    <property name="autoUnLockTime" value="${cache.redis.autoUnLockTime}"></property>
    <property name="connectTimeout" value="${cache.redis.connectTimeout}"></property>
</bean>

        上边是xml的配置方式,注解的方式也可以,原理都是一样的,再有上边配置的mybatisCacheService bean可以根据自己的实际情况更换,喝本文描述的缓存策略实现没有必然关系。

        下面看下Factory类的工厂方法:

public static SqlSessionFactory getSqlSessionFactory(DataSource datasource, MybatisCacheService cacheService) {
        try {
            MyBatisInterceptor myBatisCache = new MyBatisInterceptor(cacheService);
            Properties p = new Properties();
            p.setProperty("offsetAsPageNum", "true");
            p.setProperty("rowBoundsWithCount", "true");
            p.setProperty("reasonable", "true");
            myBatisCache.setProperties(p);

            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(datasource);
            sqlSessionFactoryBean.setPlugins(new Interceptor[]{myBatisCache});
            PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/mapper/*.xml"));
            Collection l = sqlSessionFactoryBean.getObject().getConfiguration().getMappedStatements();

            Set<String> allClassName=new HashSet<String>();
            for (Object m : l) {
                if (m instanceof MappedStatement) {
                    MappedStatement ms = (MappedStatement) m;
                    //System.out.println("=============="+ms.getId());
                    String sql = ms.getBoundSql(null).getSql();
                    if (StringUtils.containsIgnoreCase(sql, "select")) {
                        Statement statement = CCJSqlParserUtil.parse(sql);
                        Select selectStatement = (Select) statement;
                        TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
                        List<String> tableList = tablesNamesFinder.getTableList(selectStatement);
                        Set<String> tables = tables(tableList);
                        //存储数据库表和mapper中的方法对应关系,数据库表中的数据发生过更改,可以知道要清除哪个方法产生的缓存
                        methods(MyBatisCacheConfiguration.TABLE_METHOD, tables, ms.getId());
                    } else if (StringUtils.containsIgnoreCase(sql, "insert")) {
                        // System.out.println(sql.split("\\s+")[2]);
                    } else if (StringUtils.containsIgnoreCase(sql, "delete")) {
                        // System.out.println(sql.split("\\s+")[2]);
                    } else if (StringUtils.containsIgnoreCase(sql, "update")) {
                        // System.out.println(sql.split("\\s+")[1]);
                    }
                    //记录所有的Mapper类
                    allClassName.add(StringUtils.substring(ms.getId(),0,StringUtils.lastIndexOf(ms.getId(),'.')));
                }
            }
            //mapper中含有@MyBatisCache(disCache = true)的方法,直接查数据库
            getDisCacheMethod(MyBatisCacheConfiguration.DIS_CACHE_METHOD,allClassName);

            return sqlSessionFactoryBean.getObject();
        }catch(Exception e){
            e.printStackTrace();
        }
        return null;
    }

        工厂方法内容:1,创建自定义的mybatis拦截器mybatisCache;2,创建SqlSessionFactory,并将拦截器配置进去;3,解析mapper下所有的xml文件,将query类型的sql中的表名提取出来并和对应的mapper方法关联起来保存到内存中(MyBatisCacheConfiguration.TABLE_METHOD),此外还有一个MyBatisCacheConfiguration.DIS_CACHE_METHOD,保存的是不需要缓存的方法(通过自定义注解@MyBatisCache(disCache = true)来标示),对缓存策略没有什么影响,就不做说明了。3,最后通过sqlSessionFactoryBean的getObject方法返回实例。

        然后再看下自定义的mybatis拦截器的实现,缓存功能逻辑都在这里边了:

    public MyBatisInterceptor(MybatisCacheService cacheService) {
        this.cacheService = cacheService;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement mappedStatement = (MappedStatement) args[0];
            Object parameter = args[1];

            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            String sql = boundSql.getSql();

            String invocationMethodName = invocation.getMethod().getName();
            if (StringUtils.equals("query", invocationMethodName)) {
                //判断方法是否在非缓存集合,在则直接查询数据库
                if (contains(mappedStatement.getId())) {
                    log.info("读取数据库");
                    if (SqlUtil.getLocalPage() != null) {
                        log.info("分页拦截");
                        return pageIntercept(invocation);
                    } else {
                        log.info("普通拦截");
                        return invocation.proceed();
                    }
                } else {
                    boolean isPage = false;
                    String method = MyBatisCacheConfiguration.MYBATIS_CACHE_PREFIX+DigestUtils.md5Hex(mappedStatement.getId());

                    //方法参数的变化会自动体现到CacheKey上
                    String cacheKey = createCacheKey(mappedStatement, parameter, (RowBounds) args[2], boundSql).toString();
                    StringBuffer sb = new StringBuffer(cacheKey);
                    Object parameterObject = boundSql.getParameterObject();
                    //判断方法参数是否为空,不为空需要将参数信息写入sb,作为key的一部分
                    if (parameterObject != null) {
                        String parameterObjectType = parameterObject.getClass().getSimpleName();

                        //参数信息
                        if (SqlUtil.getLocalPage() != null) {
                            isPage = true;
                            sb.append(":").append(parameterObjectType);

                            //拦截前获得分页数据
                            sb.append(":").append("pageNum");
                            sb.append(":").append(SqlUtil.getLocalPage().getPageNum());
                            sb.append(":").append("pageSize");
                            sb.append(":").append(SqlUtil.getLocalPage().getPageSize());
                        }
                    }
                    log.info(method);
                    log.info(sb.toString());
                    String key = DigestUtils.md5Hex(sb.toString());
                    Map<String, Object> map = getCache(method);
                    if (map.get(key) == null) {
                        Object obj = null;
                        if (isPage) {
                            obj = pageIntercept(invocation);
                            setCache(method, key, parsePage((Page) obj));

                        } else {
                            obj = invocation.proceed();
                            setCache(method, key, obj);
                        }
                        log.info("读取数据库");
                        return obj;
                    } else {
                        log.info("读取缓存");
                        if (isPage) {
                            Page page = parseMap((Map<String, Object>) map.get(key));
                            if (SqlUtil.getLocalPage() != null)
                                SqlUtil.clearLocalPage();
                            return page;

                        } else {
                            return map.get(key);
                        }

                    }
                }

            } else if (StringUtils.equals("update", invocation.getMethod().getName())) {
                if (StringUtils.containsIgnoreCase(sql, "insert")) {
                    Set<String> m = MyBatisCacheConfiguration.TABLE_METHOD.get(sql.split("\\s+")[2]);
                    for (String mapName : m) {
                        delCache(mapName);
                    }
                } else if (StringUtils.containsIgnoreCase(sql, "delete")) {
                    Set<String> m = MyBatisCacheConfiguration.TABLE_METHOD.get(sql.split("\\s+")[2]);
                    for (String mapName : m) {
                        delCache(mapName);
                    }
                } else if (StringUtils.containsIgnoreCase(sql, "update")) {
                    Set<String> m = MyBatisCacheConfiguration.TABLE_METHOD.get(sql.split("\\s+")[1]);
                    for (String mapName : m) {
                        delCache(mapName);
                    }
                }
                return invocation.proceed();
            } else {
                return invocation.proceed();
            }
        } catch (Exception e) {
            e.printStackTrace();
            return invocation.proceed();
        }
    }

        主要逻辑就是对query类和update类的处理,“query”的逻辑主要是‘ else{}’部分,逻辑在最开始已经说了,就是缓存数据,之后cache中没有的才去查数据库;“update”部分处理都是一样的,就是从sql中提取出表名,根据表明从MyBatisCacheConfiguration.TABLE_METHOD中得到方法在缓存中的key,最终清除缓存。

        通过这个实现,默认就对所有的mapper方法进行了缓存(如果有不想缓存的加上@MyBatisCache(disCache = true)),不用每个方法都去添加一遍@Cacheable注解,而且不用关心name,key等属性的维护,自动维护数据的一致性;功能上和mybatis的二级缓存逻辑没啥区别,主要就是不用在xml文件中添加cache的标签了,再有就是集成了PageHelper插件,解决了分页插件和mybatis的二级缓存联合使用的数据一致性问题,对于分页数据也可以正常缓存。

        该实现很简单,功能也是相对简陋,当然我说的方式也应该可以通过spring cache的自定义方式来实现,再有对于分页插件和Mybatis的二级缓存结合使用问题的解决应该有更简单的解决办法;在此主要是抛砖引玉,希望有想法的朋友分享下好的想法。

      

       以上的描述内容为最初的实现,代码实现以及配置做了较大改动,如将缓存拦截器与分页插件分离,同时支持PageHelper4和PageHelper5;增加了guava cache的默认cache服务实现等,欢迎指教!

        再有,关于自动缓存插件和其他插件(比如分页插件)的执行顺序问题,我在“说说MyBatis插件执行顺序(PageHelper 5 问题)”进行了说明。



猜你喜欢

转载自blog.csdn.net/mingjia1987/article/details/79424272