Redis
键的过期时间
expire key 5
setex key 5 value
查看还有多久过期
ttl
过期时间存放到 redisDb 对象里面 过期字典 persist 移除过期时间
删除策略
删除策略可能会有三种,分别是定时删除、惰性删除和定期删除。 Redis 选择的是 惰性删除 和 定期删除 搭配使用。 定时删除,添加定时器,到期之后,定时器执行删除操作 惰性删除,放任不管,只有等到下次获取该键时,判断有没有过期。 定期删除,每过一段时间,对数据进行检查,检查和删除的数量,由算法决定。因为 redis 是单线程,不能阻塞线程时间过长。
内存淘汰策略
- LRU(最近最少使用算法):内部维护一个双向链表,在插入或者 get 的时候,将节点放到链表的头部。当内存不足时,删除链表尾部的节点。 但是 Redis 提供一个候选淘汰的 pool,在需要时,随机选取 5 个 key,计算这 5 个 key 的空闲时间,如大于 pool 中的最小时,将其加入 pool, 然后淘汰掉 pool 中空闲时间最大的 key。
- LFU(最近最不常用算法):一个24 bit 的字段拆分成 16 + 8 ,16 位表示访问时间,8 位表示访问频次,默认值是 5 。每次访问时,先对访问频次根据上次访问时间 进行衰减,然后按照一定概率增加访问频次。当内存不足时,删除访问频次最小的 key。
Redis 持久化
RDB
将某个事件点上的数据库状态保存到一个 RDB 文件中
save
阻塞Redis
主线程,直到RDB
文件创建完成,在此期间不会接受其他请求。bgsave
: fork 一个子线程,由这个子线程来创建 RDB 文件。执行期间 save bgsave bgrewriteaof 命令无法执行。bgsave
执行时,bgrewriteaof
命令会等待。bgrewriteaof
执行时,bgsave
命令会被拒绝。
RDB 文件格式
- REDIS 字符串 5 字节
- db_version 4 字节
- databases 数据库
- EOF 字符 1 字节
- checksum 整个 RDB 文件的检验和 8 字节无符号整数
RDB 文件的载入实在程序启动的时候自动进行的(要求 AOF 持久化处于关闭状态,因为 AOF 频率比 RDB 高)
AOF
append only file
通过保存写命令来记录数据库状态的。
- 命令追加 将执行的命令写到 aof 缓冲区里
- 文件写入 将 aof 缓冲区数据写入 aof 文件里面
- 同步
AOF 文件读取过程
- 创建一个伪客户端
- 从 aof 读取一条命令
- 用伪客户端执行这条命令
AOF 重写
为什么要进行 AOF 重写? 因为 AOF 是记录所有的写操作。随着时间的推移,文件会越来越大。
如何重写 AOF 呢? 是通过读取数据库状态来实现的,先读取数据库的数据,然后向新 AOF 文件写入一条命令来记录这些数据。
AOF 后台重写
手动执行 AOF 重写会堵塞 Redis. AOF 后台重写不会堵塞,但是在后台 AOF 的过程中, 会执行新的写命令。Redis 设置了一个 AOF 重写缓冲区。在 AOD 后台重写完成后,会将 AOF 重写缓冲区的数据写到新的 AOF 文件中。将新文件替换旧文件。
总结
- RDB:是保存的数据库的快照,适合大规模的数据恢复,但是会丢失最后一次的数据。
- AOF:是保存的数据库的写操作,适合数据恢复,但是文件会越来越大。
RDB 在处理过期数据的时候是直接忽略掉,AOF 是会记录的,但是加上一条 DEL 命令来删除过期的数据。
Redis为什么这么快?
-
内存存储:Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1)。
-
单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。注意:单线程是指的是在核心网络模型中,网络请求模块使用一个线程来处理,即一个线程处理所有网络请求。
-
非阻塞IO:Redis使用多路复用IO技术,将epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
-
优化的数据结构:Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能。
-
使用底层模型不同:Redis直接自己构建了 VM (虚拟内存)机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
Redis的VM(虚拟内存)机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
Redis提高数据库容量的办法有两种:一种是可以将数据分割到多个RedisServer上;另一种是使用虚拟内存把那些不经常访问的数据交换到磁盘上。需要特别注意的是Redis并没有使用OS提供的Swap,而是自己实现。
Redis的数据类型有哪些?
有五种常用数据类型:String、Hash、Set、List、SortedSet。以及三种特殊的数据类型:Bitmap、HyperLogLog、Geospatial ,其中HyperLogLog、Bitmap的底层都是 String 数据类型,Geospatial 的底层是 Sorted Set 数据类型。
五种常用的数据类型:
1、String:String是最常用的一种数据类型,普通的key- value 存储都可以归为此类。其中Value既可以是数字也可以是字符串。使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。
2、Hash:Hash 是一个键值(key => value)对集合。Redishash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值。
3、Set:Set是一个无序的天然去重的集合,即Key-Set。此外还提供了交集、并集等一系列直接操作集合的方法,对于求共同好友、共同关注什么的功能实现特别方便。
4、List:List是一个有序可重复的集合,其遵循FIFO的原则,底层是依赖双向链表实现的,因此支持正向、反向双重查找。通过List,我们可以很方面的获得类似于最新回复这类的功能实现。
5、SortedSet:类似于java中的TreeSet,是Set的可排序版。此外还支持优先级排序,维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景。
三种特殊的数据类型:
1、Bitmap:位图,Bitmap想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在Bitmap中叫做偏移量。使用Bitmap实现统计功能,更省空间。如果只需要 统计数据的二值状态,例如商品有没有、用户在不在等,就可以使用 Bitmap,因为它只用一个 bit 位就能表示 0 或 1。
2、Hyperloglog。HyperLogLog 是一种用于统计基数的数据集合类型,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大
时,计算基数所需的空间总是固定 的、并且是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。场景:统计网页的UV(即Unique Visitor,不重复访客,一个人访问某个网站多次,但是还是只计算为一次)。
要注意,HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。
3、Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如朋友的定位、附近的人、打车距离计算等。
主从
- 同步 sync : 将 slave 服务器的状态更新至与 master 服务器一致。(从服务器侧发起)
- 传播 propagate:master 变更后,将 slave 服务器状态更新至与 master 服务器一致。(主服务器侧发起)
sync 的过程
- slave 向 master 发送 SYNC 命令。
- master 收到 SYNC 后,在后台执行一个 bgsave 命令,并将此后的写命令写到缓冲区里
- bgsave 命令执行完成后,将生成的 RDB 文件发送给 slave
- 将缓冲区的数据发送给 slave
命令传播
master 将在自己身上执行的写命令发送给 slave。
断线后重同步问题
slave 执行完 SYNC 后,在 master 上有一些修改,在 master propagate 到 slave 的时候,slave 与 master 断连,经过一段时间后,slave 重新连接。 此时会重新执行 SYNC 命令,但 SYNC 很耗资源。 Redis 提供了 RSYNC 命令。
PSYNC 分为两种模式。
- 完整重同步:与 SYNC 一致
- 部分重同步:只将断连后的命令发送给 slave。
- 主从服务器都会维护自己的复制偏移量
- master 的复制积压缓冲区 (默认 1 M)
- 服务器的运行 ID
主从模式数据丢失的情况
- master 到 slave 未完成的时候,master 宕机
- 脑裂,存在多个 master。
解决方案:要满足当至少有一个 slave 同步不超过 10s ,否则 master 就不接收命令(客户端可将命令写入消息队列)。
min-slaves-to-write 1
min-slaves-max-lag 10
redis-sentinel 哨兵
Redis哨兵是怎么工作的?
-
每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。
-
如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被当前 Sentinel 标记为主观下线。
-
如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
-
当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。
-
当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 (在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 )。
-
若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会变成主观下线。若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
-
sentinel节点会与其他sentinel节点进行“沟通”,投票选举一个sentinel节点进行故障处理,在从节点中选取一个主节点,其他从节点挂载到新的主节点上自动复制新主节点的数据。
故障转移时会从剩下的slave选举一个新的master,被选举为master的标准是什么?
如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来,会考虑slave的一些信息。
- 跟master断开连接的时长。 如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master.
( down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
-
slave优先级。 按照slave优先级进行排序,slave priority越低,优先级就越高
-
复制offset。 如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
-
run id 如果上面两个条件都相同,那么选择一个run id比较小的那个slave。
同步配置的时候其他哨兵根据什么更新自己的配置呢?
执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的。
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch 作为新的version号。
这个version号就很重要了,因为各种消息都是通过一个channel去发 布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的,其他的哨兵都是根据版本号的大小来更新自己的master配置的。
集群
redis cluster
通过分片 sharding
来进行数据共享,并提供复制和故障转移功能。
节点
cluster meet ip:port
将当前节点与指定节点进行握手。
cluster node
查看集群的节点。
槽指派
集群的整个数据库分为 16834 个 slot
在集群中执行命令
可能会出现 2 个错误。
- MOVED:节点发现键所在的 slot 不是自己复制的时候,会报 MOVED 错误。
- ASK:重新分片的时候,访问的 key 属于被移动的 slot 时,如果节点在自己的数据库没有找到这个 key,则会报 ASK 错误。
节点也分为主节点和从节点。
秒杀系统
- 全局唯一 id (全局 ID 生成器)
- UUID
- Redis 自增 (incr命令)
- 雪花算法
- 数据库自增
- 超卖问题(乐观锁)
- 悲观锁
- synchronized
- Lock
- 共享锁(一般是在修改数据时使用):核心时在更新数据之前判断数据是否被修改过
- 使用版本号机制, set stock = stock-1,verison = verison+1 where stock = ? and version = ?
- CAS : set stock = stock -1 where id = ? and stock = ?
- 问题:成功率低
- set stock = stock -1 where id = ? and stock > 0
- 一人一单 (悲观锁)
synchronzied(userId.toString().intern()){ .... }
需要注意事务问题
- 集群模式下的问题 synchronized 只适用于单体环境。
使用分布式锁来解决这个问题。
set lock thread1 nx ex 10
del lock
锁误删
- 线程超时,误删其他线程的锁。 加uuid
- 执行
del lock 超时
,误删其他线程的锁。
用 lua 脚本保证“判断锁是否是当前线程加的,已经删除锁” 是一个原子操作。
不可重入 不可重试 超时释放 主从一致性
Redisson 客户端提供了分布所的实现。
- 可重入锁的原理:hash 结构,存储 线程id 和次数
keys 与 scan 命令
语法
keys pattern
scan cursor [match pattern] [count count]
scan
需要注意的是:第一次执行的时候需要指定 cursor
为 0
,如果指定了 count
,返回结果不一定是 指定的 count
个。
并且返回的结果是一个空数组时,也不能表示查找结束,只有当返回点 cursor
为 0
时才表示查找结束。scan查找的结果会有重复需要自己过滤。
keys
会一次查询出所有匹配的 key
, 会阻塞 redis
服务器。
scan
会一次查询出一部分匹配的 key
,不会阻塞 redis
服务器。