Redis:
redis是以内存作为数据存储介质的,所以读写速率极高,单机理想状态下,读的速度可高达110000次/s,写的速度高达81000/s
Redis为什么速度快?
1、Redis是基于内存操作的,内存处理效率远高于数据库。
2、影响Redis的瓶颈的因素不是CPU,而是内存和网络宽带。
3、Redis是单线程(线程安全的),利用I/O多路复用消耗最小的资源,达到最大的处理
速度,单线程避免了上下文切换的消耗。
一、Redis常用的数据类型
1、String
String 是二进制安全的,Redis中字符创最多可以存储512M。
操作命令:
set <key> <value> 存值
get <key> 取值
apend <key> <value> 在字符串后面追加
strlen <key> 获取value的长度
setnx <key> <value>存值,只有key不存在可以存值,key存在不能存值,不会覆盖。
incy <key> 将key 存储的数字值加1
decy <key> 将key存储的数字值减1
incrby/decrby <key> <步长> 加/减 步长值
mset <key1> <value1><key2> <value2>...... 同时设置多个key-value
mget<key1> <key2> ...... 同时获取多个value值
msetnx <key1> <value1><key2> <value2>......同时设置多个key-value ,但是key里面有一个key存在,所有操作都不成功。
setex <key> <过期时间> 设置key-value的同时可以设置过期时间
2、Hash
hash是一个键值对的集合,hash是string类型的filed和value的映射表,hash特别适合存储对象。
操作命令:
hset <key> <filed> <value> 给 key集合中的field字段赋值value。
hget <key> <filed> 从key集合中取出字段field的对应值。
hmset <key1> <filed1> <value2>......批量设置hsah值。
hexists <key> <filed> 判断hash表,key集合中的field是否存在。
hkeys <key> 列出hash表key集合中的所有field。
hvals <key> 列出hash表key集合中的所有value值。
hincrby <key> <filed> <increment>为hash表key集合中的field对应的值加1或-1。
hsetnx <key> <filed> <value>集合key中的field值设置为value,仅当field不在有效,不覆盖旧值。
数据结构:
hashtable+ziplist,当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
3、List(列表)
Redis列表是简单的字符串列表,安插入顺序排列,可以添加元素到列表头部或者尾部,list底层实际是双向链表,对两端的操作性很高,通过索引下标操作中间节点数据性能较低。
操作命令:
lpush/rpush <key><value1><value2><value3>......从左边/右边 插入一个或多个值
lpop/rpop <key>从左边/右边 土储一个值 ,list 值在键在,值光键亡。
rpoplpush <key1><key2>从列表key1的右边吐出一个值插入key2左边
lrang <key><起始位置><结束位置>按照索引下标获取元素(从左到右)
lrang key 0 -1 ,0左边第一个,-1右边第一个
lindex <key> <index>根据索引下标获取元素
llen <key>获取列表长度
insert <key> before <value> <newvalue>在 的后面插入
lrem <key> <n> <value> 从左边删除N个value
lset <key> <index> <value> 将 <key>下标为index的值替换为value
数据结构:
list 的数据结构为quickList,首先在列表元素较少的情况下,会使用一块连续的内存存储,这个结构是ziplist(压缩列表),将所有元素紧挨在一起存储,分配的是一块连续的内存。
当数据量比较多得时候才是quicklist,用链表将多个 quicklist连接起来,组成quicklist。
4、set
set是String类型无序不重复集合,底层其实是一个value值为null的hash表,所以删除,查找,添加的复杂度为1(o)。
操作命令:
sadd <key><value1><value2>........将一个或者多个member元素加入到集合key中,已存在的元素忽略。
smembers <key>取出集合的所有值
sismember <key><value>判断集合key是否含有value值,有返回1没有返回0。
scard <key>返回该集合的元素个数。
srem <key><value1><value2>...删除集合中的某个元素。
spop <key>随机从集合中吐出一个值。
srandmember <key><n>随机从集合中吐出n个值。
smove <key1><key2><value>把集合1中的value值移动到集合2中。
sinter <key1><key2> 返回两个集合的交集元素。
sunion <key1><key2> 返回两个集合的并集。
sdiff <key1><key2> 返回两个集合的差集。k1中的元素在k2中没有的元素。
数据结构:
set数据结构是dictionary(dict)字典,字典用hash表实现的。
5、sort set
redis有序集合zset与普通set相似,是一个没有重复元素的字符串集合.(注意set是无序集合)不同之处是有序集合zset,每个成员都关联一个评分(score),这个评分按照从低到高
的方式排列集合中的成员,集合中的成员是唯一的,但是评分可以是重复的。
操作命令:
zadd <key><score1><value1><score2><value2>.....将一个或多个member元素及其score值加入到有序集合key。
zrang <key> <start> <end> [withscores] 返回集合key下标在start和end之间的元素。带withscore 可以让分数和一起和值返回到结果集。
zrangbyscore <key> minmax [withscores] [limit offset count] 返回有序集合key,所有score值 介于 min 和 max 之间的的元素,包括等于min 和 max的元素,按score值递增排序.
zrangbyscore <key> maxmin [withscores] [limit offset count] 同上,按score递减排序。
zincrby <key><increment>为元素的score加上 increment的值
zrem <key><value>删除集合指定 的元素。
zcount <key> <min><max>统计集,分数区间内的元素个数。
zrank <key> <value>返回值在集合中的排名,从0开始。
数据结构:
hash和跳跃表(skiplist),hash作用就是关联元素value和权重score,保障元素value的唯一性,可以通过value找到对应的score值。
Redis6.0以后新数据类型
6、Bitmaps
比较节省空间,计算机用二进制(位)作为信息的基础单位,一个字节8位。
操作命令:
setbit <key><offset><value>设置 bitmaps 某个偏移量的值 offset从0开始。
getbit<key><offset>
7、HyperLogLog
基数,数量统计好用
操作命令:
pfadd<key><element>
pfcount<key> 统计基数 ( 不重复元素的个数=基数)
8、GeoSpatial
地理信息的缩写,该类型就是元素的二维坐标,地图上就是经纬度,redis基于该类型提供经纬度的设置,查询,范围查询,距离查询,经纬度hash等。
例如:
geoadd china:city 121.47 31.23 shanghai 城市上海的经纬度
geopos <key><member 获取位置坐标
geodist <key> <member1> <member2> [m l km lft lmi] 单位可选
获取两个位置的直线距离
georadius <key> <longitude> <datitude>radius [m l km lft lmi]
例如:georadius china:city 110 30 1000km 给定经纬度为中心,找出半径1000米的元素
二、Redis常见的并发问题
缓冲穿透:
比如数据中不存在ID=-1的订单数据,如果请求查询这条数据,在缓存中查询不到,会将请求打到下层数据库上,这就是缓存穿透。
查询缓存中不存在的数据,会导致请求打到数据库,这就是缓存穿透。
1、低频的缓存穿透是不可避免的。
2、需要避免高频的缓存穿透,高频穿透有可能导致数据库并发量太高,引起系统瘫痪。
解决方案:1:
缓存空对象
请求到Redis没有命中该数据,请求会命中数据库,如果数据库中也没有改数据,则Redis缓存一个空对象。
注意:缓存空对象设置过期时间,如果数据库中真存了这样的数据,从缓存中查询的是空对象,所以设置一个短暂的过期时间,
保证Redis和数据库的一个“弱一致性”,在一定程度上解决这个问题。
缺点1:
可能会缓存很多值为空的KEY,占用内存空间,同时Redis有LRU和LFU的内存淘汰策略,可能将有价值的数据淘汰。
缺点2:
设置了过期时间,会导致数据库和Redis中的数据在某一个时间段数据不一致。
解决方案2:
布隆过滤器
为了防止穿透,可以将数据库中存在的ID提前存放在一个List数组,后续请求到达,可以到List中查询请求的ID,如果存在请求继续,如果不存在,直接返回。
缺点1:数组的占用空间大,效率也会降低,使用Bloom Filter,BF是一个占用空间小,效率很高的随机数据结构。有bit数组和hash算法构成,可判断一个元素是否在意个集合中。
(BF存在hash碰撞为题,存在一定的错误率)
缓存击穿:
缓存击穿是缓存穿透的表现之一。
当某个数据被高并发访问时,如果这个数据的Redis的Key突然失效,会导致所有请求都命中数据库,数据库扛不住了,就会导致系统瘫痪。(多数会对应一些热点数据,比如微博热搜)
解决方案1:
热点数据不过期:
不设置热点数据的过期时间,会引发新的问题,数据库中已有的数据发生变化,Redis中没有变化,导致数据不一致。
这就需要保证Redis和数据库的一致性,强一致性很难做到,但是弱一致性还是要保证的,数据库发生变化时,同步到Redis中,通过监控Binlog日志,同步数据。
解决方案2:
分布式锁:
当热点数据key过期,允许这个key在Redis查不到数据,将其中一个请求命中数据库,然后将数据缓存到Redis,后面的请求就可以从Redis中拿到数据了。
缓存雪崩:
缓存雪崩也是缓存穿透的表现之一,缓存击穿时一个热点key失效,缓存雪崩就是多个热点数据的key同时失效。
雪崩原因
1、缓存过期时间比较一致,某一刻key大面积失效。
解决方式:将过期时间设置成随机数,不一致。
2、Redis挂了,或因网络抖动无法访问Redis
解决方式:使用Redis集群。
三、Redis持久化
1、RDB(快照)
将当前Redis的内存快照保存到磁盘上的dump.rdb文件中,这个过程 主要执行一个命令bgsave。触发RDB持久化机制:(Redis默认是开启RDB的)
1、满足配置文件中 save m n 触发条件条件
2、slave 全量复制master时,会发请求信息给master,master这时会 执行bgsave命令,执行完bgsave之后会将dump.rdb文件发给slave。
3、执行shutdown 和 flushall 命令时,Redis会自动 执行bgsave命令,进行持久化操作。
缺点:
1、RDB不能做到实时持久化 ,因为 bgsave每次执行都会fork操作,创建子线程。
2、在一定 间隔时间备份一次,两次快照间隔时间段的数据有丢失的可能 ,Redis意外down掉的话,最后一次快照的所有修改都会丢失。
2、AOF日志文件(append only file)
以独立日志的方式记录每次除了读操作外的命令,重启时再重新执行AOF文件中的命令达到库数据恢复的目的。(目前是Redis持久化的主流方式)
1、Redis默认是不开启AOF,需要 redis.conf配置文件中手动打开append only mode no 改成 yes 开启AOF,redis.conf 配置文件appendfsync 参数控制写入appendonly.aof 的策略。
1、NO( AOF_FSYNC_NO) :
概念:只写入 不保存
风险: 损失上一次rsync后的命令 写入-阻塞 保存-阻塞
触发: 1 AOF 或 Redis 关闭时执行,2 不做任何策略,将策略配置交给操作系 统,一般是操作系统是等待缓冲区被占满后,将数据写入aof 中。
2 、(EVERYSEC)AOF_FSYNC_EVERYSEC :
概念:每一秒钟保存一次。
风险: 损失前一秒的命令 写入-阻塞 保存-不阻塞
触发:每秒触发写入和保存
3、(ALWAYS) AOF_FSYNC_ALWAYS :
概念:每执行一个命令保存一次 高消耗,最安全,
风险:损失上一个命令 写入-阻塞 保存-阻塞
触发: 每条命令确保保存到磁盘后才会处理下个请求
3、混合持久化:
Redis 4.0以后可以开启 aof-use-rdb-preamble yes 混合持久化。(必须先开启aof)
如开启混合持久化,aof重写时,不在是单纯将内存数据写到aof文件,而是将重写这一刻之前的内存数据做 RDB快照处理(rbd快照的数据格式写到aof文件中),
并且将rdb快照内容和增量的AOF缓冲区的命令存在一起(都写在aof文件中),都写如新的aof文件,等重写完新的aof文件替换掉旧的aof文件。
于是Redis重启的时候,可以先加载RDB内容,然后在重放增量aof日志就可以代替之前的aof全量文件重放,因此重启效率大幅提升。
总结:混合持久就是aof重写的时候,用rdb二进制压缩格式写入aof文件的。
AOF重写策略:
重写策略解决文件不断增加的问题。
CopyOnWrite原理,就是写时复制,其基本策略就是,如果共享数据发生来改变(某线程进行了写操作),那么将会生成一个数据的复本供父线程使用。
AOF重写过程
重写:redis是通过读取当前数据库状态实现重写的,并不是对旧的aof文件进行读取。从而减少了redis操作命了和aof文件的大小。
(1)redis 服务fork 出一个子进程,该子进程的目的就是不依赖已有的AOF文件,根据redis已有的数据生成新的AOF文件;
(2)主进程将实时的redis操作写入AOF缓存区(这个数据要写入老的aof文件)和重写缓冲区(最终追加到新的AOF文件中,缓冲区作用是避免漏追加)。
(3)子进程对旧数据重写完成会通知主进程,向主进程发送一个信号。
(4)主进程收到信号,调用一个信号处理函数,该函数把重写缓冲区的命了追加到新的AOF文件中,然后替换掉现有的AOF。
重写触发条件
1、手动触发 bgrewriteaof
2、自动触发
1)没有bgrewriteaof/bgsave 在进行
2)当前aof大小 大于aoto_aof_rewrite_min_size (默认1MB)
3)当前 AOF 文件大小和最后一次 AOF 重写后的大小之间的比率大于等于指定的增长百分比。
参数:
appendonly 开启aof
appendfilename aof文件名(默认appendonly.aof)
appendfsync aof写入策略
auto-aof-rewrite-percentage 当前和上次aof文件增长大小对比,进行重写
aoto-aof-rewrite-min-size 重写最小值;
Redis数据恢复:
redis重启,首先判断AOF功能是否开启,如果开启并存在AOF持久化文件,则只会使用AOF恢复数据,不会使用RBD快照恢复。
aof-load-truncafed 配置,兼容AOF尾部命令写入不全的情况。(遇到不全的情况会自动忽略)。
优点:AOF每一次修改都会同步,文件完整性高。
缺点:AOF文件远大于RDB,数据恢复速度较慢。
Redis哨兵模式:
1、哨兵模式是基于主从模式的,主从模式下,当master宕机后,需要手动把一台服务器切换成master,需要人工干预。
2、哨兵是一个独立的进程,它会独立运行,原理是,哨兵进程向所有redis实例发送指令,等待redis服务器响应,从而监控多个运行的redis实例。
3、哨兵会有多个,一般为了方便选举,使用奇数哨兵,哨兵可以和redis部署在一起,也可以单独部署在其他服务器上,哨兵之间也会互相通信,检查其他哨兵是否正常运行,
同时发现master是否宕机,如果宕机哨兵之间会进行决策选举新的master。不需要人工干预。
Redis事物:
Redis事物是一个单独的、隔离操作。事物中的所有命令都会序列化,按顺序执行,事物在执行过程中,不会被其他客户端发送的命令请求打断。
Redis事物的作用:
Redis事物的作用主要就是串联多个命令,防止别的命令插队,事物间都是相互隔离的。
Multi、Exec、discard:
multi->表示事物开启,可以组队,操作命令
Exec-> 执行事物提交
discard-> 放弃组队,不执行命令。
multi命令开始后,输入的命令都会一次进入命令队列中,但不会执行,直到输入exec,Redis会将之前命令队列中的命令依次执行。组队过程中可以通过discard放弃组队
(exec命令执行前放弃)。组队过程中出现错误,所有命令都执行不了,组队过程没有错误,exec执行时,那个命令有错误那个命令执行不了,其余可以正常执行。
事物的三特性:
1、单独的隔离操作、事物中的所有命令都会序列化,按顺序执行,事物在执行过程中,不会被其他客户端发送的命令请求打断。
2、没有隔离级别的概念、因为事物提交前任何命令都不会被执行。
3、不保证原子性、事物中有如果有一条指令执行失败,其余指令仍然被执行,没有回滚。
Redis回收策略:
当内存使用达到了设置的maxmemory触发回收策略:
maxmemory-policy noevication不回收 内存达到预值,再次申请内存报错(默认策略)
Volatile-lru 尝试回收最长时间未被使用的key,仅限于在过期的集合中
Allkeys-lru 尝试回收最长时间未被使用的key,作用范围所有key
Volatile-lfu 尝试回收一定时间内使用次数最少的,仅限于过期集合
Allkeys-lfu 尝试回收一定时间内使用次数最少的,作用范围所有key
Volatile-random 从已过期的数据中随机删除
Allkeys-random 从所欲的数据中随机删除
Volatile-ttl 从已过期的数据集中任意挑选
多路复用的理解:
Redis服务端对于命令的处理是单线程的,但是在I/O层面却可以同时面对多个客户端并发的提供服务,并发到内部单线程的转化通过多路复用框架实现
一个IO操作的完整流程是数据请求先从用户态到内核态,也就是操作系统层面,然后再调用操作系统提供的API,调用相应的设备去获取相应的数据。
当相应的设备准备好数据后,会将数据复制到内核态。数据从相应的设备到内核态的处理方式分为:阻塞和非阻塞。
阻塞:用户请求会等待数据准备好从操作系统调用相应的设备返回到内核态,如果没有返回 处于阻塞状态。
非阻塞:非阻塞是操作系统接收到一组文件描述符,然后操作系统批量处理这些个文件描述符,然后不管有没有准备好数据都立即返回,
如果没有对应的准备好的文件描述符,则继续轮询获取准备好数据的文件描述符。
数据从内核态到用户态的复制数据的处理方式的不同可分为:同步和异步
同步:用户请求等待数据从内核态向用户态复制数据,在此期间不做其他的事情,次称为同步
异步:在数据从内核态向用户态复制的过程中,用户请求不会一直处于等待状态而是做其他事情,称为异步。
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。
简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。
核会一直监听这些套接字上的连接请求或数据请求。
一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
主从复制:
1、从服务器向主服务器发送 SYNC 命令。
2、收到 SYNC 命令的主服务器执行 BGSAVE 命令, 在后台生成一个 RDB 文件, 并使用一个缓冲区记录从现在开始执行的所有写命令。
3、当主服务器的 BGSAVE 命令执行完毕时, 主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器, 从服务器接收并载入这个 RDB 文件,
将自己的数据库状态更新至主服务器执行 BGSAVE 命令时的数据库状态。
4、主服务器将记录在缓冲区里面的所有写命令发送给从服务器, 从服务器执行这些写命令, 将自己的数据库状态更新至主服务器数据库当前所处的状态。
Redis线程模型:
![]()
1、文件事件处理器
redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler。这个文件事件处理器,是单线程的,redis才叫做单线程的模型,采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来处理这个事件
如果被监听的socket准备好执行accept、read、write、close等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。
文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了redis内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个socket,IO多路复用程序,文件事件分派器,事件处理器(命令请求处理器、命令回复处理器、连接应答处理器,等等)。
多个socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,但是会将socket放入一个队列中排队,每次从队列中取出一个socket给事件分派器,事件分派器把socket给对应的事件处理器。
然后一个socket的事件处理完之后,IO多路复用程序才会将队列中的下一个socket给事件分派器。文件事件分派器会根据每个socket当前产生的事件,来选择对应的事件处理器来处理。
2、文件事件
当socket变得可读时(比如客户端对redis执行write操作,或者close操作),或者有新的可以应答的sccket出现时(客户端对redis执行connect操作),socket就会产生一个AE_READABLE事件。
当socket变得可写的时候(客户端对redis执行read操作),socket会产生一个AE_WRITABLE事件。
IO多路复用程序可以同时监听AE_REABLE和AE_WRITABLE两种事件,要是一个socket同时产生了AE_READABLE和AE_WRITABLE两种事件,那么文件事件分派器优先处理AE_REABLE事件,然后才是AE_WRITABLE事件。
3、文件事件处理器
如果是客户端要连接redis,那么会为socket关联连接应答处理器
如果是客户端要写数据到redis,那么会为socket关联命令请求处理器
如果是客户端要从redis读数据,那么会为socket关联命令回复处理器
4、客户端与redis通信的一次流程
在redis启动初始化的时候,redis会将连接应答处理器跟AE_READABLE事件关联起来,接着如果一个客户端跟redis发起连接,此时会产生一个AE_READABLE事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的socket,同时将这个socket的AE_READABLE事件跟命令请求处理器关联起来。
当客户端向redis发起请求的时候(不管是读请求还是写请求,都一样),首先就会在socket产生一个AE_READABLE事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从socket中读取请求相关数据,然后进行执行和处理。
接着redis这边准备好了给客户端的响应数据之后,就会将socket的AE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在socket上产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入socket,供客户端来读取。
命令回复处理器写完之后,就会删除这个socket的AE_WRITABLE事件和命令回复处理器的关联关系。