Mybatis Redis 缓存
1-1.mybatis一级,二级缓存
一级缓存:
一级缓存基于sqlSession默认开启,在操作数据库时需要构造SqlSession对象,在对象中有一个HashMap用于存储缓存数据。不同的SqlSession之间的缓存数据区域是互相不影响的
二级缓存:
二级缓存的作用域是mapper的同一个namespace。不同的sqlSession两次执行相同的namespace下的sql语句,且向sql中传递的参数也相同,即最终执行相同的sql语句,则第一次执行完毕会将数据库中查询的数据写到缓存,第二次查询会从缓存中获取数据,不再去底层数据库查询,从而提高效率,多表联合查询时会造成脏读的情况
2.使用缓存存在的问题
2.1缓存穿透
概念:
是指查询数据库中一定不存在的数据。先在缓存中查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存中。如果数据库查询对象空,则不放进缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决办法:
1.布隆过滤
最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,一个一定不存在的数据会被bitmap拦截掉从而避免了对底层存储系统的查询压力。
2.强而有力
访问key未在DB查询到的空值写进缓存,设置较短过期时间
2.2缓存雪崩
概念:
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤 增,引起雪崩。
解决办法:
可以给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时 刻失效。
Redis
1.简介
1.1简介
Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供string,list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
1.2Redis的5种数据类型及其适用场景
(1)String:可以包含任何数据,比如jpg图片或者序列化的对象。
(2)List:链表(双向链表),增删快,提供了操作某一段元素的API。适用于:最新消息排行等功能;消息队列。
(3)Set:集合。哈希表实现,元素不重复,为集合提供了求交集、并集、差集等操作。适用于:共同好友;利用唯一性,统计访问网站的所有独立ip;好友推荐时,根据tag求交集,大于某个阈值就可以推荐。
(4)Hash 键值对集合,即编程语言中的Map类型。适合存储对象,并且可以像数据库中update一个属 性一样只修改某一项属性值。适用于:存储、读取、修改用户属性。
(5)Sorted Set:有序集合。将Set中的元素增加一个权重参数score,元素按score有序排列。数据插入集合时,已经进行天然排序。适用于:排行榜;带权重的消息队列。
1.3优势
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性.
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
1.4其他key-value存储有什么不同?
- Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
- Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
1.5Redis的特点
- Redis是一个高性能key/value内存型数据库
- Redis支持丰富的数据类型如(String,list,set,zset,hash)
key(string) value(String list hash zset set)
- Redis支持持久化
- Redis单线程,效率高 (redis 单线程效率发挥极致)
- (Memcache key value (String Object) 多线程 并发操作同一个数据 内部线程锁 c)
1.6基于aop,现实通用的缓存
aop的机制:
AOP是Spring框架面向切面的编程思想,AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。
切入点表达式
语法结构: execution( 方法修饰符 方法返回值 方法所属类 匹配方法名 ( 方法中的形参表 ) 方法申明抛出的异常 )
eg:
execution(* com.baizhi.service..query(…))
自定义注解
@redisCaChe
2.下载
- redis主页: www.redis.io
- 下载redis的最新资料包
- 将下载redis资料包上传到Linux中将压缩包在linux系统中解压
- 解压完成后进入解压目录直接的目录中执行make即可
3.安装,启动
1.创建虚拟机
2.安装linux镜像
3.启动虚拟机设置密码相关参数
4.在linux系统中设置自动获取ip地址
vi /etc/sysconfig/network-scripts/ifcfg-ens33
修改:ONBOOT=YES
5.重启虚拟机 reboot
2.克隆虚拟机
3.CRT连接虚拟机
1.安装gcc运行环境
yum -y install gcc #redis是C语言写的
2.vim的安装
yum install -y vim* #//在线安装vim
3.上传redis的资料包
在CRT中 alt+p , 直接拖进
4.解压redis的压缩包
tar -zxvf xxx.zip
5.安装包不能直接用,需要进入redis包执行指令编译redis
make
#如果遇到致命错误:执行
make MALLOC=libc
6.编译完成之后安装redis
make install PREFIX=/usr/redis
7.启动redis bin目录下执行
./redis-server
#默认执行配置文件 /path/to/redis.conf
#将配置文件拷贝到 /usr/redis目录 进入到家目录解压完成之后的redis中执行
cp redis.conf /usr/redis
#指定配置文件启动 usr/bin目录下执行
./redis-server ../redis.conf
8.修改启动端口
#将redis.conf中的默认端口修改为指定端口
port 6379 port 7000
9.使用redis的客户端连接redis服务 bin目录下
#默认连接
./redis-cli
#指定端口连接
./redis-cli –p 6379
10.守护模式模式启动
#将redis.conf中的默认端口修改为指定端口
daemonize no 改为 daemonize yes
11.关闭防火墙
service iptables stop
systemctl stop firewalld
systemctl disable firewalld
12.查看进程
ps aux|grep redis
#杀死进程
kill -9 8989
4.redis常用指令
说明 : 使用redis的默认配置器动redis服务后,默认会存在16个库,编号从0-15
可以使用select 库的编号 来选择一个redis的库
切换库 select 库名 select 1
查看值的类型 type key
设置一个key/value set
根据key获得对应的value get
清空当前库 flushdb
清空全部库 flushall
5.使用
6.Redis中的持久化机制
说明 : Redis提供了两种不同的持久化方法来将数据存储到硬盘里面
- 快照(snapshotting)
这种方式可以将某一时刻的所有数据都写入硬盘中,当然这也是redis的默认持久化方式,保存的文件是以.rdb形式结尾的文件因此这种方式也称之为RDB方式
- AOF(append only file)只追加文件
这种方式可以将所有客户端执行的写命令记录到日志文件中
6.1快照持久化
a) 快照持久化也是redis中的默认开启的持久化方案, 根据redis.conf中的配置,快照将被写入dbfilename指定的文件里面(默认是dump.rdb文件中)
b) 根据redis.conf中的配置,快照将保存在dir选项指定的路径上
c) 创建快照的几种方式
- 客户端可以使用BGSAVE命令来创建一个快照,当接收到客户端的BGSAVE命令时,redis会调用fork¹来创建一个子进程,然后子进程负责将快照写入磁盘中,而父进程则继续处理命令请求
- 客户端还可以使用SAVE命令来创建一个快照,接收到SAVE命令的redis服务器在快照创建完毕之前将不再响应任何其他的命令
注意 : SAVE命令并不常用,使用SAVE命令在快照创建完毕之前,redis处于阻塞状态,无法对外服务
名词解释 : fork当一个进程创建子进程的时候,底层的操作系统会创建该进程的一个副本,在类unix系统中创建子进程的操作会进行优化:在刚开始的时候,父子进程共享相同内存,直到父进程或子进程对内存进行了写之后,对被写入的内存的共享才会结束服务
- 如果用户在redis.conf中设置了save配置选项,redis会在save选项条件满足之后自动触发一次BGSAVE命令,如果设置多个save配置选项,当任意一个save配置选项条件满足,redis也会触发一次BGSAVE命令
- 当redis通过shutdown指令接收到关闭服务器的请求时,会执行一个save命令,阻塞所有的客户端,不再执行客户端执行发送的任何命令,并且在save命令执行完毕之后关闭服务器
6.2AOF持久化机制
6.2.1.在redis的默认配置中AOF持久化机制 是没有开启的,AOF持久化会将被执行的写命令写到AOF的文件末尾,以此来记录数据发生的变化,因此只要redis从头到尾执行一次AOF文件所包含的所有写命令,就可以恢复AOF文件的记录的数据集.
6.2.2.开启AOF持久化机制,需要修改redis.conf的配置文件,
- 通过修改redis.conf配置中appendonly yes来开启AOF持久化
- 通过appendfilename指定日志文件名字(默认为:appendonly.aof)
- 通过appendfsync指定日志记录频率
6.2.3.AOF 日志记录频率的选项
6.2.3.1.可选项
选项 | 同步频率 |
always | 每个redis写命令都要同步写入硬盘,严重降低redis速度 |
everysec | 每秒执行一次同步显式的将多个写命令同步到磁盘 |
no | 由操作系统决定何时同步 |
6.2.3.2.三种日志记录频率的详细分析 :
- 如果用户使用了always选项,那么每个redis写命令都会被写入硬盘,从而将发生系统崩溃时出现的数据丢失减到最少;遗憾的是,因为这种同步策略需要对硬盘进行大量的写入操作,所以redis处理命令的速度会受到硬盘性能的限制;
注意 : 转盘式硬盘在这种频率下200左右个命令/s ; 固态硬盘(SSD) 几百万个命令/s;
警告 : 使用SSD用户请谨慎使用always选项,这种模式不断写入少量数据的做法有可能会引发严重的写入放大问题,导致将固态硬盘的寿命从原来的几年降低为几个月
- 为了兼顾数据安全和写入性能,用户可以考虑使用everysec选项,让redis每秒一次的频率对AOF文件进行同步;redis每秒同步一次AOF文件时性能和不使用任何持久化特性时的性能相差无几,而通过每秒同步一次AOF文件,redis可以保证,即使系统崩溃,用户最多丢失一秒之内产生的数据(推荐使用这种方式)
- 最后使用no选项,将完全有操作系统决定什么时候同步AOF日志文件,这个选项不会对redis性能带来影响但是系统崩溃时,会丢失不定数量的数据
- 另外如果用户硬盘处理写入操作不够快的话,当缓冲区被等待写入硬盘数据填满时,redis会处于阻塞状态,并导致redis的处理命令请求的速度变慢(不推荐使用)
7.AOF文件的重写
aof 的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。为了压缩aof的持久化文件Redis提供了AOF重写机制
7.1.重写 aof 文件的两种方式
- 执行BGREWRITEAOF命令
- 配置redis.conf中的auto-aof-rewrite-percentage选项
7.2.BGREWRITEAOF 方式
收到此命令redis将使用与快照类似的方式将内存中的数据 以命令的方式保存到临时文件中,最后替换原来的文件。具体过程如下
- redis调用fork ,现在有父子两个进程子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
- 父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
- 当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
- 现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。
**注意 **: 重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,替换原有的文件这点和快照有点类似。(AOF重写过程完成后会删除旧的AOF文件,删除一个体积达几十GB大的旧的AOF文件可能会导致系统随时挂起 )
7.3. 配置redis.conf中的auto-aof-rewrite-percentage选项
- AOF重写也可以使用auto-aof-rewrite-percentage 100
和auto-aof-rewrite-min-size 64mb来自动执行BGREWRITEAOF.
说明: 如果设置auto-aof-rewrite-percentage值为100和auto-aof-rewrite-min-size 64mb,并且启用的AOF持久化时,那么当AOF文件体积大于64M,并且AOF文件的体积比上一次重写之后体积大了至少一倍(100%)时,会自动触发,如果重写过于频繁,用户可以考虑将auto-aof-rewrite-percentage设置为更大
8.两种持久化方案的总结
- AOF持久化既可以将丢失的数据的时间降低到1秒(甚至不丢失任何数据),那么我们还有什么理由不是用AOF呢?
注意 :
这个问题实际上并没有这么简单,因为redis会不断将执行的写命令记录到AOF文件中,所以随着redis运行,AOF文件的体积会不断增大,在极端情况下甚至会用完整个硬盘,还有redis重启重新执行AOF文件记录的所有写命令的来还原数据集,AOF文件体积非常大,会导致redis执行恢复时间过长
两种持久化方案既可以同时使用,又可以单独使用,在某种情况下也可以都不使用,具体使用那种持久化方案取决于用户的数据和应用决定
无论使用AOF还是快照机制持久化,将数据持久化到硬盘都是有必要的,除了持久化外,用户还应该对持久化的文件进行备份(最好备份在多个不同地方)
9.基于SpringData设计redis通用缓存
1.导入jar包
<!--springData操作redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.配置redis
spring:
redis:
host: 192.168.199.131 #ip地址
port: 7000 #端口号
database: 3 #操作的库
3.使用redisTemplate操作
3.1.下表是具体的操作视图接口类介绍:
Key类型操作 | |
opsForValue | Redis String/Value 操作 |
opsForList | Redis List 操作 |
opsForSet | Redis Set 操作 |
opsForZSet | Redis Sort Set 操作 |
opsForHash | Redis Hash 操作 |
redisTemplate.opsForValue(); //操作字符串
redisTemplate.opsForHash(); //操作hash
redisTemplate.opsForList(); //操作list
redisTemplate.opsForSet(); //操作set
redisTemplate.opsForZSet(); //操作有序set
StringRedisTemplate与RedisTemplate
- 两者的关系是StringRedisTemplate继承RedisTemplate。
- 两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
- SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。
StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。
RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。
简单使用
使用:redisTemplate.opsForValue().set("name","tom");
结果:redisTemplate.opsForValue().get("name") 输出结果为tom
使用:redisTemplate.opsForValue().set("name","tom",10, TimeUnit.SECONDS);
结果:redisTemplate.opsForValue().get("name")由于设置的是10秒失效,十秒之内查询有结果,十秒之后返回为null
stringRedisTemplate.opsForValue().set("test", "100",60*10,TimeUnit.SECONDS);//向redis里存入数据和设置缓存时间
stringRedisTemplate.opsForValue().get("test")//根据key获取缓存中的val
stringRedisTemplate.boundValueOps("test").increment(-1);//val做-1操作
stringRedisTemplate.boundValueOps("test").increment(1);//val +1
stringRedisTemplate.getExpire("test")//根据key获取过期时间
stringRedisTemplate.getExpire("test",TimeUnit.SECONDS)//根据key获取过期时间并换算成指定单位
stringRedisTemplate.delete("test");//根据key删除缓存
stringRedisTemplate.hasKey("546545");//检查key是否存在,返回boolean值
stringRedisTemplate.expire("red_123",1000 , TimeUnit.MILLISECONDS);//设置过期时间
stringRedisTemplate.opsForSet().add("red_123", "1","2","3");//向指定key中存放set集合
stringRedisTemplate.opsForSet().isMember("red_123", "1")//根据key查看集合中是否存在指定数据
stringRedisTemplate.opsForSet().members("red_123");//根据key获取set集合
4.配置切面类
@Configuration
@Aspect
public class RedisCache {
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
}
5-1.创建通用缓存(string,string)
/*
* 添加缓存
* */
@Around("execution(* com.baizhi.service.*.query*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("==环绕通知==");
/*解决缓存乱码*/
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
//获取切面切的方法
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
//判断该方法上是否有添加缓存的注解
boolean annotationPresent = method.isAnnotationPresent(AddCache.class);
if(annotationPresent){
StringBuilder sb = new StringBuilder();
//设计一个key(类名+方法名+实参) value(查询的结果)
//获取类的全限定名
String clazzName = proceedingJoinPoint.getTarget().getClass().getName();
sb.append(clazzName);
//获取方法名
String methodName = proceedingJoinPoint.getSignature().getName();
sb.append(methodName);
//返回方法的实参
Object[] args = proceedingJoinPoint.getArgs();
for (Object arg : args) {
sb.append(arg);
}
//获取key
String key = sb.toString();
//String 类型的操作
ValueOperations valueOperations = redisTemplate.opsForValue();
//判断key是否存在
Boolean aBoolean = redisTemplate.hasKey(key);
Object result = null;
//判断缓存中对否存在
if(aBoolean){
//存在 查询redis 返回结果
result = valueOperations.get(key);
}else{
result = proceedingJoinPoint.proceed();
//不存在 纳入redis缓存
valueOperations.set(key,result);
}
return result;
}else{
//没有该注解直接放行
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
}
5-2.清除缓存
/*
* 清空缓存
* */
@After("execution(* com.baizhi.service.*.*(..)) && !execution(* com.baizhi.service.*.query*(..)) ")
public void after(JoinPoint joinPoint){
System.out.println("==清空缓存==");
//类的全限定名
String className = joinPoint.getTarget().getClass().getName();
//获取所有的key
Set<String> keys = stringRedisTemplate.keys("*");
//遍历所有的key
for (String key : keys) {
//判断符合条件的key
if(key.startsWith(className)){
//清除
stringRedisTemplate.delete(key);
}
}
}
5-3.自定义注解
package com.baizhi.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AddCache {
//String value() default "";
//String name();
}
6-1.创建通用缓存(string,hash)
/*
* 添加缓存
* */
@Around("execution(* com.baizhi.service.*.query*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("==环绕通知==");
/*解决缓存乱码*/
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
//获取切面切的方法
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
//判断该方法上是否有添加缓存的注解
boolean annotationPresent = method.isAnnotationPresent(AddCache.class);
if(annotationPresent){
//创建可变长字符串
StringBuilder sb = new StringBuilder();
//设计一个key(类名+方法名+实参) value(查询的结果)
//获取类的全限定名
String clazzName = proceedingJoinPoint.getTarget().getClass().getName();
//获取方法名
String methodName = proceedingJoinPoint.getSignature().getName();
sb.append(methodName);
//返回方法的实参
Object[] args = proceedingJoinPoint.getArgs();
for (Object arg : args) {
sb.append(arg);
}
//获取key
String key = sb.toString();
//hash 类型的操作
HashOperations hashOperations = redisTemplate.opsForHash();
//判断key是否存在
Boolean aBoolean = redisTemplate.hasKey(key);
Object result = null;
//判断缓存中对否存在
if(aBoolean){
//存在 查询redis 返回结果
result = hashOperations.get(clazzName,key);
}else{
result = proceedingJoinPoint.proceed();
//不存在 纳入redis缓存 KEY key value
hashOperations.put(clazzName,key,result);
}
return result;
}else{
//没有该注解直接放行
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
}
6-2.清除缓存
/*
* 清空缓存
* */
@After("execution(* com.baizhi.service.*.*(..)) && !execution(* com.baizhi.service.*.query*(..)) ")
public void after(JoinPoint joinPoint){
System.out.println("==清空缓存==");
//类的全限定名
String className = joinPoint.getTarget().getClass().getName();
//获取所有的key
stringRedisTemplate.delete(className);
}
10.Java源注解
1、@Documented:
用于标记在生成javadoc时是否将注解包含进去,可以看到这个注解和@Override一样,注解中空空如也,什么东西都没有。
2、@Target
用于定义注解可以在什么地方使用,默认可以在任何地方使用,也可以指定使用的范围,开发中将注解用。
TYPE : 类、接口或enum声明
FIELD: 域(属性)声明
METHOD: 方法声明
PARAMETER: 参数声明
CONSTRUCTOR: 构造方法声明
LOCAL_VARIABLE:局部变量声明
ANNOTATION_TYPE:注释类型声明
PACKAGE: 包声明
示例:
@Target({ElementType.PARAMETER, ElementType.METHOD}) //参数声明、方法声明
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Servicelock {
String description() default “”;
}
3、@Inherited
允许子类继承父类中的注解,可以通过反射获取到父类的注解
4、@Constraint
用于校验属性值是否合法
5、@Retention
注解的声明周期,用于定义注解的存活阶段,可以存活在源码级别、编译级别(字节码级别)、运行时级别。
SOURCE:源码级别,注解只存在源码中,一般用于和编译器交互,用于检测代码。如@Override, @SuppressWarings。
CLASS:字节码级别,注解存在于源码和字节码文件中,主要用于编译时生成额外的文件,如XML,Java文件等,但运行时无法获得。 如mybatis生成实体和映射文件,这个级别需要添加JVM加载时候的代理(javaagent),使用代理来动态修改字节码文件。
RUNTIME:运行时级别,注解存在于源码、字节码、java虚拟机中,主要用于运行时,可以使用反射获取相关的信息。