redis-深入理解-总览篇
redis 6.x.x安装注意事项
1.安装gcc套装
yum install cpp
yum install binutils
yum install glibc
yum install glibc-kernheaders
yum install glibc-common
yum install glibc-devel
yum install gcc
yum install make
2.更新gcc
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
scl enable devtoolset-9 bash
3、设置永久升级:
echo “source /opt/rh/devtoolset-9/enable” >>/etc/profile
解压后。切换到文件夹下
1.make persist-settings
2.make
3.make install
redis命令:https://www.redis.net.cn/order/
1.什么是redis?
它的全称是 Remote Dictionary Service,远程字典服务
是开源的、c语言实现的、基于内存且可持久化的高性能k-v数据库。
特点:
高性能、高并发
功能丰富(支持多种数据类型)
分布式锁、全局唯一id、分布式限流、网站访问统计、用户签到统计、秒杀解决方案(lua)、bitmap()、热点数据缓存(最常见的用法)提高查询速度、分担后端数据库压力、微博排名(zset)、消息队列
内存淘汰机制、key过期机制
可持久化(aof、rdb)
主从复制
哨兵机制
高可用集群
字符串string 的最大长度限制 是 512M
2.关系型数据库(SQL)和非关系型数据库(NOSQL)?
关系型数据库(SQL)有sql-server、mysql、oracle
特点:
一、它以表格的形式,基于行存储数据,是一个二维的模式
二、它存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应表结构。
三、表与表之间存在关联(Relationship) innodb支持外键
四、大部分关系型数据库都支持 SQL(结构化查询语言)的操作,支持复杂的关联查询。
五、通过支持事务(ACID酸)来提供严格或者实时的数据一致性。
缺点:
1、不支持动态的扩缩容。水平扩容需要复杂的技术来实现,比如分库分表。
2、表结构修改困难,因此存储的数据格式也受到限制。
3、在高并发和高数据量的情况下,我们的关系型数据库通常会把数据持久
化到磁盘, 基于磁盘的读写压力比较大。
非关系型数据库的特点:
1、存储非结构化的数据,比如文本、图片、音频、视频、键值对。
2、数据之间没有关联,可扩展性强。
3、保证数据的最终一致性。遵循 BASE(碱)理论。 ---- CAP理论的平衡(平衡了 C 一致性和 A 可用性) P表示分区容错性
Basically Available(基本可用):允许系统出现部分功能故障,只要核心功能能够正常运行即可
Soft-state(软状态):从节点在一定时间和主节点数据不一致,并不影响数据访问
Eventually Consistent(最终一致性):经过一定延时,从节点最终和主节点保持数据一致
4、支持海量数据的存储和高并发的高效读写。
5、支持分布式,能够对数据进行分片存储,扩缩容简单(redis高可用集群的水平动态扩容)。
常见类型nosql:
1、KV 存储,用 Key Value 的形式来存储数据。比较常见的有 Redis 和 MemcacheDB。
2、文档存储,MongoDB。
3、列存储,HBase。
3.redis 的参数可以通过三种方式配置,
一种是 redis.conf,
一种是启动时 --携带的参数
一种是 config set。
4.基本操作
查看所有键 keys *
获取键总数 dbsize
查看键是否存在 exists key [key …]
删除键 del key [key …]
重命名键 rename key new_key
查看数据类型 type key
查看具体编码 object encoding key
5.redis几种数据类型?
String、Hash、List、Set、Zset、Hyperloglog、Geo、Streams
6.为什么叫Binary-safe strings呢?
因为c语言原生字符串(c语言字符串就是字符数组)是二进制不安全的,因为c语言中的字符串是以‘\0’结尾,所以如果存储的二进制中存在\0,就会出现内容截断(内容丢失 | 读取丢失),redis的字符串类型,
采用的是sds(simple dynamic string),对字符串进行了封装,引入len属性,不用\0作为字符数组读取结束,而是用len判断是否读取结束。
7.string 字符串
存储类型:
字符串embstr、整型int、浮点数float
操作命令:
mset key1 v1 key2 v2 … 设置多个值(批量操作,原子操作)
setnx k v 设置值,如果key存在,则不成功,否则成功
基于此可实现分布式锁。用 del key 释放锁。 但如果释放锁的操作
失败了,导致其他节点永远获取不到锁,怎么办? 加过期时间。单独
用 expire 加过期,也失败了,无法保证原子性,怎么办?多参数
set key value [ex | px timeout] [nx | xx] 原子操作,实现分布式锁 — 推荐使用,因为具有原子性
incr key (整数)值递增
incrby key increment (整数)值增加increment
incrbyfloat key increment 浮点值增加increment
mget key1 key2… 获取多个值
strlen key 获取值的长度
append key value 字符串追加内容
getrange key startIdx endIdx 获取字符串从startIdx位置到endIdx位置的值
*存储(实现)原理:
因为Redis是KV(字典)数据库,它是通过 hashtable(哈希表)实现
的(我们把这个叫做外层的哈希)。所以每个键值对都会有一个dictEntry
(源码位置:dict.h),里面有指向了key和value的指针。next指向dictEntry。通过拉链法解决哈希冲突
dict.h数据结构:
typedef struct dictEntry {
void *key; /* key 关键字定义 */
union {
void *val; /* value定义 */
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; /* 指向下一个键值对节点 */
} dictEntry;
key是字符串,但是Redis没有直接使用C的字符数组,而是存储在自定义的
SDS (simple dynamic string)中.
value既不是直接作为字符串存储,也不是直接存储在SDS中,而是存储在
redisObject中。实际上五种常用的数据类型的key和value都是一个redisObject,都是通过
redisObject来存储的。
redisObject:
redisObject定义在src/server.h文件中。
数据结构:
typedef struct redisObject {
unsigned type:4; /* 对象的类型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */
unsigned encoding:4; /* 具体的数据结构 */
unsigned lru:LRU_BITS; /* 24 位,对象最后一次被命令程序访问的时间,与内存回收有关 */
int refcount; /* 引用计数器。当 refcount为0的时候,表示该对象已经不被任何对象引用,则可
以进行垃圾回收了 */
void *ptr; /* 数据指针,指向对象实际的数据结构 */
} robj;
字符内部三种编码
1、int 存储8个字节的长整型(long,2^63-1)。
2、embstr 代表embstr格式的SDS(Simple Dynamic String 简单动态字符串),存储小于44个字节的字符串。
3、raw,存储大于44个字节的字符串(3.2 版本之前是 39 字节)。
8.什么是SDS?
redis中字符串的实现,Simple Dynamic String简单动态字符串
在 3.2 以后的版本中,SDS又有多种结构(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代
表 2^5=32byte, 28=256byte,216=65536byte=64kb,2^32byte=4GB
之所以设计这么明细的数据结构,就是为了最大程度节省内存空间
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3位保存类型,5位保存长度 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3位保存长度,5位未使用 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3位保存长度,5位未使用 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
为什么三位保存类型?
是因为存在5种类型,如果用2位就会不够,所以采用三位,对于sdshdr5,
9.为什么要使用sds?
c语言本身没有字符串类型(只能用char数组来实现)
c语言字符数组特嗲:
1、使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
2、如果要获取字符长度,必须遍历字符数组,时间复杂度是 O(n)。
3、C 字符串长度的变更会对字符数组做内存重分配。
4.是通过空字符判断是否结束,如果保存的二进制内容中存在空字符,可能会导致内容被截断,出现二进制安全问题。
SDS的特点:
1.不需要担心内存溢出,SDS会自动扩容
2.获取字符串长度,时间复杂度为O(1),因为定义了len属性
3.通过“空间预分配”和“惰性空间释放”减少内存重分配次数
4.通过len属性判断数组内容是否结束,保证了二进制安全
但是同样是以空字符('\0')结尾,目的是为了使用c语言中函数库操作字符串的函数
10.embstr 和 raw 的区别?
embstr的使用只分配一次内存空间(因为RedisObject和SDS是连续的),
而raw需要分配两次内存空间(分别为 RedisObject和SDS分配空间)。
因此与raw相比,embstr 的好处在于创建时少分配一次空间,
删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。
而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,
整个RedisObject和SDS都需要重新分配空间,因此Redis中的embstr实现为只读。
11.:int和embstr什么时候转化为 raw?
int不再是整型或超过了long的范围(2^63-1)时,会自动转换为embstr。
对embstr编码的值进行append操作,会转化为raw类型
12.明明没有超过阈值,为什么变成 raw 了?
因为embstr其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw对象再进行修改
因此,只要是embstr对象,修改后的对象一定是raw的,无论是否达到44个字节。
13.当长度小于阈值时,会还原吗?
redis内部编码的转换,都符合以下规律:编码转换在redis写入数据时完成,且转换过程不可逆,
只能从小的内存编码转换为大的内存编码(不包括重新set)。
14.为什么要对底层的数据结构进行一层包装呢?
通过封装,可以根据对象的类型动态的选择存储结构,以达到节省内存空间和提高查询速度目的。
15.String类型应用场景?
1.缓存
热点数据缓存、对象缓存、全页缓存
提高热点数据访问速度
2.分布式数据共享
分布式session
3.分布式锁
方式一、
setnx key value
expire key second
方式二
set key value [ex second | px millisecond] [nx | xx]
setex key value second
4.全局ID
incrby key increment(推荐) | incr key
5.计数器
incrby key increment | incr key
6.限流
7.位统计
setbit key position 1/0
因为 bit 非常节省空间(1 MB=8388608 bit),可以用来做大数据量的统计
场景:在线用户统计,留存用户统计
setbit onlineusers userid 1 标识用户在线
setbit onlineusers userid 0 标识用户离线
setbit onlineusers 1001 1
setbit onlineusers 1002 1
setbit onlineusers 1003 1
setbit onlineusers 1003 0
bitcount onlineusers查看在线用户
bitcount onlineusers userid 判断用户是否在线
支持按位与、按位或等等操作。
BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。
BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey
如果一个对象的 value 有多个值的时候,怎么存储? | 如何保存表的一行记录?
通过拼接key定位一个表中的一个字段,项目名:表名:字段名 value
mset k1 v1 k2 v2 ...设置一行记录
mget k1 k2 ... 获取一行记录
问题:key过长,导致占用的内存过多。有没有更好的方式?
hash数据类型存储,一行数据存储为一个hash,项目名:表名:id组合作为key ,用列名作为field,列值作为value
16.hash类型
存储类型
存储键值对映射集合,field不能重复,value可以重复
Hash类型和String类型的区别?
1.把所有的值聚集到一个key中,节省内存空间
2.只使用一个key,避免了key冲突
3.当需要批量取值时,只需要一个命令,减少内存、cpu、Io消耗
不适合的场景:
1.Field不能单独设置过期时间
2.不能进行bit操作
3.需要考虑数据量分布问题(value值非常大的时候,不能分布到多个节点)
操作命令
hset key field value 设置键值对
hget key field 获取key中field的value
hmset key field value [field value ... ] 批量设置之间
hmget key field field2...返回哈希表key中,与给定域field、field..相关联的值
hgetall key 获取所有的键值对
hvals key 获取所有的值
hkeys key 查找key中所有的field
hexists key field 判断一个值是否存在
hlen key 获取key中键值对数量
hdel key field 删除key中的field键值对
hstrlen key field 返回哈希表key中,与给定域field相关联的值的字符串长度(string length)
hSCAN key cursor [MATCH pattern] [COUNT count] 查找
存储(实现)原理
Redis键值对是用hashtable实现,叫做外层哈希,存储hash数据类型的叫做内层的哈希
具体实现的:
ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)
hashtable:OBJ_ENCODING_HT(哈希表)
ziplist具体结构:
typedef struct zlentry {
unsigned int prevrawlensize; /* 保存上一个节点长度所用字节*/
unsigned int prevrawlen; /* 上一个节点字节数 */
unsigned int lensize; /* 保存当前节点长度所用字节数 */.
unsigned int len; /* 当前节点字节数
unsigned int headersize; /* 头结点长度 */
unsigned char encoding; /* 具体的数据结构 */
unsigned char *p; /* 指向具体的内容 */
} zlentry;
压缩列表节点不保存上一个节点或下一个节点的指针,而是记录上一个节点的大小,虽然一定程度上降低了读写性能,
但是换来了高效的内存利用率。
16.List数据类型
操作命令
lpush key value [value2 … ] 从左边向list类型的key中推入一个或多个值
rpush key value [value2 … ] 从右边向list类型的key中推入一个或多个值
lpop key 从左边取出一个值
rpop key 从list集合key的右边取出一个值
lrange key start end 返回list集合key从start到end位置元素
lindex key idx 查看list集合key中idx位置的值
blpop key[key1 …] 阻塞的从右边弹出list集合key1…等中的一个值
brpop key[key1 …] 阻塞的从右边弹出list集合key1…等中的一个值
llen key 获取列表长度
底层实现:
3.2版本之前:
数据量较少是采用zipList存储,达到临界值时转换为LinkedList存储,分别对应的编码
OBJ_ENCODING_ZIPLIST 和 OBJ_ENCODING_LINKEDLIST
3.2之后:
统一用quicklist来存储。quicklist存储了一个双向链表,每个节点都是一个
ziplist。
zipList的总体布局:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
zlbytes:占用内存的字节数
zllen:节点数
zltail:列表末端偏移量
zlend:标识末端
quickList:
快速列表是ziplist和linkedlist的结合体
head和tail分别指向双向链表的表头和表尾
typedef struct quicklist {
quicklistNode *head; /* 指向双向列表的表头 */
quicklistNode *tail; /* 指向双向列表的表尾 */
unsigned long count; /* 所有的ziplist中一共存了多少个元素 */
unsigned long len; /* 双向链表的长度,node的数量 */
int fill : 16; /* fill factor for individual nodes */
unsigned int compress :16; /* 压缩深度,0:不压缩; */
} quicklist;
17.set无序集合数据类型
无序不重复集合
操作命令
sadd key v1 [v3…] 向集合中添加一个或多个元素
smembers key 获取集合中的所有元素
sismember key value 判断value是否为key集合中的元素
scard key 返回集合元素个数
srem key value [value2 …]从集合中移除一个或多个元素
srandmember key 随机返回集合中的一个元素
spop key count 随机返回集合中的count个元素,并移除
sunionstore destKey key1 key2 … 将多个集合的并集放入到destKey集合中
sdiff key1 key2 … 取多个集合的差集
sunion key1 key2 …取多个集合的并集
sinter key1 key2… 取多个集合的交集
存储(实现)原理
采用intset或hashtable数据结构存储,如果是整数类型用intset存储,非整数类型用hashtable存储(数组和链表,链表用来解决hash冲突)。
问题:kv怎么存储set的元素?key属性就是元素的值,value为null
如果是整数类型采用intset存储,当元素个数达到512(默认),转换为hashtable存储。
可以通过修改set-max-intset-entries的值,决定什么时候将Intset转换为hashtable
18.zset数据类型
操作命令
zadd key [NX|XX] [CH] [INCR] score member [score member …] 添加一个或多个成员到zset集合中
zrange key start end [withscores] 返回有序集合key从start到end位置的元素
zrevrange key start end [withscores] 返回倒排有序集合key从start到end位置的元素
zrem key member [value … ] 删除有序集合key中的一个或多个成员
zcard key 返回有序集合的成员个数
zcount key min max 返回有序集合从min到max的成员个数
zrangebyscore key startScore endScore 返回有序集合中startScore到endScore的成员
zincrby key increment member 有序集合中的指定成员的score值增加increment
zrank key member 返回有序集合中member成员的序号(从0开始)
zscore key member 查看member成员的score值
存储原理
使用ziplist存储元素需要满足:
redis.conf
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
元素个数小于128
所有member成员的长度都小于64字节数
在zipList内部是按照score递增排序来存储,插入的时候要移动之后的数据
只要超过阈值就会转换为skiplist+dict存储
为什么不使用avl树或者红黑树?
因为skiplist更加简洁
19.HyperLogLog数据类型
是用来做基数统计的算法,HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的
在Redis里面,每个HyperLogLog键只需要花费12 KB内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
命令:
PFMERGE destkey sourcekey [sourcekey ...] 将多个HyperLogLog合并为一个HyperLogLog
PFADD key element [element ...] 添加指定元素到HyperLogLog中。
PFCOUNT key [key ...] 返回给定HyperLogLog的基数估算值。
20.发布订阅
publish channel message 发布消息到渠道
subscribe channel [channel … ] 订阅多个渠道
unsubscribe channel 取消订阅
psubscribe pattern 订阅一个或多个符合给定模式的频道。
21.redis事务
特点:
1.按照命令进入队列的顺序执行
2.不会受到其他客户端请求的影响
命令:
watch 监控指定key
可以为redis提供事务的乐观锁行为,也就是多个线程修改值的时候,会判断key的值是否发生改变,如果没有改变就设置
值,否则被认为是过期数据。
用watch监控一个或多个key,开启事务后,如果有一个
key在事务中,在事务执行exec之前,如果这个key的value发生修改,那么整个事务就会被取消(key提前过期除外),可
以用unwatch取消
multi 开启事务
discard 取消事务
exec 执行事务
事务可能出现的问题?
1.执行exec之前发生错误
事务会被拒绝执行,队列中所有的命令都不会得到执行
2.执行exec之后发生错误
事务正常执行,错误的命令会被跳过,其他命令正常执行
不符合原子性的定义,也就是没办法用redis的事务机制来实
现原子性,保证数据一致性。
22.为什么在一个事务中存在错误,Redis不回滚?
因为redis设计的初衷就是为了加快热点数据速度的访问,缓存热点数
据,而不是为了存储数据,所以设计方便相对于mysql这种强一致性的数
据而言,就简单些,在事务方面也没有提供原子性保证,如果要实现原
子性,就拿mysql来说,实现事务,还是引入了innodb引擎。
23.lua脚本
eval lua-script key-num [key1 key2 key3 …] [value1 value2 value3 …]
eval 代表执行Lua语言的命令
lua-script 代表Lua语言脚本内容。
key-num 表示参数中有多少个 key,需要注意的是Redis中key是从1开始的,如果没有key的参数,那么写0。
[key1 key2 key3…]是key作为参数传递给Lua语言,也可以不填,但是需要和 key-num 的个数对应起来。
[value1 value2 value3 ….]这些参数传递给 Lua 语言,它们是可填可不填的。
lua脚本中调用redis命令
redis.call(command,key[param1,param2....])
eval "redis.call('set' , KEYS[1] , ARGV[1])" 1 lua value 等价于
set lua value
限流:
local num = tonumber(redis.call("incr" , KEYS[1]))
if num == 1 then
redis.call("expire" , KEYS[1] , ARGV[1])
return 1
elseif num > tonumber(ARGV[2]) then
return 0
else
return 1
end
文件形式执行lua脚本
./redis-cli –eval [lua 脚本] [key…]空格,空格[args…]
24.为什么lua脚本修改了redis数据,阻塞后,不能通过script kill进行中止?
redis需要保证lua脚本的原子性,如果脚本执行了一部分就终止,那么就违背了脚本原子性要求。最终要保证脚本要么都执行,要么都不执行。
解决方案:
新开一个客户端,执行shutdown nosave | shutdown save强制中止redis进程
25.shutdown save 和 shutdown nosave的区别?
nosave不会进行持久化操作,意味着发生在上次快照之后的数据库修改操作会丢失。
26.Redis为什么这么快?
1.纯内存操作
2.单线程
避免线程上下文切换带来的CPU资源消耗
没有线程安全问题,不需要加锁,速度更快
避免创建和销毁线程带来的消耗
3.多路Io复用
异步非阻塞I/O,多路复用处理并发连接。
27.Redis为什么是单线程的?
因为单线程已经够用了,cpu不是redis的性能瓶颈。redis的瓶颈最有可能是机器
内存或网络带宽。既然单线程容易实现,而且cpu不会成为瓶颈,那就顺理成章的
采用单线程方案。
28.单线程为什么快?
因为redis是基于内存的操作,同时单线程不需要进行线程创建和销毁、以及上下
切换、和考虑多非线程竞争导致资源的线程安全问题,还采用多路io复用处理并发
请求。
虚拟内存分为
内核空间(kernel space)和用户空间(user space)
内核操作系统的核心,独立于普通应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。
内核空间中存放的是内核代码和数据,进程的用户空间中存放的是用户代码和数据。
不管是内核空间还是用户空间,他们都是处于虚拟空间中,都是物理内存地址的映射
内核进程和用户进程所占虚拟内存的比例为1:3
当进程运行在内核空间时就处于内核态,运行在用户空间时就处于 用户态
进程在内核空间以执行任意命令,调用系统的一切资源;
在用户空间只能执行简单的运算,不能直接调用系统资源,必须通过系统接
口(又称 system call),才能向内核发出指令。
29.什么进程切换?
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换
30.什么是上下文?
可执行的程序代码和程序计数器
31.什么是进程阻塞?
正在运行的进程由于提出系统服务请求(如 I/O 操作),但因为某种原因未得到操 作系统的立即响应,该进程只能把自己变成阻塞状态,等待相应的事件出现后才被唤醒。进程在阻塞状态不占用CPU资源
32.传统I/O数据拷贝?
当应用程序读取数据的时候,先判断数据是否在用户进程的页内存中,如果存在就直接到内存中读取。
如果不在,从磁盘将数据加载到内核缓冲区,再将数据从内核缓冲区拷贝到用户空间的当前用户进程内存区域中。
(两次拷贝,两次user和kernel的上下文切换)
-
Blocking IO ,I/O 的阻塞到底阻塞在哪里?
当用户程序执行read | write读取一个文件,但是次文件时不可读,从磁盘加载数据到内核缓存区会阻塞,从内核缓冲区拷贝数据
到用户空间的当前进程内存页中也是阻塞的,直到copy complete ,内核返回结果,用户进程才能够解除阻塞状态如何解决阻塞问题?
1、在服务端创建多个线程或者使用线程池,但是在高并发的情况下需要的线程会很多,系统无法承受,而且创建和释放线程都需要消耗资源。
2、由请求方定期轮询,在数据准备完毕后再从内核缓存缓冲区复制数据到用户空间 (非阻塞式 I/O),这种方式会存在一定的延迟。能不能一个线程处理多个客户端请求?
能,IO多路复用
34.IO 多路复用
IO:网络IO
多路:多个tcp连接(socket 或 channel)
复用:复用一个或多个线程
原理:
不再由应用程序自己监控连接,而是由内核接替应用程序监控文件描述符,
客户端在操作的时候,会产生具有不同事件类型的socket
服务端,IO多路复用程序(I/O multiplexing module)会把消息存放在一个队列中,
然后通过文件事件分派器,转发到不同的事件处理器中。
多路复用器的select的实现?
当应用程序调用了多路复用器,进程会被阻塞。内核会监视多路复用器
负责的所有socket,当任何一个socket数据准备完毕,多路复用器就会返回可读。
用户进程再调用read操作,将数据从内核缓冲区拷贝到用户空间。
特点:
通过一种机制一个进程能同时等待多个文件描述符,
而这些文件描述符(套接字描述符)其中的任意一个进入读就绪(readable)状态,
select()函数就可以返回。
evport是Solaris系统内核提供支持的;
epoll是LINUX系统内核提供支持的;
kqueue是Mac系统提供支持的;
select是POSIX提供的,一般的操作系统都有支撑(保底方案);
35.redis内存回收机制?
redis数据存储在内存中,但是内存资源时有限的,如果不定期的淘汰掉一些k-v,可能会
出现内存不足的问题,最终导致服务崩溃,系统异常,所以为了保证redis服务正常运行,
需要定期清除过期、使用频率低、最少使用的key,释放内存资源,以redis服务正常运行。
36.过期策略?
定时过期(主动过期)
惰性过期(被动过期)
37.淘汰策略 maxmemory-policy
有哪些淘汰策略?
volatile-lru 推荐使用,在服务器正常运行的情况下,优先删除最近最少使用的键值对。
根据LRU算法删除数据库中设置了过期时间(expire key timeout)的key,直到有足够的内存为止,如果没有可以删除的key,
回退到noeviction。
allkeys-lru
根据LRU算法数据库中的key,包括没有设置过期时间的key
volatile-LFU
根据LFU算法删除数据库中设置了过期时间(expire key timeout)的key,直到有足够的内存为止。
allkeys-LFU
根据LRU算法数据库中的key,包括没有设置过期时间的key
volatile-random 随机删除数据库中设置了过期时间的key,如果没有,回退到noeviction.
allkeys-random 删除数据库中的key-v,直到腾出足够的内存
volatile-ttl 根据键值对象的ttl属性,删除最近将要过期的key-v对象,直到腾出足够内存为止。如果没有,回退到noviction策略
noeviction 不会删除任何数据,拒绝所有的写入操作并返回错误信息(error)OOM command not allowed when used memory ,此时redis
只响应读操作
如果没有符合条件的key,那么volatile-lru、volatile-random、volatile-ttl 相当于noeviction
修改配置文件:
maxmemory-policy 淘汰策略(volatile-lru、allkeys-lru)
动态配置
config set maxmemory-policy 淘汰策略
38.LRU淘汰原理
1.如果基于传统LRU算法实现Redis LRU会有什么问题?
传统LRU算法需要借助额外的数据结构存储,需要消耗一定内存,Redis LRU对传统的LRU算法进行改良,
通过随机采样实现。可以通过配置maxmemory_samples的值可以指定随机选取key的数量,然后从选取的key中删除掉
热度最低的key,值越大越能精确地定位到待淘汰的key,但是会消耗更多cpu资源,执行效率降低。
2.如何找出热度最低的数据?
redis中每个键值对都是一个dicEntry,其中key、value属性指向一个RedisObject对象,每个RedisObject对象
都有lru字段,且使用了unsigned低24位记录key的访问热度。对象创建时会记录lru值,访问时会更新lru值,不是
系统的时间戳,而是设置为全局变量server.lruclock的值。
3.server.lruclock的值怎么来的?
Redis中有一个定时处理的函数serverCron,默认每100毫秒调用函数updateCachedTime更新一次全局变量
server.lruclock的值,记录的时当前unix的时间戳。
4.为什么不获取精确的时间而是放在全局变量中?不会有延迟的问题吗?
如果每次更新RedisObject的lru值都从系统获取时间,会频繁的调用系统接口,导致消耗的cpu资源过多,降低了
系统执行效率,100毫秒更新一次完全可以满足需求,很大程程度上节省了cpu资源,并提高了执行效率。
不需要考虑延迟问题,只要lru字段有值,就可以进行键值对的热度评估。
39.为什么需要集群?
性能
单机和主从、哨兵高可用方案仍然存在单机性能瓶颈的问题,主从实现了读操作负载均衡,哨兵实现了自动故障转移,但是还是存在写操作无法负载均
衡,所以写入性能性能还是受限于单机性能瓶颈。
硬件
非集群方案,每个redis节点存储的数据都是相同的,如果数据量变大,就需要为每个节点增加内存容量,但是带来的提升是很小的。
可用性
主从架构如果master节点down掉之后,就只能提供读操作;
哨兵模式下master节点down掉,需要一定时间才能完成切换。
集群:实现读写操作负载均衡、数据分布式存储、节点故障快速自动转移、水平扩容(通过增加节点进而增加master节点数量提高集群整体的qps)。
40.主从复制
主从复制配置
replicaof 主节点ip 端口port
slaveof 主节点ip 端口port
主从复制原理
1.连接阶段
执行slaveof命令,会在本地保存master node的信息,包括主节点的host和端口
每个slave node中都会维护一个定时任务replicationCron,每隔1秒钟检查是否有新的master node需要连接和复制,如果发现,
就更master node建立socket网络连接,如果连接成功,就
2.数据同步阶段
3.命令传输阶段
60.客户端通信原理
客户端和服务器之间通过Tcp连接进行数据交互。
使用了Redis Serialization protocol(Redis序列化协议)进行编码。
Redis 序列化协议
特点:容易实现,可读性强,解析快。
总结
自己经过一段时间持续学习总结所得,redis基本数据类型和对应的底层数据结构、redis持久化机制(AOF、RDB)、内存淘汰机制、key的过期机制、主从方案、集群方案、哨兵机制都做过粗略的介绍