粗解缓存
2018-01-30 本文已影响14人
uzip柚子皮
缓存
一. 概念
1.1 客户端开发者眼中的缓存
对于客户端开发者来说,每一次向服务器请求数据,返回结果后会将体积比较大的数据(例如:图片,视频等)保存到客户端上(手机),下次再访问时不需要去服务器请求下载数据,而直接显示本地数据。
流程:
查找本地是否存在内容---》如果存在就直接显示
---》如果不存在就去网络上下载并保存到本地
1.2 服务器开发者眼中的缓存
对于服务器开发者来说,数据库一般是整个项目中最大瓶颈所在,它能提供的读写是有限的,而用户的请求数是不可以估量的。如何减少数据库的读写是每一个服务器开发者需要考虑的事情。目前最好的解决方案就是采用缓存的方式。所以这里的缓存是指将数据库中的数据直接放到内存中,然后用户请求直接访问内存而尽量少去访问数据库,以减少数据库的读写。
二. 特点
2.1 优点
1. 能够提高数据的访问效率
2. 相对于单库数据临时吞吐量来说,缓存所占的内存容量理论上来说是无限的。
3. 可以通过多线程访问。(但是会出现数据安全问题)
对于缓存来说,往往只是提供查询功能。
2.2 缺点
1. 内存容量毕竟还是有限的,所以数据不可能全部放到内存中,那么需要筛选数据。
2. 会出现数据安全的问题。
3. 会出现脏读
对于数据库来说:
脏读:是指读取了其他事务未提交的数据。
不可重复读:是指在同一个事务中两次查询同一条记录得到的结果不同。
幻读:是指在同一个事务中两次查询多条记录,记录的条数不同。
事务的隔离级别:
TRANSACTION_NONE 不使用事务。
TRANSACTION_READ_UNCOMMITTED 允许脏读。
TRANSACTION_READ_COMMITTED 防止脏读,最常用的隔离级别,并且是大多数数据库的默认隔离级别,会出现不可重复读和幻读。
TRANSACTION_REPEATABLE_READ 可以防止脏读和不可重复读,会出现幻读。
TRANSACTION_SERIALIZABLE 可以防止脏读,不可重复读取和幻读,(事务串行化)会降低数据库的效率
Spring中事务的传播性:
1. PROPAGATION_REQUIRED: 如果存在一个事务,则支持当前事务。如果没有事务则开启
2. PROPAGATION_SUPPORTS: 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行
3. PROPAGATION_MANDATORY: 强制要求以事务处理。如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
4. PROPAGATION_REQUIRES_NEW: 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
5. PROPAGATION_NOT_SUPPORTED: 总是非事务地执行,并挂起任何存在的事务。
6. PROPAGATION_NEVER: 总是非事务地执行,如果存在一个活动事务,则抛出异常
三. 缓存的分类(持久化框架中)
3.1 一级缓存
一级缓存一般是指session级别的缓存。随着session的关闭而消失。所以这种缓存存在的时间不会很久,一般不会造成太大的内存消耗,所以缺点几乎可以忽略。所以各大框架都默认对一级缓存进行支持,并且基本都不能取消。
例如:
Hibernate在一个事务里多次update一条记录,会合并。
多次查询只有第一次会去数据库查询,后面的都会查内存中的数据。
注意:
get和load区别:
如果没有查询到数据:get会返回null,而load会抛出异常。
在项目中,返回null只能知道结果没有找到;而抛出异常,在调用的时候可以根据异常的类型或异常的消息来识别结果没有找到的原因。
3.2 二级缓存
二级缓存是SessionFactory级别的缓存。这种缓存只会随着服务器的关闭而消失。所以需要对缓存进行管理。
二级缓存会带来数据库服务器的性能的提升,但是缺点也很明显,所以持久层框架默认不会开启二级缓存,需要手动管理。
而通常来说持久层框架并不会自己去实现二级缓存,需要依赖第三方库去实现。
实现数据库缓存的第三方库的比较:
Ehcache:
简单、轻量级、体积小
Memcache:
数据是key-value形式,能够支持多线程(建议线程数不超过CPU的数量)。
redis:
也是key-value形式,但是具有丰富的数据类型。支持持久化。支持主从复制。甚至于可以当数据库使用。
RDB和AOF:
Redis DataBase(简称RDB)
Append-only file (简称AOF)
淘汰策略:
重大风险:穿透和雪崩
MongoDB:
更侧重于数据库。能够作为缓存使用。支持丰富的查询方式。
Redis的特点:
Redis中数据存储模式有2种:cache-only,persistence;
cache-only即只做为“缓存”服务,不持久数据,数据在服务终止后将消失,此模式下也将不存在“数据恢复”的手段,是一种安全性低/效率高/容易扩展的方式;
persistence即为内存中的数据持久备份到磁盘文件,在服务重启后可以恢复,此模式下数据相对安全。
对于persistence持久化存储,Redis提供了两种持久化方法:
Redis DataBase(简称RDB)
Append-only file (简称AOF)
RDB是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。
优点:使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能
缺点:RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
Append-only file,将“操作 + 数据”以格式化指令的方式追加到操作日志文件的尾部,在append操作返回后(已经写入到文件或者即将写入),才进行实际的数据变更,“日志文件”保存了历史所有的操作过程;当server需要数据恢复时,可以直接replay此日志文件,即可还原所有的操作过程。AOF相对可靠,它和mysql中bin.log、apache.log、zookeeper中txn-log简直异曲同工。AOF文件内容是字符串,非常容易阅读和解析。
优点:可以保持更高的数据完整性,如果设置追加file的时间是1s,如果redis发生故障,最多会丢失1s的数据;且如果日志写入不完整支持redis-check-aof来进行日志修复;AOF文件没被rewrite之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的flushall)。
缺点:AOF文件比RDB文件大,且恢复速度慢。
数据淘汰策略:
redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
redis 提供 6种数据淘汰策略通过maxmemory-policy设置策略:
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
缓存穿透:
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
如何避免:
1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
2:对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。
缓存雪崩:
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
如何避免:
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
3:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
如果要解决穿透和雪崩的问题,最好还是架一个分布式的缓存系统。
1. 什么样的数据适合存放到二级缓存中?
1) 很少被修改的数据
2) 不是很重要的数据,允许出现偶尔并发的数据
3) 不会被并发访问的数据
2、 不适合存放到第二级缓存的数据?
1) 经常被修改的数据
2) 财务数据,绝对不允许出现并发
3) 与其他应用共享的数据。
四. 二级缓存的使用
4.1 Hibernate + Ehcache
使用EHcache的步骤:
1、导入第三方库
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>5.2.10.Final</version>
</dependency>
2、在hibernate.cfg.xml中添加对应的二级缓存的配置
<!-- 配置二级缓存 -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
<!-- hibernate3的二级缓存配置 -->
<!-- <property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property> -->
<!-- 开启查询缓存 -->
<property name="hibernate.cache.use_query_cache">true</property>
3、添加ehcache.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!--如果缓存中的对象存储超过指定的缓存数量的对象存储的磁盘地址-->
<diskStore path="D:\ehcache"/>
<!--
name:缓存名称。
maxElementsInMemory:缓存最大个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间,最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时 间无穷大。
overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
maxElementsOnDisk:硬盘最大缓存个数。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU。你可以设置为 FIFO或是LFU。
clearOnFlush:内存数量最大时是否清除。
-->
<!-- 默认cache:如果没有对应的特定区域的缓存,就使用默认缓存 -->
<defaultCache maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"/>
<!-- 指定区域cache:通过name指定,name对应到Hibernate中的区域名即可-->
<!--cache name="com.wl.test.model.Student"
eternal="false"
maxElementsInMemory="100"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200"
overflowToDisk="false">
</cache-->
</ehcache>
4、在需要缓存的实体类上添加cache
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import javax.persistence.*;
@Entity
@Table(name = "product")
@Cache(usage= CacheConcurrencyStrategy.READ_ONLY)
public class Product implements java.io.Serializable{}
5、在查询的时候加上setCachable(true)
Product product1 = session1
.createQuery("from Product where name = :name",
Product.class)
.setParameter("name", "迷你挖")
.setCacheable(true)
.getSingleResult();
注意:如果 Set 需要缓存,需要在 Set 上单独加 @Cache 注解
如果有多次查询 Query,每一次 Query 都需要加上链 .setCacheable(true),加上了之后才会去 ehcache 缓存中读取数据,否则仍然是以 session 一级缓存的规则读取数据。
4.2 Mybatis + Redis + Spring
使用Redis的步骤:
1、安装redis服务器,并启动服务器
redis-server.exe redis.windows.conf
2、使用客户端测试redis服务器是否正常启动。
redis-cli.exe
redis基本命令:
set key value 设置一个键值
get key 根据key获得value
del key 删除一个键值
flushall 清空缓存
3、准备一个使用SpringMVC+MyBatis的工程。
4、添加jedis、spring-data-redis两个jar到pom.xml
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.4.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
5、添加mybatis.xml配置setting信息
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置mybatis的缓存,延迟加载等等一系列属性 -->
<settings>
<!-- 全局映射器启用缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 查询时,关闭关联对象即时加载以提高性能 -->
<setting name="lazyLoadingEnabled" value="false"/>
<!-- 对于未知的SQL查询,允许返回不同的结果集以达到通用的效果 -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 允许使用列标签代替列名 -->
<setting name="useColumnLabel" value="true"/>
<!-- 不允许使用自定义的主键值(比如由程序生成的UUID 32位编码作为键值),数据表的PK生成策略将被覆盖 -->
<setting name="useGeneratedKeys" value="false"/>
<!-- 给予被嵌套的resultMap以字段-属性的映射支持 FULL,PARTIAL -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 对于批量更新操作缓存SQL以提高性能 BATCH,SIMPLE -->
<!-- <setting name="defaultExecutorType" value="BATCH" /> -->
<!-- 数据库超过25000秒仍未响应则超时 -->
<!-- <setting name="defaultStatementTimeout" value="25000" /> -->
<!-- Allows using RowBounds on nested statements -->
<setting name="safeRowBoundsEnabled" value="false"/>
<!-- Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names aColumn. -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT
local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession. -->
<setting name="localCacheScope" value="SESSION"/>
<!-- Specifies the JDBC type for null values when no specific JDBC type was provided for the parameter. Some drivers require specifying the column JDBC type but others work with generic values
like NULL, VARCHAR or OTHER. -->
<setting name="jdbcTypeForNull" value="OTHER"/>
<!-- Specifies which Object's methods trigger a lazy load -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
<!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指 定),不会加载关联表的所有字段,以提高性能 -->
<setting name="aggressiveLazyLoading" value="true"/>
</settings>
</configuration>
6、添加redis.properties文件配置redis信息
# Redis settings
redis.host=127.0.0.1
redis.port=6379
redis.pass=
redis.timeout=30000
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true
7、修改spring.xml
a、添加redis.properties的引用。(第30行)
<!-- 配置config.properties的文件路径 -->
<!--<context:property-placeholder location="classpath:config.properties"/>-->
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:redis.properties</value>
<value>classpath:config.properties</value>
</list>
</property>
</bean>
b、添加对mybatis.xml文件的引用。(第70行)
<!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 自动扫描mapping.xml文件,**表示迭代查找 -->
<property name="mapperLocations" value="classpath:*Mapper.xml"/>
<property name="configLocation" value="classpath:mybatis.xml"/>
</bean>
c、添加对redis数据源和连接工厂和中间类的配置。(第110行)
<!-- redis数据源 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}" />
<property name="maxTotal" value="${redis.maxActive}" />
<property name="maxWaitMillis" value="${redis.maxWait}" />
<property name="testOnBorrow" value="${redis.testOnBorrow}" />
</bean>
<!-- Spring-redis连接池管理工厂 -->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.host}" />
<property name="port" value="${redis.port}" />
<property name="password" value="${redis.pass}" />
<property name="timeout" value="${redis.timeout}" />
<property name="poolConfig" ref="poolConfig" />
</bean>
<!-- 使用中间类解决RedisCache.jedisConnectionFactory的静态注入,从而使MyBatis实现第三方缓存 -->
<bean id="redisCacheTransfer" class="com.qianfeng.day23.util.RedisCacheTransfer">
<property name="jedisConnectionFactory" ref="jedisConnectionFactory"/>
</bean>
8、添加实体类的序列化,implements java.io.Serializable。
9、添加RedisCache和RedisCacheTransfer两个类。
10、修改*mapper.xml文件,添加<cache type="com.qianfeng.day23.util.RedisCache"/>(第4行)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qianfeng.day23.dao.BookTypeMapper">
<cache type="com.qianfeng.day23.util.RedisCache"/>
...
</mapper>
作业:
1. 使用员工部门项目集成Ehcache
2. 使用员工部门项目集成Redis