Redis入门
简介
redis:用于将各种数据存储在内存中,然后可以将这些存储在内存的数据转存到磁盘中(这就叫持久化)。
redis的作用:将数据暂时存储在内存中,这样访问数据的时候就不用去磁盘了,这样速度比较快。
默认使用端口:6379
外部程序使用 TCP 套接字和 Redis 特定协议与 Redis 通信。该协议在不同编程语言的 Redis 客户端库中实现。
安装和使用
安装:
sudo apt install lsb-release
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt-get update
sudo apt-get install redis
安装完redis,默认自动开启,然后使用redis-cli连接redis:
$ redis-cli
127.0.0.1:6379>
检查redis是否正常运行:
$ redis-cli ping
PONG
使用:
Redis的key是二进制安全的,所以key可以是字符串、图片等所有内容。
- 但是key最好不要太大,不然会太耗内存且查找困难。当key太大时,可以对key进行哈希处理。key的最大值未512 MB。
- key太小会导致可读性较差,key长度的增加相对于value来说一般较小,所以key可以适当的大一些,让其具有更好的可读性。
Strings:Strings就是一些字节序,可以存储任何信息,所以Memcached中只有Strings一种类型就够用了。
> set mykey somevalue
OK
> get mykey
"somevalue"
数据类型篇
SDS
用途:
- redis中的kv键值对中的字符串是由SDS实现。
比如RPUSH fruits "apple" "banana" "cherry"
中,fruits是一个字符串对象,底层是由SDS实现的。"apple" "banana" "cherry"
是一个列表对象,列表中的每个元素是一个字符串对象,每个字符串对象是由SDS实现的。 - AOF缓冲区,以及客户端状态中的输人缓冲区,都是由SDS实现的,
数据结构:
- 使用
\0
作为SDS的结尾,与C语言中的字符串一致,这样就可以使用C标准库中的一些函数。 - len:保存了字符串的长度(长度不包括
\0
)。
空间分配的优化策略:
- 空间预分配:
- 如果SDS长度(len)小于1M时,那么程序会额外分配len的空闲空间,总空间为(len+len+1)个字节,其中1为
\0
。 - 如果SDS长度(len)大于等于1M时,那么程序会额外分配1M的空闲空间。
- 如果SDS长度(len)小于1M时,那么程序会额外分配len的空闲空间,总空间为(len+len+1)个字节,其中1为
- 惰性空间释放:当收缩SDS时,不立即释放空闲的空间,而是等待将来使用
二进制安全:与C语言不同,SDS中允许在非结尾处存在\0
,可以用于保存二进制数据,如图片、音频等。这可能是因为数据结构中保存了len,所以SDS中可以允许在非结尾处存在\0
。
双端链表
用途:
- 数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
- 除了链表键之外,发布与订阅、慢查询、监视器等功能也用到了链表,Redis 服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区( output buffer), 本书后续的章节将陆续对这些链表应用进行介绍。
数据结构:
- 每个结点都有指向前后结点的指针。
- 链表有头指针和尾指针。
- 有表示链表长度的len。
字典
redis数据库的元素由一个个kv组成,它的底层就是使用字典来实现。
用途:哈希键中的键值较多,或元素都是较长的字符串时会使用。
数据结构:字典的底层实现使用的哈希表,如下:
- 计算key的哈希值,从而得到key对应在dictEntry中的索引。如果多个key对应同一个索引,就将这些相同索引的key使用链表连接起来(链地址法来解决键冲突)。
- ht[0]哈希表用于查找kv,ht[1]哈希表用于进行rehash。
- rehashidx记录了rehash到哈希表的哪个下标了。
- sizemask:将key映射到某个索引值时会使用到。
- size:哈希表的大小
- used:节点个数
Rehash的含义:当哈希表中的元素数量逐渐增加,可能会导致哈希表中的索引位置分布不均匀,从而引起哈希冲突(两个不同的键映射到了同一个索引位置)。为了解决这个问题,哈希表会进行扩容操作,这个操作就称为“rehash”。
rehash过程:
- 扩展和收缩的大小:
- 扩展:ht[1]的大小为第一个大于等于ht[0].used*2的\(2^n\)
- 收缩:ht[1]的大小为第一个大于等于ht[0].used的\(2^n\)
- 通过重新计算每个key的哈希值和索引值,从而将所有键值对从ht[0]迁移到ht[1]上,然后ht[1]设为ht[0],创建一个新的空白哈希表为ht[1]。
rehash 触发条件
- 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
- 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。
负载因子设置较大的原因:当进行RDB快照或AOF重写时,系统可能会有大量的IO操作和CPU使用,这可能会导致系统负载升高。如果此时再执行rehash操作,会进一步增加系统的负载,可能导致响应时间变慢,甚至系统崩溃。
【注】负载因子=哈希表已保存节点数量 / 哈希表大小
渐进式rehash:在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。这样避免了一次性 rehash 的耗时操作。
- 查找键:rehash期间,会先查找ht[0],没找到,会查找ht[1]
- 新增的键值会被添加到ht[1]中
跳表
可以先看看这个简单的跳表。
用途:一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表。
数据结构:两个数据机构——头和节点。
- header指向跳表的头,tail指向跳表的尾。
- BW用于从尾向前遍历。
- 成员对象(obj)与分值:每个节点都有一个obj指针指向字符串对象,而字符串对象保存着一个SDS值。分值用来在让对象在跳表中排序,如果分值相同,则按照字典序进行排序。
【注】每个节点中的成员对象都应该是唯一的,因为要作为有序集合的低层实现,集合的元素当然不能重复。
整数集合
整数集合的用途:集合键只包含整数元素,且元素个数不多时使用。
数据结构:
- encoding:代表contents中元素的类型,可以是int16_t、int32_t或者int64_t
整数集合特点:
- 从小到大排序,保证不出现重复元素。
- 升级:如int16的数组中有3个元素,那么添加65535时,由于65535大于2^16-1,所以数组中的所有元素都会转换为int32,故需要分配的空间为
4*32-3*16
。
向整数集合添加元素的时间复杂度为O(n)
压缩列表
压缩列表是一种为节约内存而开发的顺序型数据结构。
压缩列表的用途:列表键和哈希键中元素较少且都是较短的整数或字符串时,会使用压缩列表来实现列表键和哈希键。
【注】列表键是什么:redis中每个元素都是k:v
,列表键代表v是一个列表。如果列表中的元素都是较短的整数或字符串时,会使用压缩列表来实现列表键。
【注】哈希键是什么:redis中每个元素都是k:v
,哈希键代表v中的每个元素都是k:v
。如果哈希中的元素都是较短的整数或字符串时,会使用压缩列表来实现哈希键。
压缩列表的数据结构:由多个节点组成,每个节点保存字节数组或一个整数值。列表结构:
- zlbytes:压缩列表占用的字节数
- zltail:起始到尾部有多少个字节,用于定位尾部节点。
- zllen:节点数量
- entry:节点
- zlend:用于代表列表结束,类似字符串中的
\0
节点结构:
- previous_entry_length:前一个节点的长度。通过previous_entry_length就可以从尾部开始向前遍历节点。
- encoding:记录了节点的content的数据的类型(字节数组或整数)以及长度。
- content:数据内容。
previous_entry_length和encoding节省空间的方式:
- previous_entry_length和encoding:对于较长的字节数组就使用较多的字节来表示其长度,较短的字节数组就使用较少的字节来表示其长度,从而减少空间的浪费。
- previous_entry_length和encoding的前几个字节用于表示此数据类型的长度用几个字节来表示或者表示当前节点是什么数据类型,后面的字节代表数据的长度。
连锁更新:添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。下面是这两种情况的介绍:
-
添加节点new导致的连锁更新:new的长度大于等于254,所以可能需要扩展e1的previous_entry_length字段到五个字节。e1长度变长了,e2的previous_entry_length字节可能也需要变长,依此类推,后面的每个字节都可能需要更新。
-
删除节点new导致的连锁更新:big大于等于254,small小于254,所以删除small,可能引起连锁更新。
对象
对象简介:
- redis有字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象低层使用到了前面提到的数据结构中的一种或多种进行实现。针对不同场景,对象的低层可能使用不同的数据结构进行实现。
- redis中存储的是键值对,其中键只能是用字符串对象,值可以是五种类型中的任意一种。
- 使用类似shared_ptr的引用计数的方式回收内存和共享对象。
- Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。【没看懂】
对象的数据机构:
- type:对象的类型。
- encoding:对象的类型低层采用的数据结构。
- ptr:指向对象的底层实现数据结构
字符串键对象
字符串对象的几种encoding:
- int:底层使用long实现。如果一个整数可以使用long表示,那么就使用long作为底层。
- raw:长度大于32字节,就用SDS。
- embstr:长度小于等于32字节,就用embstr。
- embstr调用一次内存分配函数来分配一块连续的空间。raw中创建和释放字符串对象需要两次内存操作,embstr只需要一次。
- 连续的内存,可以更好载入到缓存中。
【不懂】我不太清楚书中讲long double 的目的。
编码转换:
- int如果添加了字符,那么就会从int转为raw。
- embstr是只读的,对embstr执行修改操作,会先将对象的编码从embstr转换成raw,然后冉执行修改命令。因为这个原因,embstr编码的字符串对象在执行修改命令之后,总会变成一个raw编码的字符串对象。
字符串命令的实现:
列表对象
列表对象的几种encoding:
- 压缩列表(ziplist编码):列表键中元素较少且都是较短的整数或字符串时
- 省内存,且由于保存在连续内存中,所以可以更快载入到缓存中。
- 双端链表(linkedlist编码):大量元素时
编码转换:当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码。
- 列表对象保存的所有字符串元素的长度都小于64字节;
- 列表对象保存的元素数量小于512个;
不能满足这两个条件中的任意一个的列表对象需要使用linkedlist编码。
列表命令的实现:
哈希对象
哈希对象的几种encoding:
- 压缩列表(ziplist):按照kv连续存放,每次填到到列表尾部。
- 哈希表(hashtable):k和v都是一个字符串对象。
编码转换:
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码。
- 哈希对象保存的所有键或值的长度都小于64字节;
- 哈希对象保存的键值对数量小于512个;
不能满足这两个条件中的任意一个的哈希对象需要使用hashtable编码。
哈希命令的实现:
集合对象
集合对象的几种encoding:
- 整数集合(intset)
- 哈希表(hashtable):每个键都是字符串对象,值都为NULL。
编码转换:
当集合对象可以同时满足以下两个条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过512个。
不能满足这两个条件的任意一个的集合对象需要使用hashtable编码。
集合命令的实现:
有序集合对象
有序集合对象的几种encoding:
- 压缩列表(ziplist):每个元素除了元素本身以外,还有一个分值用于排序。分值是插入时,需要指定的。
- 跳表(skiplist):同时使用了跳表和字典来实现。两种数据结构都会通过指针来共享相同元素的成员和分值
- 跳表的每个元素中保存了成员和分值,分配用于排序。
- 字典:用0(1)复杂度查找给定成员的分值。只使用字典来保存元素是不行的,因为执行ZRANK、ZRANGE等命令时,需要对元素进行排序,排序的代价是很大的。
编码转换:
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:
- 有序集合保存的元素数量小于128个;
- 有序集合保存的所有元素成员的长度都小于64字节;
不能满足以上两个条件的任意一个的有序集合对象将使用skiplist编码。
有序集合命令的实现:
其他
类型检查与命令多态
- 不同类型可以执行的命令不同,所以执行某个命令时redis会检查redisObiect结构的type属性来判断此类型是否可以执行此命令。
- 对象不同的低层实现在处理时调用的函数不同,通过redisObiect结构的encoding去调用不同的操作函数。
内存回收:使用类似shared_ptr的引用计数的方式回收内存和共享对象。
- Redis只对包含整数值的字符串对象进行共享。目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
对象的空转时长:redisObject的lru属性记录了对象最后一次被命令程序访问的时间。在服务器启用了maxmemory功能的情况下,当服务器占用的内存数超过了ma xmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。
持久化篇
AOF(Append Only File) 持久化
AOF(Append Only File) 持久化:Redis 每执行一条写操作命令后,就把该命令以追加的方式写入到一个文件(即AOF日志文件)里,然后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它,这就相当于恢复了缓存数据了。
持久化存在的问题:命令未写到磁盘、影响命令执行。
- 执行命令后,服务器宕机,命令还未写入AOF日志文件中
- 将命令写入写入AOF日志文件中是需要时间的,这会影响下一个写命令的执行。
redis将日志写入到文件中的过程:
- 1.执行命令,并将此命令以日志的形式记录在用户缓冲区中
- 2.write命令将用户缓冲区数据复制到内核缓冲区中。
- 3.将内核缓冲区数据写入到磁盘上。
三种将命令写入AOF日志文件的策略:redis中可以调用fsync()函数立马将硬盘数据内核缓冲区数据写到硬盘,从而有了下面三种策略。
- Always:每次执行完命令以后,立即将命令写入到AOF日志文件中。(高可靠、低性能)【立即调用fsync(),同步执行影响主进程】
- Everysec:先将命令写入到内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;【每秒执行一次fsync(),异步执行不影响主进程 】
- NO:先将命令写入到内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。(高性能、低可靠)【不执行fsync()】
AOF 重写机制:
- 含义:假设前后执行了「set name xiaolin」和「set name xiaolincoding」。那么在使用重写机制后,就会读取 name 最新的 value(键值对) ,然后用一条 「set name xiaolincoding」命令记录到新的 AOF 文件,从而压缩AOF文件。
- 实现:AOF 重写机制是在重写时,读取当前数据库(全在在内存中)中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
子进程实现重写:
- 避免阻塞主进程;
- 写时复制:
- 在 Unix-like 操作系统中,主进程在通过 fork 系统调用生成子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。这样做减少了fork执行的时间。但是页表太大时,子进程创建的时间就会较长,即主进程阻塞时间较长。
- 当其中一个进程尝试修改其中一个页,操作系统会为该进程复制该页,使得两个进程的内存数据分离,互不影响。
使用子进程,不使用线程的原因?
有两个阶段会导致阻塞父进程:
- 创建子进程时,拷贝页表等数据结构。
- 如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存。
- 将AOF重写缓冲区的日志写到文件中。
AOF 重写流程图:
AOF重写缓冲区:重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,这个命令会被保存在AOF重写缓冲区中。子进程完成 AOF 重写工作会向主进程发送一条信号,然后主进程将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中。最后新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。
RDB 快照
RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
创建一个子进程来生成 RDB 文件
或者直接在主进程中保存RDB文件。
RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。
,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。
写时复制技术
如果主线程(父进程)要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A'),然后主线程在这个数据副本(键值对 A')进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对 A)写入到 RDB 文件。
发生了写时复制后,RDB 快照保存的是原本的内存数据
RDB 快照都无法写入主线程刚修改的数据
,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍。
所以,针对写操作多的场景,我们要留意下快照过程中内存的变化,防止内存被占满了。
RDB +AOF时, AOF 日志重写过程 :
- 在 AOF 重写日志时,fork 出来的重写子进程会先将与主进程共享的内存数据以 RDB 方式写入到 AOF 文件。
- 子进程完成 AOF 重写工作会向主进程发送一条信号,然后主进程将 AOF 重写缓冲区中的所有内容以 AOF 方式写入到 AOF 文件。
故,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。
Redis 大 Key 对持久化有什么影响?看总结。。。
Linux 开启了内存大页,会影响 Redis 的性能的。即每一页可以有2MB
??
但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
LFU 算法是根据数据访问次数来淘汰数据的
redis中LFU的具体公式
缓存篇
缓存雪崩、击穿、穿透
当用户的请求,都访问数据库的话,请求数量一上来,数据库很容易就奔溃的了,所以为了避免用户直接访问数据库,会用 Redis 作为缓存层。引入了缓存层,就会有缓存异常的三个问题,分别是缓存雪崩、缓存击穿、缓存穿透。
缓存雪崩:
大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,大量用户直接访问数据库,导致数据库承受不住。
- 大量数据同时过期;
- 过期时间最好加上一个随机数,让缓存不在同一个时间失效。
- 将要可能要被访问的热点数据预先加载到缓存中,并设置不同的过期时间。
- 将热点的key放在不同的节点上。
- 控制请求的数量。
- 后台不断检测缓存,有缓存失效就添加数据到缓存,总是保留一定的缓存,防止大量缓存失效。
- Redis故障宕机:
- 暂停服务或只允许一定数量的请求。
- 集群。
缓存击穿:
同一时间大量请求访问某些数据,但是这些数据不在缓存中,大量用户直接访问数据库,导致数据库承受不住。
- 不给热点数据设定过期时间
- 互斥锁:保证同一个时间只有一个请求可以更新缓存,当有一个请求将热点数据放到了缓存中以后,其他请求就可以直接从缓存中访问数据。
缓存穿透:
大量请求访问缓存和数据库中不存在的数据,就会导致数据库的压力骤增。
- 非法请求的限制:
- 直接在API入口处判断请求参数是否合法。
- 针对查询的数据,在缓存中设置一个空值或者默认值
- 缓存空值或者默认值;
- 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
缓存穿透发生的可能:
- 黑客故意访问
- 不小心删掉了。
数据库与缓存的一致性保证(redis更新的方式)
数据库和缓存如何保证一致性:由于引入了缓存,那么在数据更新时,不仅要更新数据库,而且要更新缓存,这两个更新操作存在前后的问题:
- 先更新数据库,再更新缓存的问题:A 请求先将数据库的数据更新为 1,然后在更新缓存前,请求 B 将数据库的数据更新为 2,紧接着也把缓存更新为 2,然后 A 请求更新缓存为 1。
- 先更新缓存,再更新数据库的问题:A 请求先将缓存的数据更新为 1,然后在更新数据库前,B 请求来了, 将缓存的数据更新为 2,紧接着把数据库更新为 2,然后 A 请求将数据库的数据更新为 1。
- 总结:上述发生不一致的原因都是上一次未执行完的写命令的旧数据覆盖了最新的更新。
Cache Aside 策略(旁路缓存策略):不更新缓存,而是删除缓存中的数据。然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
- 写策略的步骤:更新数据库中的数据和删除缓存中的数据,先更新还是先删除:
- 先删除缓存,再更新数据库:删除缓存以后,还没来得及更数据库,此时读命令会将旧的数据写入到缓存中。解决这个问题的方法:[删除缓存+更新数据库+睡眠一段时间+删除缓存],睡眠一段时间保证读命令执行完成。但是睡眠时间很难确定,所以这种方法一般不用。
- 先更新数据库,再删除缓存:读取数据时,还没将未命中缓存写到缓存中,此时如果发生写命令先更新数据库并删除缓存,不会删除到缓存,然后读命令会将旧的数据写入缓存中。
因为缓存的写入通常要远远快于数据库的写入,所以上述情况很难发生,故“先更新数据库,再删除缓存”可行。为了保证可靠性,还给每个缓存数据加上了过期时间,这样就可以通过过期时间将旧缓存删除。如果删除缓存失败了,也可以通过过期时间将旧缓存删除。 - 总结:写策略发生不一致都是由于读命令将旧数据拷贝到缓存中了。
- 读策略的步骤:
- 如果读取的数据命中了缓存,则直接返回数据;
- 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。
「先更新数据库,再删除缓存」每次更新数据都需要删除缓存,这就导致缓存命中率降低。所以,如果我们的业务对缓存命中率有很高的要求,我们可以采用「更新数据库 + 更新缓存」的方案,因为更新缓存并不会出现缓存未命中的情况。两种做法:
- 在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
- 在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。
「先更新数据库,再删除缓存」如何保证“删除缓存”不是失败的:
- 将要删除的数据放入队列中,然后不断尝试删除,多次尝试失败就告诉返回错误。如果成功就将数据从队列中删除。
- 订阅 MySQL binlog,再删除操作缓存:更新数据库成功,就会产生一条变更日志,记录在 binlog 里。订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除。【没看懂,怎么保证删除成功的?】
redis分布式锁
参考:视频、
分布式锁:就是多机器访问数据库时,需要加上分布式锁。
redis实现分布式锁:使用setNX设置分布式锁,setNX就是向redis的某个key中存入value,如果存储成功则认为获取到锁。如果value非空,则其他机器就无法获取到锁
redis分布式锁使用redisson来实现:
- 使用setNX设置分布式锁,并设置过期时间和看门狗。看门狗负责定时查看是否有任务在执行,如果有,就不释放锁。
- 每一次加锁都有一个UUID,从而防止当前锁被其他线程解锁。
redlock:如果redis使用主从集群的话,那么需要redlock。redlock保证了集群中每个节点都获取到了锁,才认为加锁成功。
布隆过滤器
使用N个哈希函数对x计算哈希值,然后将对应位图上的位置置为1。所以如果y经过N个哈希函数计算出来的值对应到位图上的都为1,则代表元素存在。
特点:认为存在,则不一定存在;认为不存在,则有一定不存在。
redis过期策略和内存淘汰
过期时间设置方法:
- 设置在多久后过期
- 设置到哪个时间戳时过期。
过期字典中保存着所有key的过期时间,使用哈希表实现,查找复杂度为O(1)
Redis 选择「惰性删除+定期删除」这两种策略配和使用
- 惰性删除:访问时发现过期则删除
- 定期删除:每个一段时间(每秒十次)随机选20个key,将其中过期的进行删除,过期超过25%,则继续随机选取,最多操作 25ms。
内存淘汰策略:超过最大内存以后采取的策略。
- 不淘汰数据,报超出内存的错误。
- 淘汰数据:淘汰使用次数少的或淘汰最久未使用
- LRU(最近最少使用,即淘汰最久未使用):使用链表按序保存,数据访问时需要移动链表元素。有大量数据被访问时,开销很大。
redis中每个对象保存着最近访问的时间,当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
缺点:一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。 - LFU(最近最不常用,即淘汰使用次数少的):redis不是单纯使用访问次数要淘汰数据,而是通过logc值:
- logc随着时间变小
- 访问 key 时logc会增加,logc越大的增加幅度越小。
- LRU(最近最少使用,即淘汰最久未使用):使用链表按序保存,数据访问时需要移动链表元素。有大量数据被访问时,开销很大。
Redis 究竟是单线程还是多线程?
参考:Redis 是单线程的正确理解
Redis 究竟是单线程还是多线程?
Redis 4.0 之前的事件处理流程:Redis 基于 Reactor 模式开发了网络事件处理器,和muduo差不多,不过是单线程的。Redis会将发生的事件放进一个队列中,muduo中是放进activeChannels_
(也相当于一个队列)中。
- Redis 在网络 IO 和键值对读写是采用一个线程来完成的,但对于 Redis 的其他功能来说,比如持久化、异步删除、集群数据同步等,其实都是由额外的线程执行的。
- 使用单线程可行的原因:CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。官方网站有说明,普通笔记本轻松处理每秒几十万的请求。
- 单线程的好处:不需要锁进行同步的开销
Redis 4.0 之后加入 Lazy Free 机制:针对耗时较长的命令(比如删除一个含有上百万对象的 Set 键),会将此任务放进一个队列中,然后其他线程从队列中取出任务并执行。具体是:将要删除的对象从数据库字典摘除,再判断下对象的大小(太小就没必要后台删除),如果足够大就丢给后台线程,最后后台线程清理下数据库字典的条目信息。
Redis 6.0 之后将网络 IO 异步化:将IO操作放到队列中,IO线程池中的线程从队列中取出待执行的IO操作。线程数一定要小于机器核数。
总结:Redis 4.0(耗时较长的命令放到其他线程)、Redis 6.0(IO放到其他线程)
redis 6.0与muduo库的比较:
- redis 6.0线程争抢队列的问题,不会出现muduo中的饿死问题。
- muduo库中如果线程A连接上的某个请求需要执行很久,那么线程A上的其他连接的请求就会被阻塞,即使有其他线程是空闲的,也不能处理线程A上的请求。这就导致线程中的连接上的请求可能会饿死。所以muduo更加适合IO密集型,而不是CPU密集型,即muduo用来处理每个IO都很短。但是muduo没有队列,所以没有锁的争用问题。