谈谈个人网站的建立(八)—— 缓存的使用
欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。
一、概述
1.1 缓存介绍
系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
缓存常用语:
数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
可查看Redis实战(一) 使用缓存合理性
1.2 本站缓存架构
从没有使用缓存,到使用mybatis缓存,然后使用了ehcache,再然后是mybatis+redis缓存。
二、Mybatis缓存
2.1 mybatis一级缓存
Mybatis的一级缓存是指Session回话级别的缓存,也称作本地缓存。一级缓存的作用域是一个SqlSession。Mybatis默认开启一级缓存。在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。Mybatis 默认支持一级缓存,不需要在配置文件中配置。
我们来查看一下源码的类图,具体的源码分析简单概括一下:SqlSession实际上是使用PerpetualCache来维护的,PerpetualCache中定义了一个HashMap<k,v>来进行缓存。
(1)当会话开始时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;
(2)对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。如果命中,则返回结果,如果没有命中,则去数据库中查询,再将结果存储到cache中,最后返回结果。如果执行增删改,则执行flushCacheIfRequired方法刷新缓存。
(3)当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
2.2 mybatis二级缓存
Mybatis的二级缓存是指mapper映射文件,为Application应用级别的缓存,生命周期长。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。在同一个namespace下的mapper文件中,执行相同的查询SQL。实现二级缓存,关键是要对Executor对象做文章,Mybatis给Executor对象加上了一个CachingExecutor,使用了设计模式中的装饰者模式,
2.2.1 MyBatis二级缓存的划分
MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下:
a.为每一个Mapper分配一个Cache缓存对象(使用
b.多个Mapper共用一个Cache缓存对象(使用
2.2.2 二级缓存的开启
在mybatis的配置文件中添加:
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
然后再需要开启二级缓存的mapper.xml中添加(本站使用了LRU算法,时间为120000毫秒):
<cache eviction="LRU"
type="org.apache.ibatis.cache.impl.PerpetualCache"
flushInterval="120000"
size="1024"
readOnly="true"/>
2.2.3 使用第三方支持的二级缓存的实现
MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在
- MyBatis自身提供的缓存实现;
- 用户自定义的Cache接口实现;
- 跟第三方内存缓存库的集成;
具体的实现,可参照:SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置
MyBatis中一级缓存和二级缓存的组织如下图所示(图片来自深入理解mybatis原理):
2.3 Mybatis在分布式环境下脏读问题
(1)如果是一级缓存,在多个SqlSession或者分布式的环境下,数据库的写操作会引起脏数据,多数情况可以通过设置缓存级别为Statement来解决。
(2)如果是二级缓存,虽然粒度比一级缓存更细,但是在进行多表查询时,依旧可能会出现脏数据。
(3)Mybatis的缓存默认是本地的,分布式环境下出现脏读问题是不可避免的,虽然可以通过实现Mybatis的Cache接口,但还不如直接使用集中式缓存如Redis、Memcached好。
下面将介绍使用Redis集中式缓存在个人网站的应用。
三、Redis缓存
Redis运行于独立的进程,通过网络协议和应用交互,将数据保存在内存中,并提供多种手段持久化内存的数据。同时具备服务器的水平拆分、复制等分布式特性,使得其成为缓存服务器的主流。为了与Spring更好的结合使用,我们使用的是Spring-Data-Redis。此处省略安装过程和Redis的命令讲解。
3.1 Spring Cache
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。
下面是Spring Cache常用的注解:
(1)@Cacheable
@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
属性 | 介绍 | 例子 |
---|---|---|
value | 缓存的名称,必选 | @Cacheable(value=”mycache”) 或者@Cacheable(value= |
key | 缓存的key,可选,需要按照SpEL表达式填写 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,key=”#userName”) |
(2)@CachePut
@CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
属性 | 介绍 | 例子 |
---|---|---|
value | 缓存的名称,必选 | @Cacheable(value=”mycache”) 或者@Cacheable(value= |
key | 缓存的key,可选,需要按照SpEL表达式填写 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,key=”#userName”) |
(3)@CacheEvict
@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空
属性 | 介绍 | 例子 |
---|---|---|
value | 缓存的名称,必选 | @Cacheable(value=”mycache”) 或者@Cacheable(value= |
key | 缓存的key,可选,需要按照SpEL表达式填写 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,key=”#userName”) |
allEntries | 是否清空所有缓存内容,默认为false | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false | @CachEvict(value=”testcache”,beforeInvocation=true) |
但是有个问题:
Spring官方认为:缓存过期时间由各个产商决定,所以并不提供缓存过期时间的注解。所以,如果想实现各个元素过期时间不同,就需要自己重写一下Spring cache。
3.2 引入包
一般是Spring常用的包+Spring data redis的包,记得注意去掉所有冲突的包,之前才过坑,Spring-data-MongoDB已经有SpEL的库了,和自己新引进去的冲突,搞得我以为自己是配置配错了,真是个坑,注意,开发过程中一定要去除掉所有冲突的包!!!
3.3 ApplicationContext.xml
需要启用缓存的注解开关,并配置好Redis。序列化方式也要带上,否则会碰到幽灵bug。
<!-- 启用缓存注解开关,此处可自定义keyGenerator -->
<cache:annotation-driven/>
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${host}"/>
<property name="port" value="${port}"/>
<property name="password" value="${password}"/>
<property name="database" value="${redis.default.db}"/>
<property name="timeout" value="${timeout}"/>
<property name="poolConfig" ref="jedisPoolConfig"/>
<property name="usePool" value="true"/>
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
<!-- 序列化方式 建议key/hashKey采用StringRedisSerializer。 -->
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
<property name="hashValueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
</bean>
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg name="redisOperations" ref="redisTemplate" />
<!--统一过期时间-->
<property name="defaultExpiration" value="${redis.defaultExpiration}"/>
</bean>
3.5 自定义KeyGenerator
在分布式系统中,很容易存在不同类相同名字的方法,如A.getAll(),B.getAll(),默认的key(getAll)都是一样的,会很容易产生问题,所以,需要自定义key来实现分布式环境下的不同。
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object o, Method method, Object... objects) {
StringBuilder sb = new StringBuilder();
sb.append(o.getClass().getName());
sb.append(".");
sb.append(method.getName());
for (Object obj : objects) {
sb.append(obj.toString());
}
return sb.toString();
}
}
之后,存储的key就变为:com.myblog.service.impl.BlogServiceImpl.getBanner。
3.4 添加注解
在所需要的方法上添加注解,比如,首页中的那几张幻灯片,每次进入首页都需要查询数据库,这里,我们直接放入缓存里,减少数据库的压力,还有就是那些热门文章,访问量比较大的,也放进数据库里。
@Override
@Cacheable(value = "getBanner", keyGenerator = "customKeyGenerator")
public List<Blog> getBanner() {
return blogMapper.getBanner();
}
@Override
@Cacheable(value = "getBlogDetail", key = "'blogid'.concat(#blogid)")
public Blog getBlogDetail(Integer blogid) {
Blog blog = blogMapper.selectByPrimaryKey(blogid);
if (blog == null) {
return null;
}
Category category = categoryMapper.selectByPrimaryKey(blog.getCategoryid());
blog.setCategory(category);
List<Tag> tags = tagMapper.getTagByBlogId(blog.getBlogid());
blog.setTags(tags.size() > 0 ? tags : null);
asyncService.updatebloghits(blogid);//异步更新阅读次数
logger.info("没有走缓存");
return blog;
}
3.5 测试
我们调用一个getBlogDetail(获取博客详情)100次来对比一下时间。连接的数据库在深圳,本人在广州,还是有那么一丢丢的网路延时的。
public class SpringTest {
@Test
public void init() {
ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:spring-test.xml");
IBlogService blogService = (IBlogService) ctx.getBean("blogService");
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
blogService.getBlogDetail(615);
}
System.out.println(System.currentTimeMillis() - startTime);
}
}
为了做一下对比,我们同时使用mybatis自身缓存来进行测试。
3.6 实验结果
统计出结果如下:
没有使用任何缓存(mybatis一级缓存没有关闭):18305
使用远程Redis缓存:12727
使用Mybatis缓存:6649
使用本地Redis缓存:5818
由结果看出,缓存的使用大大较少了获取数据的时间。
部署进个人博客之后,redis已经缓存的数据:
3.7 分页的数据怎么办
个人网站中共有两个栏目,一个是技术杂谈,另一个是生活笔记,每点击一次栏目的时候,会根据页数从数据库中查询数据,百度了下,大概有三种方法:
(1)以页码作为Key,然后缓存整个页面。
(2)分条存取,只从数据库中获取分页的文章ID序列,然后从service(缓存策略在service中实现)中获取。
第一种,由于使用了第三方的插件PageHelper,分页获取的话会比较麻烦,同时整页缓存对内存压力也蛮大的,毕竟服务器只有2g。第二条实现方式简单,缺陷是依旧需要查询数据库,想了想还是放弃了。缓存的初衷是对请求频繁又不易变的数据,实际使用中很少会反复的请求同一页的数据(查询条件也相同),当然对数据中某些字段做缓存还是有必要的。
四、如何解决脏读?
对于文章来说,内容是不经常更新的,没有涉及到缓存一致性,但是对于文章的阅读量,用户每点击一次,就应该更新浏览量的。对于文章的缓存,常规的设计是将文章存储进数据库中,然后读取的时候放入缓存中,然后将浏览量以文章ID+浏览量的结构实时的存入redis服务器中。本站当初设计不合理,直接将浏览量作为一个字段,用户每点击一次的时候就异步更新浏览量,但是此处没有更新缓存,如果手动更新缓存的话,基本上每点击一次都得执行更新操作,同样也不合理。所以,目前本站,你们在页面上看到的浏览量和数据库中的浏览量并不是一致的。有兴趣的可以点击我的网站玩玩~~
五、题外话
兄弟姐妹们啊,个人网站只是个小项目,纯属为了学习而用的,文章可以看看,但是,就不要抓取了吧。。。。一个小时抓取6万次宝宝心脏真的受不了,虽然服务器一切都还稳定==
个人网站:http://www.wenzhihuai.com
个人网站源码,希望能给个star:https://github.com/Zephery/newblog
参考:
1.《深入理解mybatis原理》 MyBatis的一级缓存实现详解
2.《深入理解mybatis原理》 MyBatis的二级缓存的设计原理
3.聊聊Mybatis缓存机制
4.Spring思维导图
5.SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置
6.《深入分布式缓存:从原理到实践》
![](https://upyuncdn.wenzhihuai.com/201809090952591097120145.png)
您的资助是我最大的动力!
金额随意,欢迎来赏!