redis的线程模型
- Server socket 监听到客户端发的请求,会产生一个AE_REABLE的事件
- IO多路复用程序将AE_REABLE事件压入队列中,依次执行
- <font color='red'>文件事件分派器</font>从队列中取数据交给<font color='red'>链接应答处理器</font>
- 链接应答处理器创建与客户端的长链接socket,并将这个长链接和需要执行的命令给到<font color='red'>命令请求处理器</font>
- 命令请求处理器进行命令执行,执行完成之后将socket的长链接与<font color='red'>命令回复处理器</font>关联
- 文件事件分派器使用对应的socket关联与命令回复处理器的执行断开socket请求
线程模型的意义
- 使用非阻塞IO多路复用程序,所以支持高并发
- 使用的文件事件分派器是单线程的,所以redis是单线程的
redis的击穿(穿透)
出现原因
出现大量redis中的key不存在的请求,导致创建了太多的jdbc链接从而跳过了redis的缓存机制,给数据库带来太大的压力
解决办法
1. 增加数据规则的验证,只有符合规则的key才进行查询,防止人为恶意的进行请求
2. 数据库中查询为null的对应key值信息也进行缓存
redis的缓存雪崩
出现原因
同一时间点大量的缓存数据失效,导致透过redis直接请求数据库出现问题
解决办法
1. 均匀分配缓存的过期时间(业务逻辑角度)
2. 创建多级缓存来辅助修改(springBoot+redis+Ecache)
3. 使用分布式锁的逻辑,保证获取数据库资源有一个一定的上限,超过上限就进行等待知道锁释放
redis的主从、集群和哨兵
主从复制
产生的原因
1. 容错性(只有一台的情况,一旦挂掉就会影像业务逻辑)
2. 并发性(一台的并发能力必然很低,读的能力会很差)
使用主从的结果
1. 数据冗余
2. 读写分离
原理
1. 主节点负责读/写,从节点只能进行读操作
2. 可以一主多从
3. 数据同步:一般初次会讲所有主节点数据同步到从节点,后续都是补发更新的数据,并且主从节点有长链接的心跳机制
哨兵机制
产生的原因
在搭建主从之后,主节点只有一个,一旦发生错误之后就没有主节点信息了
原理
- 在主节点外层新增一套哨兵逻辑
- 暴露给外部的是哨兵的信息,相当于哨兵进行了一层封装
- 一旦主节点出现问题,哨兵机制会在剩下的从节点中通过<font color='red'>选举算法</font>选出一个节点充当主节点
哨兵原理
- 每个
sentinel
会以心跳机制请求master、slave进行PING命令,如果一个实例响应超过设置的时间. - 当有足够数量的额sentinel确定master下线,就会认定下线,就会进行选举算法选举出新节点作为主节点
当使用哨兵模式之后,就不要再直接连redis的ip和端口了,而是访问哨兵的信息,由哨兵进行<font color='red'>转发</font>
redis淘汰策略
1.noeviction:禁止驱逐数据,当内存上限的时候,再添加数据会产生异常。从而保证数据不会丢失
2.allkeys-lru: 全体数据中找最近使用最少的数据进行淘汰
3.volatile-lru: 从有设置过期时间的数据中找最近使用最少的数据进行淘汰
4.allkeys-random: 全体数据,随机淘汰
5.volatile-random: 设置过期时间的数据,随机淘汰
6.volatile-ttl: 从设置过期时间的数据中,随机找一些数据淘汰。距离过期时间越短,淘汰优先级越高
Redis淘汰策略主要分为LRU淘汰、TTL淘汰、随机淘汰三种机制。
LRU淘汰
LRU(Least recently used,<font color="red">最近最少使用</font>)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
在服务器配置中保存了 lru 计数器 server.lrulock,会定时(redis 定时程序 serverCorn())更新,server.lrulock 的值是根据 server.unixtime 计算出来进行排序的,然后选择最近使用时间最久的数据进行删除。另外,从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru。每一次访问数据,会更新对应redisObject.lru。
在Redis中,LRU算法是一个近似算法,默认情况下,Redis会随机挑选5个键,并从中选择一个最久未使用的key进行淘汰。在配置文件中,按maxmemory-samples选项进行配置,选项配置越大,消耗时间就越长,但结构也就越精准。
TTL淘汰
也就是按照过期时间进行淘汰。距离过期时间越短,淘汰的优先级越高。
随机淘汰
在随机淘汰的场景下获取待删除的键值对,随机找hash桶再次hash指定位置的dictEntry即可。
Redis中的淘汰机制都是几近于算法实现的,主要从性能和可靠性上做平衡,所以并不是完全可靠,所以开发者们在充分了解Redis淘汰策略之后还应在平时多主动设置或更新key的expire时间,主动删除没有价值的数据,提升Redis整体性能和空间。
总结六种:
- 不淘汰
- 随机淘汰
- 有过期时间的随机淘汰
- 使用最少
- 有过期时间的使用最少
- 过期时间越短淘汰率越高
redis的八种数据结构
- String,字符串类型
- hash,对象类型
- list,有序可重复类型
- set,无需不可重复类型
- zset,无需可重复类型
- Geospatial,地理位置类型
- bitmap,位图类型(key:value(1,2))
- Hyperloglog,数学上的集合类型
队列模式:
list作为队列,rpush生产消息,lpop消费消息
redis持久化
不建议开启redis的持久化操作
RDB
原理
快照的方式保存数据,每隔一段时间有固定多少key值发生变更,将数据进行快照备份一次
由父进程fork开启一个子进程,使用子进程进行I/O操作将内存中的快照保存在硬盘上
缺点
耗时,耗性能
AOF
原理
将操作日志保存下来,做的是增量保存.在重启的时候执行所有操作一遍
缺点
文件体积大,恢复速度慢
现在一般采用两者集合的方式来进行持久化。底层对这两种方案做了融合处理
redis的事务
redis支持事务,但是整体redis集群不支持事务
1. 事务的一般执行流程
1. 开启事务`MULTI`
2. 执行操作,也就是执行你需要进行的操作set等
3. 提交事务`EXEC`,会执行一并所有的操作一起提交
2. 使用SpringBoot+redis执行事务
//1. 创建对应执行的RedisTemplate对象
@Autowired
StringRedisTemplate stringRedisTemplate
//2. 开启事务权限
stringRedisTemplate.setEnableTransationSupport(true);
//3. 开启事务
stringRedisTemplate.multi();
//4. 回滚
stringRedisTemplate.discard();
//5. 提交
stringRedisTemplate.exec();
如果开启事务,所有的命令只有在执行了exec。才会往redis中插入
redis实现分布式锁
原理
- redis是单线程的
如何实现
1.线程 A setnx(上锁的对象,超时时的时间戳 t1),如果返回 true,获得锁。
2.线程 B 用 get 获取 t1,与当前时间戳比较,判断是是否超时,没超时 false,若超时执行第 3 步;
3.计算新的超时时间 t2,使用 getset 命令返回 t3(该值可能其他线程已经修改过),如果
t1==t3,获得锁,如果 t1!=t3 说明锁被其他线程获取了。
4.获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,不用处理(防止删除其他线程的锁)。
核心
<font color='red'>setnx
</font>关键字,将key对应的资源锁定,set成功返回1,失败返回0
对比redis与zookeeper实现分布式锁
edis实现分布式锁与Zookeeper实现分布式锁的区别
相同点
在集群的环境下,保证只允许有一个jvm进行执行
从技术上分析
Redis是nosql数据库,主要特点是缓存
Zookeeper是分布式协调工具,主要用户分布式解决方案
实现思路分析
获取锁
Zookeeper,多个客户端(jvm)会在Zookeeper上创建同一个临时节点,因为Zookeeper节点命名路径保证唯一,不允许出现重复,只要谁能创建成功就能获取这个锁
redis,多个jvm会在redis中使用setnx命令创建相同的一个key,因为redis的key保证唯一,不允许重复。只要谁先创建成功,谁就能获取锁
释放锁
Zookeeper直接关闭 临时节点session会话连接,因为临时节点生命周期session会话绑定在一起,如果session会话连接关闭的话,就会使这个临时节点被删除
然后客户端使用事件监听,监听到临时节点被删除,就释放锁
redis释放锁的时候,为了保证是锁的一致性问题,在删除锁的时候需要判断对应的value是否是对应创建的那个业务的id
死锁解决
Zookeeper使用会话有效方式解决死锁的现象
redis是对key设置有效期来解决死锁现象
性能上:
因为redis是nosql,所以redis比Zookeeper性能好
可靠性
Zookeeper更加可靠。因为redis有效求不是很好控制,可能会产生延迟
其余面试问题
7、一个字符串类型的值能存储最大容量是多少?
512M
8、为什么 Redis 需要把所有数据放到内存中?
Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。
所以 redis 具有快速和数据持久化的特征,如果不将数据放在内存中,磁盘 I/O 速度为严重影响 redis 的性能。
在内存越来越便宜的今天,redis 将会越来越受欢迎, 如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。
11、MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中的数据都是热点数据?
使用的淘汰策略可以是LRU策略,可以实现淘汰最近最少使用的数据淘汰掉
22、Redis 中的管道有什么用?
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多 POP3 协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
25、Redis key 的过期时间和永久有效分别怎么设置?
EXPIRE 和 PERSIST 命令
26、Redis 如何做内存优化?
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。
比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面。
27、Redis 回收进程如何工作的?
一个客户端运行了新的命令,添加了新的数据。Redi 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。一个新的命令被执行,等等。
所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。
如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。
29.锁互斥机制
那么在这个时候,如果客户端 2 来尝试加锁,执行了同样的一段 lua 脚本,会咋样呢?很简单,第一个 if 判断会执行“exists myLock”,发现 myLock 这个锁 key 已经存在了。接着第二个 if 判断,判断一下,myLock 锁 key 的 hash 数据结构中,是否包含客户端 2 的 ID,但是明显不是的,因为那里包含的是客户端 1 的 ID。
所以,客户端 2 会获取到 pttl myLock 返回的一个数字,这个数字代表了 myLock 这个锁 key的剩余生存时间。比如还剩 15000 毫秒的生存时间。此时客户端 2 会进入一个 while 循环,不停的尝试加锁。
30.watch dog 自动延期机制
客户端 1 加锁的锁 key 默认生存时间才 30 秒,如果超过了 30 秒,客户端 1 还想一直持有这把锁,怎么办呢?
简单!只要客户端 1 一旦加锁成功,就会启动一个 watch dog 看门狗,他是一个后台线程,会每隔 10 秒检查一下,如果客户端 1 还持有锁 key,那么就会不断的延长锁 key 的生存时间。
缓存与数据库不一致怎么办
假设采用的主存分离,读写分离的数据库,
如果一个线程 A 先删除缓存数据,然后将数据写入到主库当中,这个时候,主库和从库同步没有完成,线程 B 从缓存当中读取数据失败,从从库当中读取到旧数据,然后更新至缓存,这个时候,缓存当中的就是旧的数据。
发生上述不一致的原因在于,主从库数据不一致问题,加入了缓存之后,主从不一致的时间被拉长了
处理思路:在从库有数据更新之后,将缓存当中的数据也同时进行更新,即当从库发生了数据更新之后,向缓存发出删除,淘汰这段时间写入的旧数据。
主从数据库不一致如何解决场景描述,对于主从库,读写分离,如果主从库更新同步有时差,就会导致主从库数据的不一致
1、忽略这个数据不一致,在数据一致性要求不高的业务下,未必需要时时一致性
2、强制读主库,使用一个高可用的主库,数据库读写都在主库,添加一个缓存,提升数据读取的性能。
3、选择性读主库,添加一个缓存,用来记录必须读主库的数据,将哪个库,哪个表,哪个主键,作为缓存的 key,设置缓存失效的时间为主从库同步的时间,如果缓存当中有这个数据,直接读取主库,如果缓存当中没有这个主键,就到对应的从库中读取。