目录
在docker中使用指定配置文件的方式启动redis-server失败(容器启动后立刻退出)
在docker容器中使用redis-cli的shutdown命令报错
本文引用redis源码版本为3.0
redis面试题
Linux中的fork()函数
fork()函数通过系统调用创建一个与原来进程完全相同的子进程,系统会给子进程分配资源(父子进程不共享空间),然后把父进程里面所有的数据复制一份到子进程中,只有少数值与原来不同,等于克隆了一个父进程。两个进程可以做相同的事,如果初始参数或传入的变量不同,两个进程也可以做不同的事。可以参考:
什么是redis?
redis是一个非关系型数据库,数据都以键值对的方式存储在内存中,并且支持数据的持久化。redis能存储的值的类型包括:string、list、set、zset(有序集合)和hash类型。
redis支持的数据类型?
String
- 最基本的数据类型,一个key对应一个value,一个键最大能存储512M,可以存储任何数据,如图片或序列化的对象。
- String类型是二进制安全的,如果数据在传输过程汇总被篡改,可以立刻检测出来。
- 整数和字符串都以String类型进行存储,在内存中和序列化文件(如RDB文件)中,存储的格式为【ENCODING | DATA】,其中ENCODING为编码,如:REDIS_ENCODING_RAW代表字符串,REDIS_ENCODING_INT8、REDIS_ENCODING_INT16、REDIS_ENCODING_INT31代表不同长度的字符串。例子如:【REDIS_ENCODING_RAW | "hello"】和【REDIS_ENCODING_INT8 | 123】。通过编码能知道数字类String的字节长度,然后读取数字,再根据数字读取相应长度的字符串数据。
Hash
hash是一个string类型的field和value的映射表,适合存储对象,类似Java中的HashMap。通过hash表、压缩列表(ziplist)实现,redis内部实现了hash表数据结构。
如:hmset users:1 id 1 username zhangsan age 22.
List
双向链表,类似Java中的LinkedList(Deque,双端队列)。可以用来做消息队列。底层实现包括:双向链表和压缩列表(ziplist)。
Set
- set是无序集合,集合内成员是唯一的,通过字典、intset来实现。下面是set添加数据的源代码:
/* set添加。subject为set对象,value为插入元素 */
int setTypeAdd(robj *subject, robj *value) {
long long llval;
// 若set对象的底层实现为字典
if (subject->encoding == REDIS_ENCODING_HT) {
// 将(value, null)插入字典中。函数原型:int dictAdd(dict *d, void *key, void *val)
if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
// ...
// return
}
// 若set对象底层实现为intset
} else if (subject->encoding == REDIS_ENCODING_INTSET) {
// 判断value是否是整数类型
if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
// 插入
// return
} else {
// 如果value不是整数类型,则将intset转换成普通set再插入
// return
}
} else {
redisPanic("Unknown set encoding");
}
return 0;
}
/* intset底层数据结构 */
typedef struct intset {
uint32_t encoding; // 元素编码,表示contents中元素的数据类型
uint32_t length; // contents中元素个数
int8_t contents[]; // 存储数据的数组,int8_t为元素初始类型
} intset;
- 可以统计一些具有唯一性的数据,如访问网站的IP。
ZSet
- 有序集合,集合内成员是唯一的,底层数据结构为压缩列表(ziplist)、intset、跳表(skiplist)其中之一,CRUD的时间复杂度都是O(lgn)。
- zset中的每个元素都会关联一个double类型的数,redis就是根据这个数来进行从小到大排序的,这个数是可以重复的。
- 可以用来做一些排行榜的应用。
Redis对象
redis中的基本对象类型为:string、list、hash、set、zset,但在源码中使用较多的是封装的抽象类型redisObject,数据结构如下:
typedef struct redisObject {
// 对象类型,如:string、list等
unsigned type:4;
// 对象编码,当前类型底层使用的数据结构
unsigned encoding:4;
// 对象最后一次被命令访问的时间,用于计算对象的空转时长
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 对象引用计数,用于对象的回收
int refcount;
// 对象实际内存指针
void *ptr;
} robj;
跳表(SkipList)
跳表是zset类型的实现方式之一,是有序的链表。数据结构如下:
更直白一点如下图,每次插入元素时都会根据幂次定律(越大的数出现的概率越小),随机生成一个介于1~32之间的数作为level数组的大小,这个大小也是插入元素的层数。
- 时间复杂度:增删改查的时间复杂度都为O(logn),与表的最大高度相关。
跳跃表的原理及实现_学习AI算法,请关注微信公众号:机器学习算法全栈工程师……-CSDN博客
redis数据库底层实现
- 数据结构:redis数据库底层使用dict(字典)实现,里面包含2个hash表,一个用于存储数据,一个用于rehash。
- hash算法:使用Murmurhash算法计算出hahs值,hash & sizemask(sizemask为hash表的长度减1)。Murmurhash计算的速度非常快,而且即使输入的键是有规律的,得出来的hash仍然保持较高的随机分布性。
- 冲突:字典使用链地址法解决hash冲突。
- rehash算法:随着操作的执行,hash表中保存的键值对会不断增加或减少,为了让hash表的负载因子维持在一个合理值,要进行rehash,具体分为缩容或扩容。在此期间,CRUD操作在两张表上进行。
- rehash触发时机:如果执行了BGSAVE指令,就会触发持久化操作。copy on write就是copy一部分键值对,持久化,再copy一部分,再持久化。如果copy了50%,突然发生了rehash,那子进程就不知道接下来从哪里copy了,必须重头开始copy。
- 渐进式rehash:如果hash表中的数据量非常大,那么一次性完成rehash耗费的时间很长,因此有了渐进式的rehash。在此期间,CRUD操作在两张表上进行。
redis持久化机制
redis的持久化机制有两种:RDB和AOF。
RDB(redis database)
RDB持久化方式就是在指定的条件下对内存中的数据进行快照存储,数据存储到一个.rdb的二进制文件中。
RDB优点:
- 能很方便的保存过去某个时间点的数据,且保存的快照文件体积小,数据恢复的速度快,非常适合用来做备份。
- 通过fork(见上面fork()介绍)出一个redis子进程来做保存,能最大化redis的性能。fork的时机在redis.conf中配置如下:
save [seconds changes]——在s秒之内如果有c个key被更新,则进行持久化
#Redis默认配置文件中提供了三个条件:
#save 900 1
#save 300 10
#save 60 10000
#也可以手动执行命令进行持久化:
1)bgsave命令是异步保存,此时redis可以继续提供服务,但是命令执行后新的更新不会被保存。
2)save命令时同步保存,当子进程保存时,redis会停止接受新的命令。
RDB缺点:
- redis意外终止可以会导致上一次持久化之后的更新数据的丢失。
AOF(append only file)
通过每隔一段时间(指定的),保存所有对服务器的更新操作来做持久化,所有操作按顺序添加到一个文件末尾。
AOF优点:
- 通过创建一个后台线程来执行fsync()函数来将缓冲区中的用户命令追加到AOF文件末尾,能提高redis性能。在redis.conf中的配置如下:
1)appendfsync no
2)appendfsync always
3)appendfsync everysec 每隔1s就将缓存中的数据写到AOF文件末尾
- 在上面的策略下,能最大限度地保证数据的完整性(默认配置下,最多丢失1s的数据)。
AOF缺点:
- 对相同的数据集而言,AOF文件的体积要大于RDB文件的体积。
- 当数据集比较大时,AOF在数据的恢复上比RDB要慢。
如何选择使用哪种持久化方式?
- 若你不关心几分钟内数据的丢失就可以只选择RDB方式。
- 若你对数据的安全性要求很高,那最好两种方式都使用。
- 不推荐只使用AOF一种方式,因为RDB在数据备份和数据恢复方面要比AOF的速度要快。
什么是缓存穿透?
- 定义:用户查询的key缓存和数据库中都没有,以后每次查询这个key都会请求数据库,因为数据库中查不到自然也不会写到缓存中。当对这类key的并发访问量非常大时,会给底层数据库带来非常大的负担。
- 解决:key在数据库中也查询不到时,把这个空的结果缓存,然后设置一个过期时间 ; 布隆过滤器。
什么是缓存击穿?
- 定义:用户查询的key缓存中没有,但是数据库中有。某个key查询的并发量非常大,如果这个key失效了,失效的瞬间会产生大量的数据库请求,给数据库带来很大的负担。
- 解决:热点数据设置永不过期 ; 使用分布式锁,将查到的数据库的值写回缓存,然后释放锁。
// 服务器的数据请求
Object res = getFromRedis(key); // 同一时间大量线程访问到这里,如果缓存为空
if (res == null){
synchronized (key){ // 在分布式环境下synchronized 会失效,要用setnx来加锁
Object res = getFromRedis(key);
if (res == null){
res = getFromMysql(key); // 就都从数据库获取
setToRedis(key); // 将结果放入redis中
}
}
}
return res;
什么是缓存雪崩?
- 定义:在某一时间有大量的key失效,造成大量的数据库请求,给数据库造成巨大的压力。
- 解决:将key的失效时间均匀分开 ; 采用分布式锁,获取锁失败的线程自旋或阻塞或放入消息队列,之后从缓存中获取数据。
布隆过滤器与缓存穿透
- 功能:能够概率性知道哪些数据不存在(判断不存在的一定不存在,判断存在的也可能不存在)。能过滤掉大部分不存在的数据,剩下的少数误判会导致请求数据库,但带来的负担大大减轻。所以这是一种缓存穿透解决办法。
- 数据结构:bit数组和若干个hash函数的结合。数组位置上只存储0和1,hash函数用来对添加的数据做映射。只能添加,不能删除。
- 原理:当添加数据时,将数据通过若干个hash函数映射到bit数组中,映射位置的值设为1。当查询某个数据是否存在时,通过若干个hash函数查出映射位置的值,若有一个位置为0,说明不存在。
- 效率:效率与hash函数的个数和数组的长度有关。一般来说,数组长度越长,准确率越高;hash函数少了,准确率低,多了数组容易被填满。所以要平衡这两个参数。
- 注意:若查询某个数据,所有位置都为1,那这个数据也不一定存在,这意味着查询不存在的数据时还是会直接访问数据库。
- redis缓存穿透应用:能够知道某个key是否存在,若不存在直接返回,避免访问数据库。
- 缺陷:但就如上面所说,判断是有误差的,查询不存在的数据时可能还是会直接访问数据库;当缓存过期或者数据库中数据被删除时,由于bit数组不会一起更新,所以下次查询被删除的数据时会判断存在,进而直接访问数据库。(不能解决缓存失效或雪崩)
redis与memcahch的区别?
- redis支持数据的持久化,memcahch不支持持久化。
- redis支持string、list、hash、set、zset等数据类型,memcahch只支持string。
- redis的value最大为512M,memcahch最大为1M。
- redis支持单线程,memcahch支持多线程。
- 集群方面。。。
redis为什么是单线程的?
- 可行性:因为不像mysql把数据存在磁盘上,redis的数据都是存在内存中的,内存中的数据读写速度是非常快的,即使在单线程的情况下也能达到每秒平均10w次的读写速度(官方数据),因此单线程方案是可行。这样一来,线程数量(CPU)不是redis的瓶颈,内存大小才是,内存越大,redis就能存储更多的数据,更多的用户请求就能达到内存级别的响应速度。(后面要测试mysql每秒的读写速度)。
- 优点:同时,使用单线程也有一些好处。使用单线程能够避免加锁、释放锁的销号,不用考虑死锁的问题,也能减少进线程切换带来的系统开销。
下面是官方的bench-mark数据:
测试完成了50个并发执行100000个请求。
设置和获取的值是一个256字节字符串。
Linux box是运行Linux 2.6,这是X3320 Xeon 2.5 ghz。
文本执行使用loopback接口(127.0.0.1)。
结果:读的速度是110000次/s,写的速度是81000次/s 。
redis速度快的原因?
- 数据存储在内存中(最基础最重要)。
- 单线程,减少了上下文的切换。
- 采用非阻塞IO多路复用机制。(【Java NIO】网络IO模型与Java中的NIO之间的联系_奋进的小白粥-CSDN博客)
redis缓存过期策略(怎么删除过期的缓存)
redis中过期缓存的删除采用的是:定期删除+惰性删除+内存淘汰策略。
- 定期随机删除:redis默认每隔100ms随机检查一批数据,并将其中过期的数据删除。
- 惰性删除:每次用户查询数据时,检查数据是否过期,过期则删除。
- 内存淘汰策略:过期删除和惰性删除都具有随机性,在极端情况下会出现redis存在大量过期数据的情况,这会占用大量内存资源,严重影响redis性能。所以redis使用内存淘汰策略来防止这种情况的发生。
内存淘汰策略
在redis.conf中有两个属性:
- maxmemory [bytes]:用来指定redis的能够占用的最大内存。
- maxmemory-policy [policy]:当redis内存超过maxmemery后,执行指定的内存淘汰策略。
redis5.0中的内存淘汰策略包括:
- volatile-lru:从已设置过期时间的数据中挑选最近最少使用的数据。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
- volatile-random:从已设置过期时间的数据集中选择任意数据淘汰。
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
- allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
- allkeys-random:从数据集(server.db[i].dict)中选择任意数据淘汰
- no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,读操作可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。
redis官方的集群搭建方案
redis从3.0开始,官方提供了一种分布式的集群方案。
- 分布式:在数据的存储上没有采用一致性hash,而是引入了hash槽。redis集群有16384个hash槽,每一个节点负责一部分hash槽。通过将key进行运算然后对16384取模,决定数据存在哪一个节点上。
- 集群:集群至少有3个master节点,官方推荐至少设立6个节点,3主3从,当主节点挂掉时,从节点替代成为主节点。
- 缺点:redis并不能保证强一致性,这意味着可能发生丢失写操作的情况。
- 1.因为主从复制发生在对客户端发送回复命令之后,若回复之后复制完成之前主节点宕机,则数据会丢失。
- 2.网络分区。client向A节点写入数据,但client和A被分为一区,与其它节点孤立开来,若A1在网络回复之前被选举为master,则写操作丢失。
一致性hash算法
一致性hash算法主要是为了解决分布式缓存的问题。
好刚: 7分钟视频详解一致性hash 算法_哔哩哔哩_bilibili
- 传统缓存方式:假设有3台图片缓存服务器A、B、C,当插入一张图片a.jpg时,按照:hash(a.jpg)%3来决定将图片插入或读取哪一台服务器。
- 问题:当添加或减少一台服务器时,3变成4或2,会直接影响取模的结果,导致读取到其它的服务器上,出现缓存雪崩,压垮后台服务器。
一致性hash算法将服务器和插入的图片hash到一个0~2^32-1的hash环上,图片按顺时针存储在最近的服务器上(通过比较图片和所有服务器的hash值,能快速确认),当一个新的服务器节点D加入进来后,仅仅映射在节点C和节点D之间的图片会定位到错误的服务器而导致访问后台数据库,其它范围的数据还是能正常从缓存中读取。
注:也可能出下面情况,一个节点缓存了大量的数据,当D如下图加入时,会有大量的缓存失效,引起缓存雪崩。解决办法就是缓存大量数据的节点增加许多虚拟节点,使得缓存数据均匀分布。
redis分布式锁
这里以减库存的例子作介绍,多个线程对redis中的库存数量进行-1操作,要保证线程安全。
单server + 单redis
单server下可以不用setnx,直接用synchronized进行并发控制,只有获取锁的线程才能对redis的库存进行--。
server集群 + 单redis
server集群下,synchronized就没用了,要用setnx来设置分布式锁。
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,Redis中可以使用SETNX命令实现加锁。
将 key 的值设为 value ,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作。
解锁:使用 del key 命令就能释放锁;或者给key设置自动过期的时间。
解决死锁:
1)通过Redis中expire()给锁设定最大持有时间,如果超过,则Redis来帮我们释放锁。
2) 使用 setnx key “当前系统时间+锁持有的时间”和get set key “当前系统时间+锁持有的时间”组合的命令就可以实现。
注意:当客户端A获取锁了,使用完资源之后会释放锁,即del key,但是如果A执行时间过长,导致锁过期了,此时锁被另一个客户端B获取了,A再释放锁,所导致A释放了B的锁,引发程序错误,所以这里下面要用clientId(只要是唯一值即可)座位value,来保证A删除的是自己的锁,而不是B的。
server集群 + redis集群
如果是多redis服务,则要逐一给所有的redis服务器发送setnx请求,只有获取到了n/2+1个服务器的锁,才能是获取锁成功。获取锁成功且使用完后或获取锁失败后,都要在所有redis服务器上释放锁。详情见:REDIS distlock -- Redis中国用户组(CRUG)。因此要在上面减库存的代码中,更改setnx代码,对所有的redis服务器进行setnx,只有在超过半数的redis上设置成功才算是获取锁成功。
redis分布式锁(官方)
REDIS distlock -- Redis中国用户组(CRUG)
如何保证redis和数据库数据一致性?
- 能忍受数据不一致:设置缓存失效时间,比如商品的库存数量,设置失效时间为1分钟,1分钟内可能出现数据不一致情况,但失效之后就会更新保持一致。
- 不能忍受数据不一致(读多写少):分布式读写锁,保证数据的强一致性。
- 不能忍受数据不一致(读多写多):异步写回。应用程序只面向redis进行读写,同时在后台开启线程进行异步写回数据库,根据业务场景设置异步写回的频率。
redis容器报错
在docker中使用指定配置文件的方式启动redis-server失败(容器启动后立刻退出)
使用的命令如下,创建并启动一个redis容器,然后启动redis容器的server服务并指定server服务的启动配置文件。
docker run
-p 6379:6379 #端口映射
-v /usr/local/docker/redis/redis.conf:/usr/local/etc/redis/redis.conf #将本地的redis.conf映射到启动的redis容器中
-v /usr/local/docker/redis/data:/data #同上
--name redis #创建的容器名称
-d #后台启动
redis:5.0 #容器的镜像名称
redis-server /usr/local/etc/redis/redis.conf #指定redis启动的配置文件
在使用docker安装redis时,不能在redis.conf中将daemonize 属性设置为yes,否则通过指定配置文件的方式启动redis-server会失败,docker容器启动后会立刻退出。
在docker容器中使用redis-cli的shutdown命令报错
成功启动redis容器后,使用以下命令后报错:
docker exec -it redis redis-cli -a xxxxxx
set a 10
shutdown
Failed opening the RDB file dump.rdb (in server root dir /data) for saving: Permission denied
当客户端关闭redis服务器的时候(shutdown命令),redis会执行持久化操作,将内存中的数据更新到dump.rdb文件中。显然上面说的是dump.rdb文件不能打开,没有权限,可是我容器的/data目录下根本就没有dump.rdb文件,所以这个错误就让人摸不着头脑了。查了一下,解决办法如下:
重启后再启动docker容器会出现Error response from daemon: error creating overlay mount to xxx merged: invalid argument的错误,此时要:
- 修改docker的配置。(注意要删除docker镜像:docker rm $(docker ps -a -q))。Error response from daemon: error creating overlay mount to xxx merged: invalid argument 正确处理_AI-CSDN博客
大厂面试题
字节跳动
1.zset结构
9870_Redis数据库 | ProcessOn免费在线作图,在线流程图,在线思维导图
2.hash扩容
3.key-value设置了ttl(过期时间),到期了一定会被删除吗?
不一定,因为redis中删除到期数据是要触发一定的条件的,条件详情参考上面“缓存过期策略”。
4.redis应用场景题,如何保证缓存与数据库一致性?