一次性、顺序性、排他性
redis单条命令保证原子性,但是事务不支持原子性
原因:异常情况
命令有错(编译时异常):事务中所有的命令都不会执行
运行时异常:其他命令正常执行,错误命令抛异常 不满足原子性
事务执行顺序:开启(multi)、命令入队、执行命令(exec)
用watch命令监控实现乐观锁:事务提交时,如果Key的值已被别的客户端改变,比如某个list已被别的客户端push/pop过了,整个事务队列都不会被执行。通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Nullmulti-bulk应答以通知调用者事务执行失败
概念:RDB是一种把当前内存中的数据集快照写入磁盘,恢复时将快照文件直接读到内存里的一种默认开启的持久化方式
生成dump.rdb文件
过程原理:Redis单独创建(fork)一个子进程来进行持久化,会先将数据写到一个临时文件中,带持久化过程都结束了,在用这个临时文件替换掉上次持久化好的文件,整个过程,主线程不进行任何IO操作,确保了极高的性能
优点:
缺点:
概念:以日志的形式记录每一个写操作(读不记录),只允许追加文件,但不可以改写文件,Redis启动之后会主动读取该文件重新构建数据。即Redis重启就根据日志文件的内容将写指令执行一次以完成数据的恢复
生成appendonly.aof文件
如果文件出错,Redis无法启动
AOF文件的重写:当AOF文件达到64M(可配置)的时候,Redis就会开辟子进程启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。同时Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区是在创建子进程后开始使用,当Redis服务器执行一个写命令之后,就会将这个写命令也发送到 AOF 重写缓冲区。当子进程完成 AOF 重写之后,就会给父进程发送一个信号,父进程接收此信号后,就会调用函数将 AOF 重写缓冲区的内容都写到新的 AOF 文件中
优点:
可以通过修改配置(appendfsync)进行不同频率的同步
always:每一次修改都同步(完整性更高)
everysec:每秒同步一次,可能丢失一秒的数据
no:操作系统自己同步,速度最快
缺点:
redis | rabbitmq | |
---|---|---|
可靠性 | 没有相应的机制保证消息的可靠消费,如果发布者发布一条消息,而没有对应的订阅者的话,这条消息将丢失,不会存在内存中 | 具有消息消费确认机制,如果发布一条消息,还没有消费者消费该队列,那么这条消息将一直存放在队列中,直到有消费者消费了该条消息,以此可以保证消息的可靠消费 |
实时性 | redis作为高效的缓存服务器,所有数据都存在在服务器中,所以它具有更高的实时性 | |
消费者负载均衡 | 发布订阅模式,一个队列可以被多个消费者同时订阅,当有消息到达时,会将该消息依次发送给每个订阅者 | 队列可以被多个消费者同时监控消费,但是每一条消息只能被消费一次,由于rabbitmq的消费确认机制,因此它能够根据消费者的消费能力而调整它的负载 |
持久性 | redis的持久化是针对于整个redis缓存的内容,它有RDB和AOF两种持久化方式(redis持久化方式,后续更新),可以将整个redis实例持久化到磁盘,以此来做数据备份,防止异常情况下导致数据丢失。 | 队列消息都可以选择性持久化,持久化粒度更小,更灵活; |
队列监控 | redis没有所谓的监控平台。 | rabbitmq实现了后台监控平台,可以在该平台上看到所有创建的队列的详细情况,良好的后台管理平台可以方面我们更好的使用; |
为了分担读压力,Redis支持主从复制
主从复制方式:修改配置/slaveof命令(暂时)
主从复制类型:全量复制、增量复制
全量复制:
补充说明: 在上述全量复制的流程中,对 master 的性能损耗较大,slave 构建数据的时间也比较长,而且传递 rdb 时还会占用大量带宽,对整个系统的性能和资源的访问影响都比较大。如果master上的磁盘空间有限,那么此时全量同步对于master来说将是一份十分有压力的操作了。可以通过无盘复制来达到目的,由master直接开启一个socket将rdb文件发送给slave服务器。(无盘复制一般应用在磁盘空间有限但是网络状态良好的情况下)
增量复制:
Redis2.8之前主从复制功能缺陷
在 Redis 中,从服务器对主服务器的复制分为两种情况:
对于初次复制来说,旧版复制功能能够很好的完成任务,但对于断线后重新复制来说,旧版复制功能虽然也能让主从服务器数据一致(重新发起SYNC请求,主服务器重新生成RDB文件进行同步),但效率却非常低(可能主从断开连接过程中主服务器只执行了一条写命令却要重新生成整个RDB文件,耗费CPU,再传输给从服务器,耗费网络带宽)。
Redis 2.8 之后复制功能的实现
为了解决旧版复制功能在处理断线重新复制情况下的低效问题,Redis 从 2.8 版本开始,使用 psync命令代替了 sync 命令来执行复制时的同步操作。
psync 命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式。
部分重同步的实现
复制偏移量
执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量
对比主从服务器的复制偏移量可知:
复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度的先进先出(FIFO)队列,默认大小为 1MB
当主服务器进行命令传播时,不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面
因此,主服务器的复制积压缓冲区里面会保存着一部分最近传播的写命令,并且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量
当从服务器重新连上主服务器时,从服务器会通过 PSYNC 命令将自己的复制偏移量(offset) 发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:
注意:建议根据下述公式调整复制积压缓冲区的大小为:2 * second * write_size_per_second
服务器运行ID
当从服务器对主服务器进行初次复制时,主服务器会将自己的运行 ID 传送给从服务器,而从服务器会将这个运行 ID 保存起来,
当从服务器断线并重新连上主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行 ID:
背景:常见主从复制模型主节点Master 只有一个,一旦主节点挂掉之后,从节点没法担起主节点的任务,那么整个系统也无法运行,为了保证主节点挂掉之后,从节点能自动变成主节点,采用哨兵模式
原理:哨兵作为独立运行的进程,通过发送命令,等待Redis服务器的响应,从而监控多个运行实例。当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机
进阶:哨兵模式也存在单点故障问题,如果哨兵机器挂了,那么就无法进行监控了,为此,我们可以使用多个哨兵建立集群进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式
多哨兵模式:假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover(故障转移)过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线
选择新的master节点原则:
数据分区方式:常见的Redis集群架构是三主三从的结构,为了保证数据分片,Redis采用了Hash槽的概念
为了保证集群中新增节点或者删除节点时,数据迁移量最少,不采用顺序分布、哈希分布,一致性哈希算法诞生
一致性哈希:一致性Hash是成一个Hash环,按照常用的Hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,当节点增加或者删除的时候,在环上顺时针找到对应节点,迁移数据
虚拟槽分区:Redis内部内置了序号 0-16383 个槽位,每个槽位可以用来存储一个数据集合,将这些槽位按顺序分配到集群中的各个节点。每次新的数据到来,会通过哈希函数 CRC16(key) 算出将要存储的槽位下标,然后通过该下标找到前面分配的Redis节点,最后将数据存储到该节点中
概念:太多请求缓存未命中,直接请求到数据库,数据库压力大
解决方式:
布隆过滤器:通过bitmap或Redission实现的一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进性校验,不符合则丢弃,从而避免对底层数据库的查询压力
缓存空对象:当底层数据库不命中时,即使返回空对象也要将其缓存起来,同时设置合理的过期时间
存在的问题:
背景:在Redis内部,每当我们设置一个键的过期时间时,Redis就会将该键带上过期时间存放到一个过期字典中。当我们查询一个键时,Redis便首先检查该键是否存在过期字典中,如果存在,那就获取其过期时间。然后将过期时间和当前系统时间进行比对,比系统时间大,那就没有过期;反之判定该键过期
过期删除策略
在设置某个key的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作
优点:定时删除对内存是最友好的,能够保存内存的key一旦过期就能立即从内存中删除
缺点:对CPU不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响
设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key
优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。从而造成内存泄漏
Redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存
缺点:难以确定删除操作执行的时长和频率;如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好;如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放;另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误
Redis结合了定期删除和惰性删除,但是如果定期删除漏掉了很多过期key,然后也没及时去查,也就没走惰性删除,大量过期key堆积在内存里,导致redis内存块耗尽 ——使用内存淘汰机制
背景:当现有内存大于 maxmemory 时,便会触发redis主动淘汰内存方式,通过设置 maxmemory-policy,设置不同的内存淘汰机制
内存淘汰机制
扩展:LFU算法
(https://www.jianshu.com/p/8f2fb61097b8)
redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理
Redis单线程执行,性能高的原因
I/O多路复用
用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。这样用户可以注册多个socket,然后不断地调用select读取被激活的socket,redis服务端将这些socket置于队列中,然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中,提高读取效率
采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作,从而提高效率
背景:更新Mysql数据后,需要同步redis中缓存的数据
同步方式:删除缓存,重新从数据库中读取
如果是更新缓存,存在分布式事务问题,可能出现修改了缓存,数据库修改失败的情况。只是删除缓存的话,就算数据库修改失败,下次查询会直接取数据库的数据,也不会出现脏数据
为什么要进行延迟双删
一般我们在更新数据库数据时,需要同步Redis中缓存的数据
所以存在两种方法:
(1)第一种方案:先执行update操作,再执行缓存清除
(2)第二种方案:先执行缓存清除,再执行update操作
弊端:当存在并发请求时,很容易出现问题
(1)第一种方案:当请求1执行update操作后,还未来得及进行缓存清除,此时请求2查询到并使用了Redis中的旧数据
(2)第二种方案:当请求1执行清除缓存后,还未进行update操作,此时请求2进行查询到了旧数据并写入了Redis
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间
public void write(String key,Object data){ redis.delKey(key); db.updateData(data); Thread.sleep(500); redis.delKey(key); }
具体的步骤就是:
先删除缓存;再写数据库;休眠500毫秒(根据业务场景自行确定);再次删除缓存
休眠目的:
确保读请求结束,写请求可以删除读请求造成的缓存脏数据