Fork me on GitHub

谈谈个人网站的建立(八)—— 缓存的使用

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

一、概述

1.1 缓存介绍

系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
缓存常用语:
数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
可查看Redis实战(一) 使用缓存合理性

1.2 本站缓存架构

从没有使用缓存,到使用mybatis缓存,然后使用了ehcache,再然后是mybatis+redis缓存。

![](http://image.wenzhihuai.com/images/20180121034503.png)
步骤: (1)用户发送一个请求到nginx,nginx对请求进行分发。 (2)请求进入controller,service,service中查询缓存,如果命中,则直接返回结果,否则去调用mybatis。 (3)mybatis的缓存调用步骤:二级缓存->一级缓存->直接查询数据库。 (4)查询数据库的时候,mysql作了主主备份。

二、Mybatis缓存

2.1 mybatis一级缓存

Mybatis的一级缓存是指Session回话级别的缓存,也称作本地缓存。一级缓存的作用域是一个SqlSession。Mybatis默认开启一级缓存。在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。Mybatis 默认支持一级缓存,不需要在配置文件中配置。

![](http://image.wenzhihuai.com/images/20180120015614.png)

我们来查看一下源码的类图,具体的源码分析简单概括一下: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对象也一并释放掉。

![](http://image.wenzhihuai.com/images/20180120022427.png)

2.2 mybatis二级缓存

Mybatis的二级缓存是指mapper映射文件,为Application应用级别的缓存,生命周期长。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。在同一个namespace下的mapper文件中,执行相同的查询SQL。实现二级缓存,关键是要对Executor对象做文章,Mybatis给Executor对象加上了一个CachingExecutor,使用了设计模式中的装饰者模式,

![](http://image.wenzhihuai.com/images/20180120030017.png)

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实现类配置在节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached、Redis的集成,总之,使用MyBatis的二级缓存有三个选择:

  1. MyBatis自身提供的缓存实现;
  2. 用户自定义的Cache接口实现;
  3. 跟第三方内存缓存库的集成;
    具体的实现,可参照:SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置

MyBatis中一级缓存和二级缓存的组织如下图所示(图片来自深入理解mybatis原理):

![](http://image.wenzhihuai.com/images/20180120120015.png)

2.3 Mybatis在分布式环境下脏读问题

(1)如果是一级缓存,在多个SqlSession或者分布式的环境下,数据库的写操作会引起脏数据,多数情况可以通过设置缓存级别为Statement来解决。
(2)如果是二级缓存,虽然粒度比一级缓存更细,但是在进行多表查询时,依旧可能会出现脏数据。
(3)Mybatis的缓存默认是本地的,分布式环境下出现脏读问题是不可避免的,虽然可以通过实现Mybatis的Cache接口,但还不如直接使用集中式缓存如Redis、Memcached好。

下面将介绍使用Redis集中式缓存在个人网站的应用。

三、Redis缓存

Redis运行于独立的进程,通过网络协议和应用交互,将数据保存在内存中,并提供多种手段持久化内存的数据。同时具备服务器的水平拆分、复制等分布式特性,使得其成为缓存服务器的主流。为了与Spring更好的结合使用,我们使用的是Spring-Data-Redis。此处省略安装过程和Redis的命令讲解。

![](http://image.wenzhihuai.com/images/20180119110640.png)

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已经缓存的数据:

![](http://image.wenzhihuai.com/images/20180120052757.png)

3.7 分页的数据怎么办

个人网站中共有两个栏目,一个是技术杂谈,另一个是生活笔记,每点击一次栏目的时候,会根据页数从数据库中查询数据,百度了下,大概有三种方法:
(1)以页码作为Key,然后缓存整个页面。
(2)分条存取,只从数据库中获取分页的文章ID序列,然后从service(缓存策略在service中实现)中获取。
第一种,由于使用了第三方的插件PageHelper,分页获取的话会比较麻烦,同时整页缓存对内存压力也蛮大的,毕竟服务器只有2g。第二条实现方式简单,缺陷是依旧需要查询数据库,想了想还是放弃了。缓存的初衷是对请求频繁又不易变的数据,实际使用中很少会反复的请求同一页的数据(查询条件也相同),当然对数据中某些字段做缓存还是有必要的。

四、如何解决脏读?

对于文章来说,内容是不经常更新的,没有涉及到缓存一致性,但是对于文章的阅读量,用户每点击一次,就应该更新浏览量的。对于文章的缓存,常规的设计是将文章存储进数据库中,然后读取的时候放入缓存中,然后将浏览量以文章ID+浏览量的结构实时的存入redis服务器中。本站当初设计不合理,直接将浏览量作为一个字段,用户每点击一次的时候就异步更新浏览量,但是此处没有更新缓存,如果手动更新缓存的话,基本上每点击一次都得执行更新操作,同样也不合理。所以,目前本站,你们在页面上看到的浏览量和数据库中的浏览量并不是一致的。有兴趣的可以点击我的网站玩玩~~

五、题外话

兄弟姐妹们啊,个人网站只是个小项目,纯属为了学习而用的,文章可以看看,但是,就不要抓取了吧。。。。一个小时抓取6万次宝宝心脏真的受不了,虽然服务器一切都还稳定==

![](http://image.wenzhihuai.com/images/20180119044345.png)

个人网站http://www.wenzhihuai.com
个人网站源码,希望能给个starhttps://github.com/Zephery/newblog

参考:
1.《深入理解mybatis原理》 MyBatis的一级缓存实现详解
2.《深入理解mybatis原理》 MyBatis的二级缓存的设计原理
3.聊聊Mybatis缓存机制
4.Spring思维导图
5.SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置
6.《深入分布式缓存:从原理到实践》

posted @ 2018-01-22 08:51  ZepheryWen  阅读(2214)  评论(16编辑  收藏  举报