文章目录
1、Redis介绍
redis是一个开源的基于内存的、可持久化的key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型的操作都是原子性的。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
2、redis与memcached的区别
-
Memcached:
•很早出现的NoSql数据库
•数据都在内存中,一般不持久化
•支持简单的key-value模式
•一般是作为缓存数据库辅助持久化的数据库
•memcached(多线程)的读写速度高于redis
•memcached不支持复制
-
redis:
•几乎覆盖了Memcached的绝大部分功能
•数据都在内存中,支持持久化,主要用作备份恢复
•除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等。
•一般是作为缓存数据库辅助持久化的数据库
redis是单线程的
3、redis五大数据类型
- 基于key的操作:
keys *
查询当前库的所有键
exists <key>
判断某个键是否存在
del <key>
删除某个键
expire <key> <seconds>
为键值设置过期时间,单位秒。
ttl <key>
查看还有多少秒过期,-1表示永不过期,-2表示已过期
dbsize
查看当前数据库的key的数量
flushdb
清空当前库
flushall
通杀全部库(会触发rdb存盘)
-
string类型
-
String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
-
String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
-
String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
-
get <key>
查询对应键值
set <key> <value>
添加键值对
setex <key> <过期时间> <value>
设置键值的同时,设置过期时间,单位秒。
setnx <key> <value>
只有在 key 不存在时设置 key 的值
incr <key>
将 key 中储存的数字值增1
只能对数字值操作,如果为空,新增值为1
decr <key>
将 key 中储存的数字值减1
只能对数字值操作,如果为空,新增值为-1
incrby / decrby <key> <步长>
将 key 中储存的数字值增减。自定义步长。
mset <key1> <value1> <key2> <value2> .....
同时设置一个或多个 key-value对
mget <key1> <key2> <key3> .....
同时获取一个或多个 value
- list类型
Redis列表是简单的字符串列表,按照插入顺序排序。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
lpush/rpush <key> <value1> <value2> <value3> ....
从左边/右边插入一个或多个值。
lpop/rpop <key>
从左边/右边弹出一个值。
值在键在,值光键亡。
rpoplpush <key1> <key2>
从<key1>列表右边弹出一个值,插到<key2>列表左边。
lrange <key> <index>
按照索引下标获得元素(从左到右)
lindex <key> <index>
按照索引下标获得元素(从左到右)
llen <key>
获得列表长度
- set类型
Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。无序不重复
sadd <key> <value1> <value2> .....
将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。
smembers <key>
取出该集合的所有值。
sismember <key> <value>
判断集合<key>是否为含有该<value>值,有返回1,没有返回0
srem <key> <value1> <value2> ....
删除集合中的某个元素。
sinter <key1> <key2>
返回两个集合的交集元素。
sunion <key1> <key2>
返回两个集合的并集元素。
sdiff <key1> <key2>
返回两个集合的差集元素。
- hash类型
Redis hash 是一个键值对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
hset <key> <field> <value>
给<key>集合中的 <field>键赋值<value>
hget <key1> <field>
从<key1>集合<field> 取出 value
hmset <key1> <field1> <value1> <field2> <value2>...
批量设置hash的值
hexists key <field>
查看哈希表 key 中,给定域 field 是否存在。
hgetall <key>
列出该hash集合的所有field和values
hincrby <key> <field> <increment>
为哈希表 key 中的域 field 的值加上增量 increment
- zset类型
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的没有成员都关联了一个评分(score) ,这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
zadd <key> <score1> <value1> <score2> <value2>...
将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
zrange <key> <start> <stop> [WITHSCORES]
返回有序集 key 中,下标在<start> <stop>之间的元素
带WITHSCORES,可以让分数一起和值返回到结果集。
有序输出从小到大
zrevrange <key> <start> <stop> [WITHSCORES]
同上,逆序按评分从大到小
zincrby <key> <increment> <value>
为元素的score加上增量
zrem <key> <value>
删除该集合下,指定值的元素
zcount <key> <min> <max>
统计该集合,分数区间内的元素个数
zrank <key> <value>
返回该值在集合中的排名,从0开始。
4、简单的动态字符串SDS和C语言自带的字符串有什么不同?
在redis里面,C字符串(以空字符结尾的字符数组)只会作为字符串字面量用在一些无须对字符串值进行修改的地方,比如打印日志。
当redis需要一个可以被修改的字符串值时,redis就会使用SDS(simple dynamic string)来表示字符串值。
除了用来保存数据库中的字符串值之外, SDS 还被用作缓冲区(buffer): AOF 模块中的 AOF 缓冲区, 以及客户端状态中的输入缓冲区, 都是由 SDS 实现的。
SDS的定义
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
SDS 遵循 C 字符串以空字符结尾的惯例, 保存空字符的 1
字节空间不计算在 SDS 的 len
属性里面, 并且为空字符分配额外的 1
字节空间, 以及添加空字符到字符串末尾等操作都是由 SDS 函数自动完成的, 所以这个空字符对于 SDS 的使用者来说是完全透明的。
遵循空字符结尾这一惯例的好处是, SDS 可以直接重用一部分 C 字符串函数库里面的函数:
SDS与C字符串的区别
常数复杂度获取字符串长度
为了获取一个 C 字符串的长度, 程序必须遍历整个字符串, 对遇到的每个字符进行计数, 直到遇到代表字符串结尾的空字符为止, 这个操作的复杂度为 O(N) 。
SDS 在len
属性中记录了 SDS 本身的长度, 所以获取一个 SDS 长度的复杂度仅为O(1)。
设置和更新 SDS 长度的工作是由 SDS 的 API 在执行时自动完成的, 使用 SDS 无须进行任何手动修改长度的工作。
杜绝缓冲区溢出
除了获取字符串长度的复杂度高之外, C 字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow)。
当 SDS API 需要对 SDS 进行修改时, API 会先检查 SDS 的空间是否满足修改所需的要求, 如果不满足的话, API 会自动将 SDS 的空间扩展至执行修改所需的大小, 然后才执行实际的修改操作。
减少修改字符串时带来的内存重分配次数
每次增长或者缩短一个 C 字符串, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作。因为内存重分配涉及复杂的算法, 并且可能需要执行系统调用, 所以它通常是一个比较耗时的操作。
为了避免 C 字符串的这种缺陷, SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联。通过未使用空间, SDS 实现了空间预分配和惰性空间释放两种优化策略。
空间预分配
空间预分配用于优化 SDS 的字符串增长操作: 当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。
其中, 额外分配的未使用空间数量由以下公式决定:
- 如果对 SDS 进行修改之后, SDS 的长度将小于
1``MB
, 那么程序分配和len
属性同样大小的未使用空间。 - 如果对 SDS 进行修改之后, SDS 的长度将大于等于
1``MB
, 那么程序会分配1``MB
的未使用空间。
在扩展 SDS 空间之前, SDS API 会先检查未使用空间是否足够, 如果足够的话, API 就会直接使用未使用空间, 而无须执行内存重分配。
通过空间预分配策略, Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。
惰性空间释放
惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free
属性将这些字节的数量记录起来, 并等待将来使用。
通过惰性空间释放策略, SDS 避免了缩短字符串时所需的内存重分配操作, 并为将来可能有的增长操作提供了优化。
与此同时, SDS 也提供了相应的 API , 让我们可以在有需要时, 真正地释放 SDS 里面的未使用空间, 所以不用担心惰性空间释放策略会造成内存浪费。
二进制安全
C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
SDS 的 API 都是二进制安全的(binary-safe): 所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf
数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。
这也是我们将 SDS 的 buf
属性称为字节数组的原因 —— Redis 不是用这个数组来保存字符, 而是用它来保存一系列二进制数据。
通过使用二进制安全的 SDS , 而不是 C 字符串, 使得 Redis 不仅可以保存文本数据, 还可以保存任意格式的二进制数据。
兼容部分 C 字符串函数
通过遵循 C 字符串以空字符结尾的惯例, SDS 可以在有需要时重用一部分 <string.h>
函数库, 从而避免了不必要的代码重复。
5、redis的过期策略和缓存淘汰机制
策略分为:
- 定期删除:redis每隔100ms检查,是否有过期的key,有过期的则直接删除。redis并不是每隔100ms将所有的key检查一次,而是随机抽取。因此,后又很多的key到时间没有删除。
- 惰性删除:获取某一个key的时候,会检查key,若过期,则删除。
机制:
- no’eviction:当内存不足以容纳新的写入数据,新写入报错
- allkeys-lru:当内存不足以容纳新的写入数据,移除最近最少用的key
- allkeys-random:当内存不足以容纳新的写入数据,键空间随机移除key
- 等等。。。。
6、reids的持久化
Redis 提供了2个不同形式的持久化方式。
RDB (Redis DataBase)
AOF (Append Of File)
- RDB:
•在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Ø备份是如何执行的?
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
Ø关于fork?
•在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”,一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程
Ørdb的优点:
•节省磁盘空间
•恢复速度快
Ørdb的缺点:
•虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
•在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
- AOF
•以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
•AOF默认不开启,需要手动在配置文件中配置,•可以在redis.conf中配置文件名称,默认为 appendonly.aof
•AOF和RDB同时开启,系统默认取AOF的数据
ØAOF同步频率设置?
•始终同步,每次Redis的写入都会立刻记入日志
•每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
•把不主动进行同步,把同步时机交给操作系统。
- Rewrite
•AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof。
ØRedis如何实现重写?
•AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
Ø何时重写?
•系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
ØAOF的优点:
•备份机制更稳健,丢失数据概率更低。
•可读的日志文本,通过操作AOF稳健,可以处理误操作。
ØAOF的缺点:
•比起RDB占用更多的磁盘空间。
•恢复备份速度要慢。
•每次读写都同步的话,有一定的性能压力。
•存在个别Bug,造成恢复不能。
7、Redis的主从复制
主从复制,就是主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主。
采用完整同步和部分同步的模式:
完整同步:通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓存区的写命令来进行同步。
部分同步:当从服务器在断线后重新连接主服务器,主服务器可以将主从服务器连接断开期间的写命令发送给从服务器,从服务器只要接受并执行这些命令,就可以将数据库更新至当前状态。
问题:假设一台master,两个slave
1、slave1、slave2是从头开始复制还是从切入点开始复制? —>从头开始复制
2 从机是否可以写?set可否?
从机是只读的,不能set。
3 主机shutdown后情况如何?从机是上位还是原地待命
主机挂掉以后,从机一直等待。
4 主机又回来了后,主机新增记录,从机还能否顺利复制?
可以,主机可以再次恢复master地位。
5 其中一台从机down后情况如何?依照原有它能跟上大部队吗?
若重新将从机加入到master中,则可以跟上,否则不行,除非在设置文件中添加主从配置。
Ø复制原理?
•每次从机联通后,都会给主机发送sync指令
•主机立刻进行存盘操作,发送RDB文件,给从机
•从机收到RDB文件后,进行全盘加载
•之后每次主机的写操作,都会立刻发送给从机,从机执行相同的命令
- 知识点:可以将两个从节点设置为链式的,即master只有一个从节点,这个从节点可以作为另一个从节点的主节点,这样可以缓解主节点的复制压力。风险是一旦某个slave宕机,后面的slave都没法备份
反客为主
• 当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。。
•用 slaveof no one 将从机变为主机。还要将其他的从节点的master进行更新。
Ø哨兵模式(sentinal)
•反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库.
哨兵模式中,若主节点挂掉,则重新选举一个新的主节点。若原来的主节点之后再次启动,也只能作为从节点使用。
8、缓存雪崩和缓存穿透
缓存雪崩:缓存同一时间大面积失效,后面的请求都会落在数据库,造成数据库短时间内承受大量的请求崩掉。
解决:缓存数据的过期时间设置为随机,防止同一时间大量的缓存失效
若缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中
设置热点数据永不过期
缓存穿透是指查询一个一定不存在的数据。由于缓存命不中时会去查询数据库,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决方案:
① 是将空对象也缓存起来,并给它设置一个很短的过期时间,最长不超过5分钟
② 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力