76

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

 6 years ago
source link: https://www.cnblogs.com/w1570631036/p/8317948.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

作者:Zephery
个人网址:http://www.wenzhihuai.com
本文为作者原创,转载请注明出处:https://www.cnblogs.com/w1570631036/p/8317948.html


1.1 缓存介绍
1.2 本站缓存架构
2.1 mybatis一级缓存
2.2 mybatis二级缓存
2.2.1 MyBatis二级缓存的划分
2.2.2 二级缓存的开启
2.2.3 使用第三方支持的二级缓存的实现
2.3 Mybatis在分布式环境下脏读问题
3.1 Spring Cache
3.2 引入包
3.3 ApplicationContext.xml
3.5 自定义KeyGenerator
3.4 添加注解
3.5 测试
3.6 实验结果
3.7 分页的数据怎么办

欢迎访问我的网站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={”cache1”,”cache2”}
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={”cache1”,”cache2”}
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={”cache1”,”cache2”}
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.《深入分布式缓存:从原理到实践》


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK