Redis是一个开源的、基于内存的数据结构存储器,可以用作数据库、缓存和消息中间件。其实是一个C/S架构。Redis的Server是单线程服务器,基于Event-Loop模式来处理Client的请求。
目录
数据结构:
string
radis中的字符串的底层数据结构是一种动态字符串(simple dynamic string)。在原码中,redis使用范型定义很多次,因为它为了对内存作极致的优化,不同长度的字符串使用不同的结构体表示。redis规定字符串不能超过512M。
动态字符串与C字符串区别在于,C字符串获取字符串长度的时间复杂度为O(N),不能安全地拼接和操作,只能保存文本信息。所以不符合redis对字符串在安全性、效率及功能的要求。
基本操作 :get/set/incr/incrby/decr/decrby/mset/mget/exists/del/type
//1.set和get命令设置键值对,set可以有第三个参数判断参数是否已经存在
//可以把任何值作为value,甚至是图片,但是value不可以超过512M
> set mykey somevalue
OK
> get mykey
"somevalue"
> set counter 100
OK
//2.incr是原子操作,哪怕有多个client去做incr操作,counter也是逐一增加的。
> incr counter
(integer) 101
> incrby counter 50
(integer) 152
//3.mset和mget命令
> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"
值得一提的是关于临时变量设置:
EXPIRE 设置过期时间,这个功能通常用来控制缓存失效时间。
PERSIST 设置永久值
GETSET 为key设置一个值并返回原值,在统计时需要返回原值的同时置0
//该变量存在5秒
> expire key 5
(integer) 1
> ttl key
(integer) 4
//重新变为永久变量,ttl不存在
> PERSIST mykey
(integer) 1
> TTL mykey
(integer) -1
//getset命令用于统计
> set key value
OK
> getset key value1
"value"
> get key
"value1"
使用场景:常规key-value缓存场景,常规计数:微博数,粉丝数。
list
Redis里面的list是一个链表结构,所以访问索引的速度不快,但是添加的速度很快。因为对于数据库系统而言,至关重要的是能够以非常快的方式将元素添加到很长的列表中。当快速访问大量元素的中间部分很重要时,可以使用另一种称为sorted set的数据结构。
- Redis链表特性:
①、双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。
②、无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。
③、带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
④、多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。
标准案例:使用生产者将项目推入列表,而消费者(通常是工人)消耗这些项目和执行的操作的消费者-生产者模式,进行流程之间的通信。比如,用户发布图片时,用lpush推入,等到查看用户主页时,我们会展示最新的几张图片。
基本操作:rpush/lpush/lrange/rpop/lpop 可以用于实现队列、栈。
> rpush mylist A B first
(integer) 3
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
> rpop mylist
"c"
值得一提的操作:
LTRIM 提供截断数组的功能,生成新的list
BRPOP 可以等待列表塞入再返回,可以同时等待多个列表
> rpush mylist 1 2 3 4 5
(integer) 5
//这里的ltrim类似于数组截断,会生成一个新的list表
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
//brpop可以等待5秒再返回pop元素,注意:也可以同时等待多个列表。
> brpop tasks 5
1) "tasks"
2) "do_something"
有几条规则要注意:
- 如果结构体不存在就插入,会先创建一个再插入;如果结构体已经存在,则不能改变其结构体状态。
- 如果从结构体种删除所有元素,则该结构体会自动销毁。但是stream结构体这种情况除外。
使用场景:
- 文章列表/取最新数据 :每个用户都有属于自己的文章列表.此时可以考虑使用列表,因为列表不但是有序的,同时支持使用lrange按照索引范围获取多个元素。lpush+ltrim=Capped Collection(有限集合)
- 消息队列系统/延时操作:redis的lpush-brpop命令组合即可实现阻塞队列,生产者客户端使用lpush命令向列表插入元素.消费者客户端使用brpop命令阻塞式的"抢"列表中的尾部元素.多个客户端保证消息的负载均衡与可用性。lpush+brpop=message queue
- 其他:lpush+lpop=Stack(栈)、lpush+rpop=queue(队列)、
hash
这里的哈希表类似于hashmap,也是数组+链表结构,用链地址法解决冲突问题。
- 扩容和缩容问题:
1.在源码中字典结构内部包含两个hash table,通常情况下只有一个是有值的,但是在字典扩容和缩容时,需要分配新的hashmap,然后进行渐进式搬迁。
2.使用渐进式搬迁是因为,单线程的redis无法承受耗时的过程,所以在查询时会查询两个hash结构,然后在后续定时任务以及hash操作指令中,循序渐进地把旧字典内容迁移。
3.扩缩容条件:元素个数=第一维数组长度,扩容的新数组是原数组大小的2倍。如果redis正在做持久化命令,则尽量不扩容。但是hash非常满,达到第一维数组的5倍时,则强制扩容。当元素个数<数组长度10%时就会缩容,缩容不会考虑redis是否在做持久化命令。
基本操作:hmset/hgetall/hset/hget
//批量操作hmset
> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"
应用场景:存储读取用户信息。用key+field(标签)作为key,操作value,不会带来序列化复杂、并发修改控制的问题。
set
set是一个很简单的数据结构,和STL中的set相似,是无序的。
内部实现:整数集合/哈希表
> sadd myset 1 2 3
(integer) 3
> smembers myset
1) "1"
2) "2"
3) "3"
> sismember myset 3
(integer) 1
> spop myset
"2"
使用场景:
- 标签(tag):微博粉丝存在于一个合集,提供了求交集、并集和差集的操作,非常方便地提供如共同关注、二度好友、共同喜好等功能。
- 唯一性、访问独立IP
sorted sets
sorted set 是一种数据类型。与set相比,sorted set增加了一个权重参数score,使之有序排列。内部使用ziplist和跳跃表来保证数据的存储和有序。
只有同时满足如下条件是,使用的是ziplist,其他时候则是使用skiplist
有序集合保存的元素数量小于128个
有序集合保存的所有元素的长度小于64字节
ziplist压缩列表:成员与score的映射,每个集合元素使用两个紧挨在一起的压缩列表结点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。
hash+跳表:使用跳表按序保存元素分值,使用哈希来保存元素和分值的对应关系。跳跃表是一种基于有序链表的扩展,简称跳表.跳表会维护多个索引链表和原链表.
排序规则:
- 如果A和B是两个分数不同的元素,则如果A.score是> B.score,则A>B。
- 如果A和B的分数完全相同,则A字符串在字典上大于B字符串,则A>B。
- A和B字符串不能相等,因为排序集仅具有唯一元素。
基础操作:
> zadd hackers 1940 "Alan Kay"
(integer) 1
> zadd hackers 1957 "Sophie Wilson"
(integer) 1
> zadd hackers 1953 "Richard Stallman"
(integer) 1
> zadd hackers 1949 "Anita Borg"
(integer) 1
> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
> zadd hackers 1916 "Claude Shannon"
(integer) 1
> zadd hackers 1969 "Linus Torvalds"
(integer) 1
> zadd hackers 1912 "Alan Turing"
(integer) 1
> zrange hackers 0 -1
> zrevrange hackers 0 -1
> zrange hackers 0 -1 withscores
> zrangebyscore hackers -inf 1950
> zremrangebyscore hackers 1940 1960
> zrank hackers "Anita Borg"
使用场景:
- topN --zrank
- 带有权重的排行榜 --zrange withscores
- 精准设定过期时间的应用 --zremrangebyscore
- 模糊查询 -ZRANGEBYLEX zset - + LIMIT 0 10 可以进行分页数据查询,其中- +表示获取全部数据
三大经典问题及解决方案
缓存穿透
业务系统访问压根就不存在的数据,就称为缓存穿透。如果存在海量请求查询压根就不存在的数据,那么这些海量请求都会落到数据库中,数据库压力剧增,可能会导致系统崩溃。
方案一:将数据库查询结果为空的key也存储在缓存中。当后续又出现该key的查询请求时,缓存直接返回null,而无需查询数据库。
方案二:当业务系统有查询请求的时候,首先去BloomFilter中查询该key是否存在。若不存在,则说明数据库中也不存在该数据,因此缓存都不要查了,直接返回null。若存在,则继续执行后续的流程,先前往缓存中查询,缓存中没有的话再前往数据库中的查询。
对于空数据的key各不相同、key重复请求概率低的场景而言,应该选择第二种方案。而对于空数据的key数量有限、key重复请求概率较高的场景而言,应该选择第一种方案。
缓存雪崩
如果缓存因某种原因发生了宕机,那么原本被缓存抵挡的海量查询请求就会像疯狗一样涌向数据库。此时数据库如果抵挡不了这巨大的压力,它就会崩溃。
方案一:缓存集群+负载平均算法
方案二:Hystrix,java类库,通过熔断、降级、限流三个手段来降低雪崩发生后的损失。
热点击穿
数据过了时间会被缓存删除,但是对于一些请求量极高的热点数据而言,一旦过了有效时间,此刻将会有大量请求落在数据库上,从而可能会导致数据库崩溃。
- 互斥锁:分为提前/查询后使用两种
当key值没有被查询到/失效之后,第一个数据库查询请求发起后,就将缓存中该数据上锁(Redis的SETNX);此时到达缓存的其他查询请求将无法查询该字段,从而被阻塞等待;当第一个请求完成数据库查询,并将数据更新值缓存后,释放锁;此时其他被阻塞的查询请求将可以直接从缓存中查到该数据。
代码:
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
优点:思路简单、保证数据一致性
缺点:可能会死锁或者阻塞
- 永远不过期:
(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
优点:异步构建缓存,不会阻塞线程池
缺点:数据不一致(可能取的是老数据)、占内存、代码复杂
定期删除和惰性删除
- 配置内存大小:配置文件redis.conf修改或者用命令修改。
> config set maxmemory 100mb
- 配置淘汰策略:配置文件redis.conf修改或者用命令修改。
config get maxmemory-policy
- 过期数据的底层数据结构:每一个数据不仅有自身的存储地址,且有一个存储空间保存key和过期时间的键值对数据。
redis的过期策略是,定期删除和惰性删除。
定期删除是指redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期则删除。
惰性删除是指redis在每次获取一个key时都会检查一下是否过期,如果过期则会删除。
如果这两个都没有走到,其实还是会有问题,那么我们就用内存淘汰机制:
淘汰策略
TTL淘汰
Random淘汰
近似LRU算法
当最大内存使用完了,如果一个数据在最近一段时间没有使用到,那么将来被使用到的可能性也很小,所以被淘汰掉。redis通过随机采样5个key,从里面淘汰最近最少使用的。redis为了实现近似LRU算法。给每个key额外增加一个24bit字段用来存储key最后一次被访问的时间。
- Redis3.0的优化
维护一个候选池,随机访问的key如果小鱼池中最小时间则放入池中,池满后线移出最近使用的key。
LFU算法
Redis4.0中,根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰。
Redis集群
主从复制模式
当我们遇到数据可用性差(redis挂了数据全部丢失)和数据查询缓慢(某一个缓存访问量特别高)时,我们想到用数据库常用的主从复制方式解决,后来还运用了Master/slave chains。
- 数据高可用:Master负责接收客户端的写入请求,将数据写到Master后,同步给Slave,实现数据备份。一旦Master挂了,可以将Slave提拔为Master;适用于备份和故障恢复。
- 提高查询效率:一旦Master发现自己忙不过来了,可以把一些查询请求,转发给Slave去处理,也就是Master负责读写或者只负责写,Slave负责读;读写分离,负载均衡。
主从复制只能从主节点复制到从节点,复制是单向的,只能由主节点到从节点。默认情况下,每台redis服务器都是主节点,且一个主节点可以有多个从节点。
主从连接流程
- 建立连接,保持主节点信息
方式一:客户端键入命令slaveof <masterip> <masterport>
方式二:启动服务器时加上--slaveof <masterip><masterport>
方式三:服务器配置文件中加上slaveof <masterip> <masterport>
- 建立socket连接
- slave周期性发送指令ping (定时器任务)
- master权限验证/身份验证,发送指令
auth password
- slave发送端口信息,master保存
状态:slave端保存了master的端口,master端保存了slave端口。
主从复制流程
- slave发送指令请求同步:
psync2 ? -1
- master创建复制缓冲区、RDB文件同步数据:master执行
bgsave
(RDB方式恢复速度比较快),生成RDB文件后通过socket发送给slave(这时候会发送runid和offset给slave)。 - slave接收RDB文件,清空数据,恢复数据 (全量复制)。
- 复制缓冲区内容恢复部分同步数据 (增量复制):
psync2 <runid> <offset>
- 关于复制缓冲区:
- 复制缓冲区大小设置要合理,建议master内存占有50%-70%,剩下的部分用于创建复制缓冲区。
- 这是个先进先出的队列结构,来源于AOF,用于接收master中数据更改的指令。master和slave分别会记录offset,即复制缓冲区中同步的偏移量。
命令传播阶段
master与slave保持实时同步,引入心跳机制,判断对方是否在线。
master:PING / 10秒
slave(请求数据):REPLCONF ACK{offset} / 1秒
假如master宕机,如何恢复数据?(哨兵模式)
假如主库127.0.0.1 6379,从库127.0.0.1 6380
1.在从数据库中执行SLAVEOF NO ONE命令,断开主从关系;
2.选择其中一个slave提升为主库继续服务,与其他slave建立主从关系;
3.主从全部重启连接
哨兵:分布式系统,也是redis服务器,不提供数据服务。提供监控、通知、投票选择master【下图】、重新连接。
首先要对master是否下限进行确认,超过半数认为master已断则证明已经断了。
选出一个哨兵去选择新的master并且进行设置。
Redis集群
- 过程:key->哈希->第二次哈希->找到存放位置->找到value
- 集群内部通讯设计:有点像ARP机制,每一个redis服务器维护一个其他redis服务器的位置表。如果一次命中则返回数据,否则定位到所在位置的redis服务器。
数据持久化机制
因为数据都是存于内存中的,当重启系统时,缓存在内存中的数据都会消失殆尽,再也找不回来了。所以,为了能够让数据长期保存,在宕机的时候可以恢复数据,就要将redis放在缓存中的数据做持久话存储。
官方提供了不同级别的数据持久化方式:
RDB
能够在指定的时间间隔对数据进行快照存储(二进制文件.rdb)
启动方式一:save命令,即刻执行
命令 save指令,不建议在线上使用,因为redis是单线程,所以如果save指令非常长,则会造成阻塞。
那么单线程效率低问题如何解决?
启动方式二:bgsave,后台异步执行。
启动方式三:save配置,满足条件时自动执行
启动方式四:shutdown 在持久话被打开时,会保证数据保存后再关闭。
配置文件中添加:save seconds(时间限制) change(次数)
- 优点:
1.RDB二进制文件文件非常紧凑而且是单一的,存储效率高。
2.保存某个时间点的数据集,是快照方式。
3.恢复数据比AOF快很多。 - 缺点:
1.备份效率不高(只能保持每个5分钟或更久的完整保存)。
2.RDB在备份过程中需要调用fork操作进程,非常影响性能。
3.RDB格式不统一,可能会发生各种格式无法兼容的情况。
AOF
AOF命令以redis协议追加保存每次写的操作到文件末尾,AOF配置:
- appendonly 打开开关
- appendsync 配置 AOF三种策略:always每次都同步、everysec每秒同步(默认配置,建议使用)、系统控制
AOF配置同时提供重写压缩机制,重写规则:
超时的命令不重写
忽略无效命令
对同一数据的多条命令合并为一条
手动重写:bgrewriteaof命令,有点类似bgsave
自动重写:
auto-aof-rewrite-min-size size
auto-aof-rewrite-percentage percentage
- 优点:
1.易读易分析可编辑(如果出错只要删除该命令)。
2.可以使用不同的Fsync策略,默认时每秒钟备份一次。
3.存储速度比RDB快。 - 缺点:
1.由于是保存的指令(指令级),占用空间较大,AOF文件大于RDB文件。
2.恢复速度没有RDB快。
两种方式共存
redis重启时会优先载入AOF文件恢复原始数据,因为通常情况下AOF会更完整。
共同点:运用了写时拷贝技术。
RDM:redis调用fork,子进程将数据集写入临时RDM文件,完成时用新RDB文件代替旧RDB,并删除旧的RDB文件。
AOF:在执行指令和重写机制中都调用fork,子进程开始将新AOF文件的内容写入到临时文件,对于新的写命令,父进程一边将他们累积到一个内存缓存中,一边将这些改动追加到现有的AOF文件的末尾。子进程完成后发信息告诉父进程将缓存直接添加到临时文件,最后临时文件代替老文件。
总结:一般追求数据完整性和保持高效率,应该两种同时使用。如果只需要保证数据完整性,优先考虑AOF;如果对数据完整性要求低,考虑恢复为主,则用RDB方式。
Redis事务
事务操作
它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令。
如果在过程中想要取消,则用discard命令销毁事务队列即可。
- 注意:单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作。无法回滚,需要程序员自己实现回滚!!
事务中的锁
客户端1操作事务过程中,如果有客户端2在事务过程中去修改,则客户端1的事务无法被执行。
#加锁
watch *key* #监控该key是否被改变
#解锁
unwatch
事务中的分布式锁
超卖问题,与上面不同的是,该值是一直在变的,该锁监控的不是数值变不变的问题,而是应该监控是否有人在改这个问题。
#加锁
setnx *lockname* *value* #该用户拥有控制权
#解锁
del *lockname*
死锁问题(优化):可以给lockname设置一个时间
并发竞争key的问题?
用分布式锁解决,或者使用消息队列。
Redis 与数据库同步的方式(数据一致性问题)
-
数据库->redis 更新
方案一:做缓存,就要遵循缓存的语义规定:
读:读缓存redis,找不到就读mysql,并将mysql的值写入到redis。
写:更新的时候,先删除缓存,然后再更新数据库。(看到有文章说,因为高并发,所以大多数情况下,先更新数据库再删除缓存比较靠谱,但还是会造成脏数据)
优点:保证数据一致性,不至于出错。 -
redis->数据库 更新
方案一:定时刷新redis中的更新数据到mysql。
缺点:如果发生宕机,故障等都会造成数据的不一致性。
方案二:将redis变更复制一份,丢到队列中,给mysql消费。
优点:可以保证数据一致性,但是耗内存,需要维护队列。
为什么redis那么快?
- 完全基于内存的操作。
- 数据结构的特殊设计,追求快速。
- 单线程,避免上下文切换的性能消耗。
- 使用多路复用I/O,避免阻塞。
巨人的肩膀:
[1] redis简明教程:https://zhuanlan.zhihu.com/p/37055648
[2] https://www.cnblogs.com/pirlo21/articles/7120935.html
[3] 官网:https://redis.io
[4] 可以自己写一写代码:https://try.redis.io/
[5] Redis-Sorted-Set底层数据结构:https://www.jianshu.com/p/14dde3031e0b
[6]https://blog.csdn.net/zeb_perfect/article/details/54135506?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param