学习笔记:一文看懂Redis

本文详细介绍了Redis中的数据结构,包括string、list、hash、set和sorted sets,以及缓存穿透、缓存雪崩、热点击穿的解决方案。探讨了Redis的定期删除和惰性删除策略,以及淘汰策略。此外,还讨论了Redis的主从复制模式、哨兵模式和集群,以及数据持久化机制如RDB和AOF。同时,涵盖了Redis事务、分布式锁和与数据库同步的方式。文章最后解释了Redis速度快的原因。
摘要由CSDN通过智能技术生成

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>
  • 关于复制缓冲区:
  1. 复制缓冲区大小设置要合理,建议master内存占有50%-70%,剩下的部分用于创建复制缓冲区。
  2. 这是个先进先出的队列结构,来源于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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值