冥加

语言是窗户,不然就是墙


  • 首页

  • 分类

  • 标签

  • 归档

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

发表于 2018-02-28 | 分类于 技能

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

该实现的逻辑就是参考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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 
<!-- 工厂类
<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类的工厂方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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拦截器的实现,缓存功能逻辑都在这里边了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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的二级缓存结合使用问题的解决应该有更简单的解决办法;在此主要是抛砖引玉,希望有想法的朋友分享下好的想法。

写demo的时候pagehelper用的v4,后这对v5的重新设计也做了相应的修改,欢迎指教:
https://github.com/mingjia-vip/MyBatisCache

HEXO个人博客搭建2

发表于 2017-11-15 | 分类于 技能

之前用Hexo搭建了自己的个人博客,发布到了Github Pages上,这么费劲搭建起来应该都是想show一下吧,接下来就是要做SEO配置了,好让google,百度等搜索引擎能够索引到,让网友们能够搜索到我们的博文。
但是gitbub做了防爬处理,你发现你的博文根本就没有收录,怎么判断有没有被收录呢?在搜索引擎中搜索一下:
site:mingjia-vip.github.io
图:百度收录验证.png
图:google收录验证.png
结果就是未收录,不过没关系,以百度为例,已经告诉我们了(红色标记),你不自觉那我就主动呗,咱们自己去提交网站;对于google这里就不说了,需要梯子,流程几乎一样。

不废话,开始:
1,注册一个百度账号,然后点击上图中提示的“提交网址”,然后按照下图说明添加网站并审核:
图:添加网站.png
需要为网站设置所属的领域
图: 设置站点的领域.png
接下来就是验证你的所有权了,三种方式,最简单的是第三种CNAME方式,但是需要自己域名,过;然后最简单的就是文件验证方式了:
图:网站验证.png
按照说明,首先下载html文件放到本地Hexo目录的/public目录中,然后依次执行生成静态网页和发布命令,这样验证文件就上传到github上了,或者通过git命令或其他git工具将文件上传到gitbub工程的根目录也是一样的,然后按照提示“点这里”访问刚刚上传上去的验证文件,如果可以打开标示已经成功上传,最后点击完成验证就可以了,等几个小时之后百度验证没有问题就会通过验证,这样我们的网站就被添加到了百度。(google也是一样的,而且验证方式也是这三种,谁抄谁的啊?哈哈)

2,还没完,网站虽然收录了,但是为了更好的被搜索引擎爬取,还需要创建网站地图文件,这个hexo有插件,直接敲命令,很简单:
命令:
$ npm install hexo-generator-sitemap –save
$ npm install hexo-generator-baidu-sitemap –save

然后修改hexo根目录下在_config.yml文件,确保url和添加的网站一致,并且添加网站地图的文件名和路径:
    url: https://mingjia-vip.github.io

    # 自动生成sitemap
    sitemap: 
      path: sitemap.xml
    baidusitemap: 
      path: baidusitemap.xml

配置完成后,重新创建静态网页:
$ hexo g
这个时候,在source文件夹下会看到多出两个文件:sitemap.xml和baidusitemap.xml,这就是网站地图文件。
到此就可以等待搜索引擎抓取或者配置自动推送或者主动推送了。

3,自动推送配置:(以Next主题为例,其他类似)
进入主题目录,修改主题配置文件,将baidu_push设置为true
$ cd themes/next/
$ vi _config.yml (将baidu_push设置为true)
查看百度推送脚本:
$ cd themes/next/layout/_third-party/seo
$ more baidu-push.swig
查看脚本内容和百度推送手册中的是否一致:
图:自动推送.png
图:自动推送1.png

在此生成静态页面,并发布到github上
$ hexo g
$ hexo d
这样子的话每次访问博客中的页面会自动向百度提交。

好,这就这么多,更多推送的操作可以看看百度和google的指导手册,接下来就是等几天看看能不能在百度和google搜索到了。
由于有网上的教程说按照这种方式在百度上还是搜索不到(配置完几天后),又有的说在coding pages中可以,所以之前的博客网站在coding上也部署了一套,coding pages和github pagese功能相同,所以操作大家也可以尝试部署一下,主要还有个好处,就是coding的账号可以创建几个私有库,当然是免费的!同样通过上述的操作将网站添加到了百度。

接下俩就是不耐烦的等待啦,吼吼

HEXO搭建个人博客

发表于 2017-11-13 | 分类于 技能

网上通过hexo搭建个人博客的教程很多,刚刚也通过网上的指导搭建了自己的个人博客,既然搭建好了就先来post一篇,记录一下搭建的过程。

搭建完成后感觉还是比较简单的,但是在跟着搭建的过程中总有很多疑问,究其原因就是因为没有提前了解相关的”知识点“(哈哈,最近看吐槽大会看多了),在记录前先说明一下:

Github Pages

在很多的教程中都要求要注册个github的账号,这是为什么呢?这就引出了“Github Pages”,不过在说她之前先来说下github是干啥的,哎呀,开发的人应该都知道吧,借用百度百科的描述:gitHub是一个面向开源及私>有软件项目的托管平台,因为只支持git 作为唯一的版本库格式进行托管,故名gitHub,说白了就是一个远程代码仓促,开发者注册github账号后可以将自己的代码提交上去,这就不多说了,接下来就要说这个“GithubPages”是干啥的了,简单说就是为用户提供了个人网站的服务功能;通常情况下搭建个人网站,都需要自己购买
域名,自己购买服务器,自己部署nginx、tomcat等web服务,自己发布网站数据等,现在GithubPages服务就相当于为我们提供了域名(当然你也可以用自己买的),提供了服务器,提供了web服务,我们只需要发布网页
就可以了,意不意外!惊不惊喜!是不是很贴心!!哈哈,当然这个功能只支持静态网站,那意境足够了,这不就是我们要的吗!
在后边的记录中会说到GithubPages的使用。

Hexo

她是一个基于NodeJs创建的静态博客程序,可以快速的创建静态网页,并且可以一键发布到github上,哈哈是不是跟GithubPages特别合,完美!
既然是基于NodeJs,所以在使用前先要有node环境,后边也会记录。

git bash

这个就不多解释了,git客户端,既然要使用github,通过git命令和githup仓库同步。很多教程中都是window环境,需要下载windows版本的gitbash,我用的centos虚拟机,所以稍有不同,但是git命令都是一样的,不用担心。既然Hexo能将静态网页发布到github上,当然也是要使用到她的,后边同样会有记录。

马么好,说完“知识点”,我们就开始吧:(哦对,再次说下我搭建环境时CentOS7minimal)

一,搭建node环境

我提前下载node的源码包,由于很久之前搭建的,当时的版本是v6.9.2,下载地址:http://nodejs.cn/download/
命令:
$ yum install gcc gcc-c++
$ tar -zxvf node-v6.9.2.tar.gz
$ cd node-v6.9.2/
$ ./configure
$ make
$ make install
验证:
$ node -v
$ npm -v

验证能输入相应的版本信息说明安装成功

二,安装git

命令:
$ yum install git
验证:
$ git –version
验证能输入相应的版本信息说明安装成功

三,安装Hexo,建站

按照官网的来:https://hexo.io/zh-cn/docs/index.html
安装命令:
$ npm install -g hexo-cli
安装 Hexo 完成后,请执行下列命令,Hexo 将会在指定文件夹中新建所需要的文件:
$ hexo init “博客目录名称”
$ cd “博客目录名称”
$ npm install
“博客目录名称”可以随便起,如:ming_blogs等,只是指定创建的博客目录的名称,没什么影响。
新建完成后,指定文件夹的目录如下:
.
├── _config.yml
├── package.json
├── scaffolds
├── source
| ├── _drafts
| └── _posts
└── themes
新创建一篇博客(还是在”博客目录名称”目录中):
$ hexo new post 测试

此时在”博客目录名称”/source/_posts/ 下就会看到测试.md,内容如下:

1
2
3
4
5
6
---
title: 测试1
date: 2018-01-26 10:53:04
tags:
---


vi这个文件,增加内容后如下:
1
2
3
4
5
6
7
8
---
title: 测试1
date: 2018-01-26 10:53:04
tags:
---
### this is a test blog
## 第一个测试博文


这样,第一篇简单的博文就创建好了。

接下来测试一下,执行如下命令会在本机4000端口启动一个web服务
$ hexo server
服务启动后,可浏览器访问 http://ip地址:4000 就能看到博客网站效果。

测试没有问题,创建静态网页,执行如下命令:
$ hexo generate
此时在”博客目录名称”/public/ 下就会创建出静态文件。

到此建站文件都已经准备好了。(不知道流水账记的清不清楚 哈哈)

四,发布

将Hexo生成的网站文件上传到Github(github pages)
1,这个时候就用到github账号了,没有的可以先去注册一下,网址也贴出来吧:https://github.com
打开Github首页,登录后新建一个仓库,需要注意的来了,仓库名称一定要写成:账号名.github.io,比如我的帐号是mingjia-vip,仓库名称应该是:mingjia-vip.github.io 。

2,仓库创建好后,回到本地环境,让hexo使用git发布网站:
为hexo安装git插件:
命令:
$ npm install hexo-deployer-git –save
修改hexo发布配置,修改文件:”博客目录名称”/_config.yml
命令:
$ cd “博客目录名称”
$ vi _config.yml
在文件的最后是发布相关的配置,改成如下:

# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
  type: git
  repo: https://github.com/mingjia-vip/账号名.github.io.git
  branch: master

3,发布:
命令:
$ hexo deploy
直接执行应该会失败,会提示之行如下两个命令:
$ git config –global user.email “github注册邮箱”
$ git config –global user.name “github注册账号名”
执行完后,再次执行 hexo deploy 就不会报错了,提示要求输入github密码,输入后就ok啦。
网上很多教程还要配置SSH,这里就不麻烦了,直接账号密码(啊 记流水账好累!)

到这儿,该做的都已经完成了,接下来就是见证奇迹的时刻(啊这么说好土),浏览器访问:https://账号名.github.io …比如我的博客地址(啊哈哈)https://mingjia-vip.github.io

First

发表于 2017-11-01 | 分类于 生活

博客,就是孤芳自赏

12
桂斌

桂斌

希望世界和平

14 日志
4 分类
32 标签
© 2018 桂斌