目录
RDB(Redis DataBase):Rdb 保存的是dump.rdb文件
为什么要有Redis数据库?
设想一个场景,如果上亿的用户同时访问数据库,呢么如果是mysql数据库,它可以支撑住这么大的压力嘛?答案是不能,mysql的极限并发量大概在10000左右,超过一万的时候他就会崩溃了。
并且抛开并发不说,他的io速度也是比较慢的,因为mysql是基于磁盘硬件存储的,这样我们拿数据的时候就要先把数据从磁盘中读取出来,并且放到内存中,然后才可以显示到电脑屏幕前。这样就很大程度上限制了软件和项目的效率。
有一个困难就会有一个解决问题的方法,现在mysql能存活至今并且成为市面上前几的数据库,就肯定有他的道理,应景而生,redis数据库就出来了,redis数据库被称为NoSQL数据库,这是什么意思呢?
NoSQL数据库,即 Not Only SQL 不仅仅是sql语言的数据库,也即非关系型数据库,这类数据库的读取速度和并发量一般都很高,可以很好的解决mysql的一系列短板。这个时候我们可以想一想,为什么同为数据库,但是Redis数据库要比mysql数据库的并发量和读取速度要快很多呢?
这是因为啊,Redis数据库的所有数据的存储是基于内存来实现的,从它启动之后,他的所有数据就都在内存中去,这就避免了频繁的io操作,并且我们还明白一个道理,内存的速度和性能是要远远的高于磁盘的,所以这也就是为什么Redis比Mysql数据库要快很多的原因,但是这也就造就了一个问题,我们知道,内存的数据是不安全的,不i是持久化的,这意味着一旦电脑意外down机,呢么我们的数据就会全部丢失,呢么如何通过一个良好的算法尽可能的避免数据的丢失和损坏呢?这就涉及到了Redis数据库的持久化操作,分别是RDB和AOF ,他们分别对应了两种方案来进行持久化。下边我们就先来谈谈这两个持久化操作。
Redis的持久化操作---RDB&AOF
RDB(Redis DataBase):Rdb 保存的是dump.rdb文件
顾名思义,他是一个redis的一个数据库备份文件,是一个快照,可以保存当前时间内数据库内的所有信息。它会在指定的时间间隔内将内存中的数据集快照写入到磁盘,也就是Snapshot快照,恢复数据的时候,将快照文件直接读取到内存中去。
原理:
Redis会单独创建一个子进程(fork)来进行持久化,会先将数据写入到一个临时文件中去,等持久化过程结束后,再用这个临时文件替换上次持久化号的文件,整个过程中,主进程不进行任何io操i做,因为所有的io操做都在子进程中进行了,相当于我备份文件不占用主进程的资源。这就确保了极高的性能。 如果需要大规模数据库的回复,且对于数据恢复的完整性不做很高的要求,呢么RDB是一种很好的方式,唯一缺点就是:最后一次持久化的数据有i可能会丢失,因为他的作用原理是在一个时间间隔内执行多少次操作后进行快照保存嘛(在redis.conf文件中配置)如果我m秒内没有执行n次操作,此时服务器意外down机了,比如我关闭线程。此时的话就会丢失这个m秒内的数据。
进行数据恢复只需要将备份的rdb文件移到当前执行链接的文件夹下就可以
save 900 1 // 每九百秒中执行一次操作执行快照
save 300 10 // 三百秒中执行10次操作触发快照
save 60 10000 // 60秒中执行10000次操作触发快照
注:Fork:Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
AOF(append only file):依赖于文件
原理:
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
aof形式的持久化文件为aof后缀的文件,这个文件如果进去更改的话,呢么下一次链接的时候就会出现链接出错的问题,因为系统监测到你的aof文件被损坏了,但是我们可以修复这个文件,具体的方法是,进入到redis安装路径对应的bin目录下,那里会有一个redis-check-aof 的黄色命令把,然后通过这个命令就可以修复了(需要注意的是,修复后,被篡改的数据会呗清除)
修复:
redis-check-rdb --fix aof文件路径
如果aof和rdb共存的话,优先寻找aof文件:
aof和rdb的持久化数据是不互通的 如果只开一个,呢么另一个的数据不受影响,比如我只开启rdb,呢么rdb内有数据,然后我关闭rdb,开启aof,将数据库清空,这个时候会同时有rdb和aof两个持久化文件,如果此时我只让rdb开启,呢么再次启动数据库,数据就i还是rdb的数据。而不是aof的空数据。
此时aof内的数据为空,而rdb有数据,如果他们同时开启,
呢么结果按aof内的数据来,就是还是空
我发现:redis的数据库恢复完全按照备份文件来,比如说:我在三个文件夹内创建了三个rdb文件,三个rdb文件的内容都不一样,呢么分别在这三个文件夹内执行redis的链接,出来的结果也是不一样的。
序列化操作:
首先我们思考一个问题,序列化是什么东西,为什么要有序列化这个东西呢?
序列化: 把内存《JVM本地方法区,静态区,栈,队,程序计算器》中存在的对象存储到磁盘(网络)上。
反序列化: 把磁盘或网络上的内容转为为java内存中的对象。
@Test// 序列化:把内存中存在的对象存储到磁盘(网络)上
public void testSerial() throws Exception{
// 创建该对象后,该对象就会存储到堆内存中
User user=new User("炎客气","女");
// 创建一个ObjectOutputStream ,用于输入java内存中的对象 到指定的磁盘上
ObjectOutputStream os=new ObjectOutputStream(new FileOutputStream("E:/ykq.txt"));
// 用的是java中默认的序列化方式jdk
os.writeObject(user); // 输出到文件
os.close();
}
@Test // 反序列化:把磁盘或网络上的内容转为java内存中的对象
public void testUnSerial() throws IOException, ClassNotFoundException {
// 读取数据
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("E:/ykq.txt"));
Object user=inputStream.readObject();
System.out.println("user = " + user);
inputStream.close();
}
为什么我们要理解序列化和反序列化这些操作呢?在文章的开头我们就说过,Redis是基于内存的数据库,呢么它的保存和读取肯定都要涉及序列化和反序列化。而我们也可以设置他的序列化和反序列化的类型。
接下来我们看一下在SpringBoot中如何整合Redis并且设置序列化的操作。
// StringRedisTemplate:存储的value类型必须为String类型。不能存放对象。 如果想存放对象,必须把java对象转换成json
// 字符串 借助fastJson工具包
@Autowired
private StringRedisTemplate stringRedisTemplate;
//这个对象可以直接存放对象或者集合。 但是在使用前,要先为该对象的key和value指定序列化的类型
@Autowired
private RedisTemplate redisTemplate;
@Test
public void Test(){
// 指定RedisTemplate 序列化的方式
redisTemplate.setKeySerializer(new StringRedisSerializer()); // 序列化的key用String方式-- 对应redis数据库中的
// 字段名
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());// 序列化的值用转换为json形式--对应reids
// 数据库中的值 用这种方式数据库内的数据直接就是json格式了
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("user3",new User("刘德华","女"));
}
这就是最基本的整合和设置序列化的操作。
在Springboot中使用Redis作为缓存:
方法1、在Service层中自己书写并且配置,
大致思路:
先在Service层注入SpringBoot为我们准备的工具类,RedisTemplate,这个类是SpringBoot给我们弄好的一个Redis链接,相当于封装了java与Redis的链接,直接拿过来就可以用,但是需要配置序列化方式,也更加的灵活。
然后我们思考一个问题,Redis在项目中到底是在哪一个层次使用呢?标题已经说明了,它作为缓存来使用,就是说,我每次查询mysql数据库之前都会查询一下缓存内有没有数据,如果有的话就直接返回,没有的话再去查询数据库,呢么同时就也有了一个问题,如果我的数据库做了修改和新增或者删除,呢么我的缓存该怎么处理呢???是应该直接删除重新查询注入数据还是怎么???
在这里,我们选择在数据库做修改的时候同时给Redis数据库做修改,就是我修改Update的时候,在Service层同时给Redis进行Set数据覆盖,如果我新增了也进行Set,如果查询的时候呢,我先去Redis中查询对应的字段有没有,如果有直接返回,如果没有,呢么给Redis进行值的注入然后返回,这样下次就会有数据了,这就是Redis缓存的思路。
贴代码:
@Service
public class DeptSerImpl implements DeptSer {
@Autowired
private DeptDao deptDao;
@Autowired
private RedisTemplate redisTemplate;// redisTemplate在序列化的时候必须为其指定setkey和setvalue的序列化类型
public RedisTemplate getRedisTemplate(){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
@Override
public List<Dept> findAllDept() {
getRedisTemplate();
ValueOperations forValue = redisTemplate.opsForValue();
List<Dept> deptAll = (List<Dept>) forValue.get("deptAll");
if (deptAll==null){
List<Dept> allDept = deptDao.findAllDept();
forValue.set("deptAll",allDept);
return allDept;
}
return deptAll;
}
@Override
public void insertDept(Dept dept) {
deptDao.insertDept(dept);
}
@Override
public Dept updateDept(Dept dept) {//当我数据库做修改的时候,为了避免频繁的redis缓存失效,选择将redis缓存中的数据
// 也做对应的修改即可
getRedisTemplate();
deptDao.updateDept(dept);// 数据库修改
redisTemplate.opsForValue().set("dept:"+dept.getDeptId(),dept);// 对redis缓存数据库也做对应的修改
return dept;
}
@Override
public void deleteDept(Integer id) {
deptDao.deleteDept(id);
}
@Override
public Dept findById(Integer id) {
getRedisTemplate();
ValueOperations forValue = redisTemplate.opsForValue();
Dept dept = (Dept) forValue.get("dept:" + id);
if (dept==null){
Dept byId = deptDao.findById(id);
forValue.set("dept:"+id,byId);
return byId;
}
return dept;
}
}
还有一种方式是在注解的环境下使用缓存:主流、
使用步骤更简单:
1、在启动类上开启注解:
@SpringBootApplication /* 不去自动加载datasource*/
@EnableCaching
public class SpringbootRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootRedisApplication.class, args);
}
}
2、在对应的方法上添加开启缓存注解:
@CachePut: 先修改数据库,再修改缓存,如果缓存中没有则放入到缓存中该对象。
--- cacheNames: 缓存的前缀名称
--- key:缓存的后缀名称。
@CacheEvict: 删除数据库并删除缓存。
@Cacheable: 先查询缓存,缓存中如果存在,则直接返回,如果不存在则执行方法体。并把方法体中执行的结果放入到缓存中。
@Service
public class DeptSerImpl3 implements DeptSer {
@Autowired
private DeptDao deptDao;
@Autowired
private RedisTemplate redisTemplate;// redisTemplate在序列化的时候必须为其指定setkey和setvalue的序列化类型
@Override
public List<Dept> findAllDept() {
return null;
}
@Override
public void insertDept(Dept dept) {
deptDao.insertDept(dept);
}
@Override
@CachePut(cacheNames = "dept",key = "#dept.deptId")// 这里的cacheNames和key拼接的值就是redis中的key
public Dept updateDept(Dept dept) {//当我数据库做修改的时候,为了避免频繁的redis缓存失效,选择将redis缓存中的数据
// 也做对应的修改即可
deptDao.updateDept(dept);// 数据库修改
return dept;
}
@Override
public void deleteDept(Integer id) {
deptDao.deleteDept(id);
}
@Override
@Cacheable(cacheNames = "dept",key = "#id")
public Dept findById(Integer id) {
Dept byId = deptDao.findById(id);
return byId;
}
}
测试:
@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringRedisCatchTest1 {
@Autowired
private DeptSer deptSer;
@Test
public void test(){
System.out.println(deptSer.findAllDept());
}
@Test
public void findDeptById(){
deptSer.updateDept(new Dept(1,"炎客气"));
System.out.println(deptSer.findById(1));
}
}
测试结果是;第一次会执行sql语句查询数据库,第二次就不会了。
Redis的复制功能:
Redis 的读并发量太大怎么办?
单机版的Redis 挂掉怎么办?
需要写并发又要安全 在redis 3.0 后,官方发布了集群方案
行话:也就是我们所说的主从复制,主机数据更新后根据配置和策略,
自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
作用就是进行读写分离,分散缓解服务器的压力,容灾回复
详细操作:
①拷贝多个redis.conf文件
②开启daemonize yes 后台运行。
③Pid文件名字 redis启动后进程号所放的位置
④port指定端口 端口号
⑤Log文件名字 redis日志产生的位置
⑥Dump.rdb名字 RDB持久化的内容所保存的位置
启动三个redis
查看三台redis的主从关系
配置6380 和6381 的主节点为6379
主节点主要负责写的功能:
从节点只能读不能写。
如果主节点挂了。 思考: 从节点会上位还是等待老大回来。
等着老大回来。
所以这种主从复制会有很大的弊端,老大一死,剩下的都不行了,所以就有了下一种方案,薪火相传:
就是机器1是机器2的老大,机器2是机器3的老大,这种就叫新货相传,但是这种只有最大的老大才可以进行写操作。
如果老大挂了,需要手动设置老二为老大,命令是slaveof no one
这种方法的弊端就是需要手动的维护老大。
哨兵模式:
于是就出了另一种更好的模式:哨兵,在主从复制的基础上,新添一个机器作为哨兵,密切关注老大的一举一动,如果老大挂了,呢么就选票推举一个奴隶当作老大,即使老大回来,也是新老大的奴隶。这样就很好的解决了需要手动维护老大的问题。
配置文件是;Sentinel.conf
然后需要进入到bin目录下启动哨兵:
./redis-sentinel /root/master-slaver/sentinel.conf
集群模式:
如果说,哨兵和薪火相传是为了解决各个机器之间老大挂掉的问题,呢么集群就是为了解决老大压力的问题,设想一下,如果一个老大只有几个小弟,呢么还好说,但是如果一个老大又成千上万个小弟的话,呢么每次老大set新数据,都要给每个小弟进行复制,呢么老大的工作压力也是很大的,所以说就出现了集群的模式,这个模式的特点就是可以配置多个老大。
3.1,哈希槽说明 a--->crc16--97%16384
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。
当你往Redis Cluster中加入一个Key时,会根据crc16(key) mod 16384计算这个key应该分布到哪个hash slot中,一个hash slot中会有很多key和value。你可以理解成表的分区,使用单节点时的redis时只有一个表,所有的key都放在这个表里;改用Redis Cluster以后会自动为你生成16384个分区表,你insert数据时会根据上面的简单算法来决定你的key应该存在哪个分区,每个分区里有很多key。
搭建集群:
复制配置文件:
修改每个配置文件的内容
bind 0.0.0.0 69行
port 7000 92行
daemonize yes 136行
# 打开aof 持久化
appendonly yes 700行
# 开启集群
cluster-enabled yes 833行
# 集群的配置文件,该文件自动生成
cluster-config-file nodes-7000.conf 841行
# 集群的超时时间
cluster-node-timeout 5000 847行
启动:
使用命令创建集群
redis-cli --cluster create --cluster-replicas 1 192.168.118.110:7000 192.168.118.110:7001 192.168.118.110:7002 192.168.118.111:7003 192.168.118.111:7004 192.168.118.111:7005
注意:不能在子节点上进行写操作。
4.1Redis支持的数据类型?
String(字符串类型)String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; 常规计数:微博数,粉丝数等。
常用命令: set,get,incr,decr,mget 等:set key value 设置值、 get key 获取值、 incr key 加一、 decr key 减一
hash(哈希)
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。
常用命令: set,get,decr,incr,mget 等:
hset key field value 设置值
hget key field 获取值
hincrby key field num 设置增数量
list(列表)
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。
可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
常用命令: lpush,rpush,lpop,rpop,lrange等:
lpush list a b c d (从list左边添加元素)、 rpush list 1 2 3 4 (从list右边添加元素)
lrange list 0 -1(从0 到 -1 元素查看:也就表示查看所有)
lpop list (从list左边取,删除)、 rpop list (从list右边取,删除)
set(集合)
Redis的Set是string类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
常用命令: sadd,spop,smembers,sunion 等:
sadd set1 a b c d d (向set1中添加元素) 元素不重复
smembers set1(查询元素)、 srem set1 a(删除元素)
sorted set(zset,有序集合)
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
例:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
常用命令: zadd,zrange,zrem,zcard等:
zadd zset1 1 a 2 b 3 c (添加元素 zadd key score member,这里添加元素a:1分、元素b:2分、元素c:3分 )
zrange zset1 0 -1 (查看zset1的所有元素,默认从小到大)
zrange zset1 0 -1 withscores (查看zset1的所有元素,包括分数score)
zrevrange zset1 0 -1 (查看zset1的所有元素,从大到小)
zincrby zset1 5 a (对zset1的a元素增加5分)
4.2什么是Redis持久化?Redis有哪几种持久化方式?优缺点是什么
RDB (快照):快照形式 ,定期将当前时刻的数据保存磁盘中。会产生一个dump.rdb文件
特点:性能较好,数据备份。但可能会存在数据丢失。
AOF(只追加文件) :append only file (所有对redis的操作命令记录在aof文件中),恢复数据,重新执行一遍即可。
特点:每秒保存,数据比较完整。但耗费性能。
【注】如果两个都配了优先加载AOF。(同时开启两个持久化方案,则按照 AOF的持久化放案恢复数据。)
4.3Redis 有哪些架构模式?讲讲各自的特点?
主从模式(redis2.8版本之前的模式)、哨兵sentinel模式(redis2.8及之后的模式)、redis cluster模式(redis3.0版本之后)
缓存穿透和缓存雪崩以及避免方法:
缓存穿透:因为我们的redis数据库缓存的数据是基于数据库的数据的,所以如果是数据库中没有的数据的话,呢么reids缓存中也一定没有,设想如果有人就利用这个特点,故意去访问大量的数据库中不存在的数据,呢么就可以绕过redis缓存去直接访问数据库,这样就会对数据库造成很大的压力,相当于redis缓存就没有用了,这样的场景就是缓存穿透。
呢么我们应该如何去解决这个问题呢???
1、在每次外部访问redis数据库的时候,如果redis数据库内不存在这个数据,就给他设置一个空数据,null值,这样下次访问的时候,就不会绕过redis缓存了
1、1但是这样就会出现另一个问题,如果恶意的访问量很大,呢么redis中就会有大量的空数据,这也不是我们想看到的,这个问题的解决方法就是给这些空数据设置一个非常短的有效时间,过期清除,还有就是可以在每次数据库insert该空值数据的时候删除他们。
2、缓存雪崩:
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
1.在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2:搭建集群
3:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀