Redis 有 5 种基础数据结构,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。
string:
list:
LinkedList
,底层实现结构是链表。hash:
HashMap
,它是无序字典。set:
Set
,它内部的键值对是无序的唯一的
,好比HashSet只要键值,value值都为NULL。zset :
有序的Set
1、绝大部分请求是纯粹的内存操作(非常快速)
2、采用单线程
,避免了不必要的上下文切换和竞争条件(所以不需考虑并发安全性)
3、非阻塞IO - IO多路复用
注意: Redis实际上是采用了线程封闭的观念,把任务封闭在一个线程,自然避免了线程安全问题,不过对于需要依赖多个redis(集群)操作的复合操作来说,依然需要锁,而且有可能是分布式锁
。
先说一下同步阻塞IO:
说以下I/O多路复用(多路网络连接复用一个io线程。):
I/O就是指的我们网络I/O,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。串起来理解就是很多个网络I/O复用一个或少量的线程来处理这些连接
大家是否知道,MySQL服务器如果突然宕机,怎样保证数据的不丢失:
Redis中也是采用这种方式,先说结论,然后在做分析?
持久化的方式来保证异常之后数据不丢失
,持久化使用的文件有两种:RBD文件
和AOF文件
。何为Redis的持久化?
快照
写入磁盘,它恢复时是将快照文件直接读到内存里主进程是不进行任何IO操作的,这就确保了极高的性能
。RDB持久化(默认的持久化方式)
RDB持久化即通过创建快照(压缩的二进制文件)的方式进行持久化
,保存某个时间点的全量数据。
RDB持久化的触发包括手动触发与自动触发两种方式。
优势
完整性
和一致性
要求不高劣势
间隔
时间做一次备份,所以如果Redis服务器意外down掉的话,就会丢失最后一次快照后的所有修改(因为有时间间隔
)AOF持久化
AOF和RDB持久化方式对比:
RDB方式在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他I0操作,所以RDB持久化方式可以最大化redis的性能。
与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些,但是数据丢失风险大。
RDB需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候fork的过程是非常耗时间的,可能会导致Redis在一些毫秒级不能回应客户端请求
RDB的恢复,直接把RDB文件里面的内容读进来就可以了
AOF文件里面存放的都是指令,如果那条指令存入时有问题了,或者被人为修改了,恢复的时候就会出问题,就算不出问题把全部执行执行一遍非常耗时。
RDB、AOF混合持久化
Redis 持久化方案的建议
如果Redis只是用来做缓存服务器
,比如数据库查询数据后缓存,那可以不用考虑持久化,因为缓存服务失效还能再从数据库获取恢复。
如果你要想提供很高的数据保障性,那么建议你同时使用两种持久化方式。如果你可以接受灾难带来的几分钟的数据丢失,那么可以仅使用RDB。
通常的设计思路是利用主从复制机制来弥补持久化时性能上的影响。即Master上RDB、AOF都不做,保证Master的读写性能,而Slave上则同时开启RDB和AOF(或4.0以上版本的混合持久化方式)来进行持久化,保证数据的安全性。
缓存穿透:
指查询一个在Redis中不存在的数据,而且在存储层还查不到该数据无法写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,可能导致 DB 挂掉。
简言之:在Redis和MySQL中都不存在
,导致每次都去访问DB,导致MySQL挂掉。
解决方案:
缓存击穿:
在Redis中不存在MySQL中存在
,导致大量请求都去访问DB,导致MySQL挂掉。解决方案:
使用互斥锁:当缓存失效时,不立即去 load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db 的操作并回设缓存,否则重试 get 缓存的方法。
永远不过期:物理不过期,但逻辑过期(后台异步线程去刷新)。
缓存雪崩:
同时失效
,请求全部转发到 DB, DB 瞬时压力过重雪崩。在Redis中不存在MySQL中存在
,导致大量请求都去访问DB,导致MySQL挂掉。解决方案:
Redis中采用的是非阻塞IO
String类型的底层实现,动态字符串
先说一下主从复制模式是咋回事:
主从复制的工作原理:
Slave启动成功连接到Master后会发送一个sync命令
Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,Master将传送整个数据文件到Slave,以完成一次完全同步
全量复制(首次
):Slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
增量复制(继续
):Master继续将新的所有收集到的修改命令依次传给Slave,完成同步
但是只要是重新连接Master,一次完全同步(全量复制)将被自动执行
主从复制是乐观复制
,当客户端发送写执行给主,主执行完立即将结果返回客户端,并异步的把命令发送给从,从而不影响性能(主先执行完,在传给从,乐观复制,认为期间不会宕机
)。也可以设置至少同步给多少个从主才可写。
缺点:一旦主节点由于故障不能提供服务, 需要人工将从节点晋升为主节点
, 同时还要通知应用方更新主节点地址
为了解决主从复制模式的这种缺点
,Redis从2.8开始正式提供了Redis Sentinel
(哨兵) 机制来解决这个问题
哨兵模式(在从
数据库的配置文件
中设置要监控主库的IP地址即可)
监控Master主服务器工作的状态
。只能保证 redis集群的高可用性
。哨兵进程的作用:
master
宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。一个哨兵进程对Redis服务器进行监控,不是太可靠
,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式,如下图。先说一下什么是主观下线和客观下线?
主观下线
(Subjectively Down, 简称 SDOWN):指的是单个 Sentinel 实例
对服务器做出的下线判断。客观下线
(Objectively Down, 简称 ODOWN):指的是多个 Sentinel 实例
在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。主观下线
状态切换到客观下线状态并没有使用严格的法定人数算法(strong quorum algorithm), 而是使用了流言协议: 如果 Sentinel 在给定的时间范围内, 从其他 Sentinel 那里接收到了足够数量的主服务器下线报告, 那么 Sentinel 就会将主服务器的状态从主观下线改变为客观下线
。,如果之后其他 Sentinel 不再报告主服务器已下线, 那么客观下线状态就会被移除。客观下线
条件只适用于主服务器, 对于任何其他类型的 Redis 实例(例如:slave实例), Sentinel 在将它们判断为下线前不需要进行协商(单个哨兵就可以将其判断为下线状态
), 所以从服务器或者其他 Sentinel 永远不会达到客观下线条件
。在说一下哨兵是如何进行监控的?
每个Sentinel(哨兵)进程以每秒钟一次
的频率向整个集群中的Master主服务器
,Slave从服务器
以及其他Sentinel(哨兵)
进程发送一个 PING 命令。
如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过down-after-milliseconds选项所指定的值,则这个实例会被 Sentinel(哨兵)进程标记为主观下线
(SDOWN)。
如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有
Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态。
当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)。
在一般情况下, 每个Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
当Master主服务器被 Sentinel(哨兵)进程标记为客观下线
(ODOWN)时,Sentinel(哨兵)进程向被标记为客观下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。
最后来说一下Redis故障时哨兵模式下自动切换过程:
主观下线
。客观下线
。当我们的需要缓存的数据非常多的时候,一个Redis实例可能就容不下了,这时候我们需要多Redis进行横向扩容,把数据分到若Redis中,这样就形成一个多master(每个master有自己的slave)的结构,每个master中存放的数据还不一样
,如何实现那,redis从3.0版本
开始引入了redis-cluster,就使用这个redis-cluster来实现。
redis-cluster:
横向扩展Redis内存
。redis-cluster vs
replication +sentinel
高并发性能
的场景,比如几个G的缓存,使用单个master就可以,也就是主从复制+哨兵模式
主要针对海量数据+高并发的场景
我们先看一下Redis的发展历程:
主从模式
:读写分离,备份,一个Master可以有多个Slaves。
哨兵sentinel
:监控,自动转移,哨兵发现主服务器挂了后,就会从slave中重新选举一个主服务器。
集群
:为了解决单机Redis容量有限的问题,将数据按一定的规则分配到多台机器,内存/QPS不受限于单机,可受益于分布式集群高扩展性。
注意:Redis的过期策略和内存淘汰机制不是一个概念:
Redis的过期策略,在Redis中过期的key不会立刻从内存中删除
,而是会同时以下面两种策略进行删除:
定期删除
:每隔一段时间,随机检查设置了过期的key并删除已过期的key;维护定时器消耗CPU资源;惰性删除
:当key被访问时检查该key的过期时间,若已过期则删除;已过期未被访问的数据仍保持在内存中
,消耗内存资源;redis 内存淘汰机制有以下几个(常用的):
allkeys-lru
:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的
)。优先移除
。英文解释:
总结:Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。
如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回)或者你可以配置内存淘汰机制
,当Redis达到内存上限时会删除一部分内容
。
事务:当一次执行多个命令,本质是一组命令的集合,一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许加塞,类似于synchronsized锁一样。
相比于MySQL中的事务来说,Redis中的事务都不能称为真正的事务,所以一般也不用Redis中的事务
注意:Redis的事务不是原子性
入队的时候就已经出错
,整个事务内的命令将都不会被执行(其后续的命令依然可以入队),如果这个错误命令在入队的时候并没有报错
,而是在执行的时候出错了
,那么redis默认跳过这个命令执行后续命令。也就是说,redis只实现了部分事务。Redis事务的三阶段三特性
单独的隔离操作
:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。没有隔离级别的概念
:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行, 也就不存在事务内的查询要看到事务里的更新,在事务外查询不能看到这个让人万分头痛的问题不保证原子性
:redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
。在说非关系型数据库的CAP原理之前,先说一下关系型数据库的ACID原理
并发
的事务之间不会互相影响分布式数据库的CAP原理:
CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。
而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的
。所以我们只能在一致性
和可用性
之间进行权衡,没有NoSQL系统能同时保证这三点
。
如果项目中采用的是Redis Cluster
(集群架构),不同的key是有可能分配在不同的Redis节点上的,在这种情况下Redis的事务机制是不生效的。
其次,Redis事务不支持回滚操作
,所以基本不用
分布式锁:是控制分布式系统不同进程
共同访问共享资源
的一种锁的实现,秒杀下单、抢红包等等业务场景,都需要用到分布式锁,我们项目中经常使用Redis作为分布式锁
。
选了Redis分布式锁的几种实现方法,大家来讨论下,看有没有啥问题哈。
命令setnx + expire分开写:
if(jedis.setnx(key,lock_value) == 1){ //加锁 expire(key,100); //设置过期时间 try { do something //业务请求 }catch(){ } finally { jedis.del(key); //释放锁 } }
setnx
加锁,正要执行
expire设置过期时间时,进程crash掉或者要重启维护了,那这个锁就长生不老
了,别的线程永远获取不到锁啦,所以分布式锁不能这么实现。setnx + value值是过期时间:
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间 String expiresStr = String.valueOf(expires); // 如果当前锁不存在,返回加锁成功 if (jedis.setnx(key, expiresStr) == 1) { return true; } // 如果锁已经存在,获取锁的过期时间 String currentValueStr = jedis.get(key); // 如果获取到的过期时间,小于系统当前时间,表示已经过期 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈) String oldValueStr = jedis.getSet(key_resource_id, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁 return true; } } //其他情况,均返回加锁失败 return false; }
jedis.getSet()
,最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。set的扩展命令(set ex px nx):
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加锁 try { do something //业务处理 }catch(){ } finally { jedis.del(key); //释放锁 } }
set ex px nx + 校验唯一随机值,再删除:
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加锁 try { do something //业务处理 }catch(){ } finally {// 代码块中不是原子操作 //判断是不是当前线程加的锁,是才释放 if (uni_request_id.equals(jedis.get(key))) { jedis.del(key); //释放锁 } } }
问题:在这里,判断当前线程加的锁和释放锁是不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。
一般也是用lua脚本代替。lua脚本如下
Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件(Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照,AOF文件过大会影响Master重启的恢复速度)
如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
尽量避免在压力很大的主库上增加从库
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…,这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。
这个问题主要考察了以下几点内容:
1.Redis的内存淘汰策略。
2.Redis的最大内存设置。
分析题目:保证Redis 中的 20w 数据都是热点数据 说明是被频繁访问的数据,并且要保证Redis的内存能够存放20w数据,要计算出Redis内存的大小。
**a、保留热点数据:对于保留 Redis 热点数据来说,我们可以使用 Redis 的内存淘汰策略来实现,可以使用allkeys-lru淘汰策略,**该淘汰策略是从 Redis 的数据中挑选最近最少使用的数据删除,这样频繁被访问的数据就可以保留下来了。
**b、保证 Redis 只存20w的数据:**1个中文占2个字节,假如1条数据有100个中文,则1条数据占200字节,20w数据 乘以 200字节 等于 4000 字节(大概等于38M);所以要保证能存20w数据,Redis 需要38M的内存。
这个是最常用的
)。volatile和allkeys规定了是对已设置过期时间的数据集淘汰数据
还是从全部数据集淘汰数据
,后面的lru
、ttl
以及random
是三种不同的淘汰策略,再加上一种no-enviction
永不回收的策略。
使用策略规则:
1、如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
2、如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
关系型数据库最典型的数据结构是表
,由二维表及其之间的联系所组成的一个数据组织
非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等。
优点:
缺点: