一、使用缓存的好处
一般情况在客户端和存储层之间都会有一层缓存层,当发送请求的时候会先去缓存层查找,如果找到则直接返回,如果没有找到,就会发生缓存穿透(就是缓存层没有穿过缓存层去查数据库),如果在存储层找到数据,则会将数据回写到缓存层,以便客户端下次请求同样数据的时候可以从缓存中找到,还可以实现熔断机制(就是当我们发现存储层挂掉或者没办法提供服务的时候可以让客户端的请求直接打在缓存层上不管有没有查询到数据的情况都直接返回)。
二、Redis快速的原因
Redis数据类型丰富、支持数据磁盘持久化存储、支持主从、支持分片。Redis可以达到100000+ 的qps(即每秒查询数)。
那么为什么可以做到这么快呢?1:完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高。redis采用的是单进程、单线程的模式key-value数据库。将数据存储在内存中读写的时候不会受到磁盘IO影响。所以速度极快。2:数据结构简单,对数据操作也简单。3:采用单线程,单线程也能处理高并发请求,想多核也可启动多实例。(单线程指的是主线程的单线程主线程包括IO事件的处理以及IO请求的处理此外主线程还负责过期键的处理,复制协调、集群协调 除了IO事件的逻辑会被主线程封装成周期性的任务进行处理)正因为采用单线程的设计对于客户端的所有读写请求都由一个主线程串行处理因为多个客户端对于一个键进行写操作的时候就不会有并发的问题, 避免了频繁的上下文切换和锁竞争,使得Redis执行起来效率更高。(并发并不是并行)单线程+IO多路复用。这里了所谓的单线程只是在处理网络请求的时候只有一个单线程来处理。一个正式的redisServer在运行的时候肯定不止一个线程的。比如redis在进行持久化的时候会以子进程或者子线程的方式,4:采用多路I/O复用模型,非阻塞IO。
redis采用的IO多路复用函数:epoll/kquue/evport/select都有可能redis会因地制宜,优先选择时间复杂度为O(1)的IO多路复用函数作为底层实现,以时间复杂度为O(n)的select作为保底函数。基于react设计模式监听IO事件。
三、Redis基本类型
String 是最基本的数据类型,二进制安全。最大可以存储512M。redis操作自带原子性使得我们不必要考虑并发问题。
Hash String元素组成的字典,适合用于存储对象。
List 列表 按照String元素插入顺序排序。(lpush、lrange)
Set String元素组成的无序集合,通过哈希表实现,不允许重复。
Sorted Set 通过分数来为集合中的成员进行从小到大的排序。
使用keys从海量key里查询出某一固定前缀的Key,keys pattern 查找所有符合给定模式pattern的key。
keys指令一次性返回所有匹配的key,但是如果键的数量过大会使服务卡顿。(解决办法使用SCAN命令)
SCAN cursor 【MATCH pattern】 【COUNT count】
基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。
以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历。(只要游标不返回0就不应该结束遍历)
不保证每次执行都返回某个给定数量的元素,支持模糊查询。
一次返回的参数不可控,只能是大概率符合count参数。
scan 0 match k1* count 10 从游标0的位置开始 匹配k1开头的 每次查找10个。会出现重复key的情况,因为我们是匹配k1开头的所有key,去重的话我们可以将查询出来的key放到set中进行去重。
四、Redis分布式锁解决的问题及使用
使用redis实现分布式锁分布式锁需要解决的问题:
互斥性:任意时间只能有一个客户端获取锁。不能同时有两个客户端获取锁。
安全性:锁只能由持有锁的客户端删除,不能由其他的客户端删除。
死锁:获取锁的客户端因为某些原因宕机了,而未能释放锁,其他客户端再也无法获取到该锁。导致的死锁。
容错:当一些redis节点宕机,客户端仍能获取锁和释放锁。
SETNX key value :如果key不存在,则创建并赋值。(时间复杂度是O(1)设置成功返回1 设置失败返回0)
解决setnx长期有效的问题,使用EXPIRE key seconds
设置key的生存时间,当key过期时(生存时间为0),会被自动删除。
但是这种方式原子性得不到满足,虽然每一个单独的操作是原子性的但是两个操作合在一起就不满足了,比如setnx后执行expire失败了 这时就会出现问题了。
redis 2.6.12开始我们就可以将这两个操作融合再一次成为一个原子性操作。
SET key value 【EX seconds】【PX milliseconds】【NX】【XX】
EX seconds 设置键的过期时间为second 秒
PX milliseconds 设置键的过期时间为millisecond 毫秒。
NX 只在键不存在时 才对键进行设置操作。
XX 只在键已经存在时 才对键进行设置操作。
Set 操作完成时 返回OK 否则返回 nil。
Java 中Jedis实现分布式锁的方式如下图:
String cacheKey = CommonUtil.getCacheKey(false, aaa, bbb);
if(redisClient.set(cacheKey, JSONObject.toJSONString(resultMap), NX.NOTEXIST, TimeUnit.SECOND, AsyncService.CACHE_TIME)){
}
出现大量key同时过期的注意事项。集中过期,由于清除大量的key很耗时,会出现短暂的卡顿现象。
解决方案:在设置key的过期时间的时候,给每个key加上随机值。
五、使用Redis做异步队列
使用redis做异步队列,使用List作为队列,RPUSH生产消息,LPOP消费消息 。
缺点:没有等待队列里有值就去消费。弥补:可以使用sleep在应用层调用LPOP进行重试。
如果不想使用sleep 进行重试,还可以使用指令:BLPOP key 【key ...】阻塞直到队列有值或者超时。
缺点:只能供一个消费者消费。
我们可以使用pub/sub模式:
pub 发送者 发送消息 sub 订阅者 订阅消息,订阅者可以订阅任意数量的频道。
pub/sub这种模式是有缺点的 消息的发布时无状态的,无法保证可达。
解决这些问题就得使用专业的消息队列。
六、Redis持久化
redis RDB持久化:保证某个时间点的全量数据快照。
SAVE :阻塞redis 的服务器进程,直到RDB文件被创建完毕。
BGSAVE : Fork 出一个子进程来创建RDB文件,不阻塞服务进程。
自动触发Redis RDB持久化的方式如下:
根据redis.conf 配置里的SAVE m n 定时触发,(用的是BGSAVE)。
主从复制时 ,主节点自动触发。
执行debug reload。
执行shutDown且没有开启AOF持久化。系统调用fork()函数,实现了 copy-on-write。
cow :如果有多个调用者同时要求相同资源(如内存或者磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时 ,系统才会复制一份副本专门给调用者而其他的调用者所见到的最初的资源仍然保持不变。
RDB 持久化 缺点:
内存数据的全量同步,数据量大会由于IO而严重影响性能。
可能会因为redis挂掉而丢失从当前至最近一次快照期间的数据。
AOF持久化 保存写状态:记录下除了查询以外的所有的变更数据库状态的指令。
以append的形式追加保存到AOF文件中。(增量)
比如我们要记录某条记录追加一百次,我们最终只要保存一百就可以了 而不需要记录一百次的操作。
日志重写解决AOF文件大小不断增大的问题,原理如下:
调用fork ()创建一个子进程
字进程把一个新的AOF写到一个临时文件里,不依赖原来的AOF文件。
主进程持续将新的变动同时写到内存和原来的AOF里。
主进程获取子进程重写AOF的信号,往新AOF同步增量变动。
使用新的AOF文件替换掉旧的AOF文件。
Redis 的数据恢复RDB和AOF共存的情况下的数据恢复流程:
redis先加载AOF文件是否存在如果存在则加载AOF,不存在的话判断是否存在RDB文件存在的话加载RDB文件,如果不存在 则启动失败。
RDB和AOF的优缺点:
RDB优点: 全量数据快照,文件小,恢复快。
RDB缺点:无法保存最后一次快照之后的数据。
AOF优点:可读性高适合保存增强数据,数据不易丢失。
AOF缺点:文件体积大,恢复时间长。
RDB-AOF混合持久化模式。
就是BGSAVE 做镜像全量持久化,AOF做增量持久化。
七、Redis使用PipeLine
使用PipeLine的好处,
PIpeLine和linux的管道类似,
Redis基于请求相应模型单个请求处理需要一一应答,这样就会发生阻塞,使用PipeLine就可以解决掉。
RTT时长过大频繁磁盘IO,使用Pipeline可以批量执行指令,节省多次IO往返的时间
有顺序依赖的指令建议分批次发送。
八、Redis主从同步
之前老的版本分为两个部分:全同步过程,增量同步过程。
slave发送sync命令道master
master启动一个后台进程,将redis的数据快照保存到文件中,
master将保存数据快照期间接收到的写命令保存起来
master完成写文件操作后,将该文件发送给salve。
使用新的AOF文件替换旧的AOF文件。
master将这期间收集到的增量写指令发送给salve端。
之后进行增量同步过程:
master接收到用户的操作指令,判断是否需要传播到salve
将操作记录追加到aof文件中。
将操作传播到其他的salve:1对齐主从库2往相应缓存写入指令。
将缓存中的数据发送给salve。
旧版本的问题就是增量同步过程:就是如果从服务器发生断线,如果断线时间很短此时发生的写指令也很少,而执行少量的写指令产生的数据通常也会比整个数据库的数据量要少的多,在这种情况下,为了让服务器补足一小部分缺失的数据,却要让主从服务器重新执行一次SYNC命令,效率不高。
新的版本使用完整重同步和部分重同步 而部分重同步可以解决上面的问题
部分重同步分为三个部分:主服务器的复制偏移量、主服务器的复制积压缓冲区、服务器的运行ID。
如果主从服务器的复制偏移量不一样那么就去复制积压缓冲区查找,如果从的偏移量在缓冲区那么就进行部分重同步操作,
如果没有那么就进行完整重同步操作。
每个redis服务器都有自己的运行ID
断线重连后通过服务器运行ID判断重新连接的是否是之前的主服务器如果是则进行部分重同步操作
如果不是则进行完整重同步操作。
Redis Sentinel (redis 哨兵)
解决主从同步master宕机后的主从切换问题。
监控:检查主从服务器是否运行异常
提醒:通过API向管理员或者其他应用程序发送故障通知。
自动故障迁移:主从切换。