欢迎访问个人网站http://www.wenzhihuai.com/ 。感谢,若是能够,但愿能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。html
系统的性能指标通常包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程当中,咱们有可能在一次数据库会话中,执行屡次查询条件彻底相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,若是是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提升性能。
缓存经常使用语:
数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
可查看Redis实战(一) 使用缓存合理性java
从没有使用缓存,到使用mybatis缓存,而后使用了ehcache,再而后是mybatis+redis缓存。
mysql
Mybatis的一级缓存是指Session回话级别的缓存,也称做本地缓存。一级缓存的做用域是一个SqlSession。Mybatis默认开启一级缓存。在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操做,则SqlSession的缓存清空。Mybatis 默认支持一级缓存,不须要在配置文件中配置。
nginx
咱们来查看一下源码的类图,具体的源码分析简单归纳一下: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对象也一并释放掉。
git
Mybatis的二级缓存是指mapper映射文件,为Application应用级别的缓存,生命周期长。二级缓存的做用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis须要手动设置启动二级缓存。在同一个namespace下的mapper文件中,执行相同的查询SQL。实现二级缓存,关键是要对Executor对象作文章,Mybatis给Executor对象加上了一个CachingExecutor,使用了设计模式中的装饰者模式,
github
MyBatis并非简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,便是Mapper级别的,即每个Mapper均可以拥有一个Cache对象,具体以下:
a.为每个Mapper分配一个Cache缓存对象(使用
b.多个Mapper共用一个Cache缓存对象(使用
在mybatis的配置文件中添加:算法
<settings> <!--开启二级缓存--> <setting name="cacheEnabled" value="true"/> </settings>
而后再须要开启二级缓存的mapper.xml中添加(本站使用了LRU算法,时间为120000毫秒):spring
<cache eviction="LRU" type="org.apache.ibatis.cache.impl.PerpetualCache" flushInterval="120000" size="1024" readOnly="true"/>
MyBatis对二级缓存的设计很是灵活,它本身内部实现了一系列的Cache缓存实现类,并提供了各类缓存刷新策略如LRU,FIFO等等;另外,MyBatis还容许用户自定义Cache接口实现,用户是须要实现org.apache.ibatis.cache.Cache接口,而后将Cache实现类配置在
MyBatis中一级缓存和二级缓存的组织以下图所示(图片来自深刻理解mybatis原理):
(1)若是是一级缓存,在多个SqlSession或者分布式的环境下,数据库的写操做会引发脏数据,多数状况能够经过设置缓存级别为Statement来解决。
(2)若是是二级缓存,虽然粒度比一级缓存更细,可是在进行多表查询时,依旧可能会出现脏数据。
(3)Mybatis的缓存默认是本地的,分布式环境下出现脏读问题是不可避免的,虽然能够经过实现Mybatis的Cache接口,但还不如直接使用集中式缓存如Redis、Memcached好。
下面将介绍使用Redis集中式缓存在我的网站的应用。
Redis运行于独立的进程,经过网络协议和应用交互,将数据保存在内存中,并提供多种手段持久化内存的数据。同时具有服务器的水平拆分、复制等分布式特性,使得其成为缓存服务器的主流。为了与Spring更好的结合使用,咱们使用的是Spring-Data-Redis。此处省略安装过程和Redis的命令讲解。
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。
通常是Spring经常使用的包+Spring data redis的包,记得注意去掉全部冲突的包,以前才过坑,Spring-data-MongoDB已经有SpEL的库了,和本身新引进去的冲突,搞得我觉得本身是配置配错了,真是个坑,注意,开发过程当中必定要去除掉全部冲突的包!!!
须要启用缓存的注解开关,并配置好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>
在分布式系统中,很容易存在不一样类相同名字的方法,如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。
在所须要的方法上添加注解,好比,首页中的那几张幻灯片,每次进入首页都须要查询数据库,这里,咱们直接放入缓存里,减小数据库的压力,还有就是那些热门文章,访问量比较大的,也放进数据库里。
@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; }
咱们调用一个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自身缓存来进行测试。
统计出结果以下:
没有使用任何缓存(mybatis一级缓存没有关闭):18305 使用远程Redis缓存:12727 使用Mybatis缓存:6649 使用本地Redis缓存:5818
由结果看出,缓存的使用大大较少了获取数据的时间。
部署进我的博客以后,redis已经缓存的数据:
我的网站中共有两个栏目,一个是技术杂谈,另外一个是生活笔记,每点击一次栏目的时候,会根据页数从数据库中查询数据,百度了下,大概有三种方法:
(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.《深刻分布式缓存:从原理到实践》