Redis学习笔记 | 青训营

137 阅读23分钟

序言
本文主要分为两部分,第一部分是学习课程《Redis-大厂程序员是怎么用的》记录的笔记,第二部分是本人自学Redis记录的笔记内容。通过学习,熟悉了解了Redis的相关基本概念、基本数据类型、相关配置等内容,通过实践课程的讲解,进一步认识了Redis的工作机制和实现原理。

1. 大厂程序员使用Redis

1.1 Redis是什么

redis 是一个key-value存储系统,主要作用是缓存,解决了数据库无法承受大量请求访问的问题。

1.2 Redis应用案例

1.2.1 连续签到

场景: 每日签到则数据加1,如果断签,计数归0。针对该场景,可以使用Redis来实现功能,通过设置key的有效时间,用户需要每天0点-23点59分59之间签到,否则失效,计数归0。
使用的数据类型: String数据类型,数据结构为sds,可以存储字符串、数字、二进制数据,通常与expire配合使用。 image.png

1.2.2 消息通知

场景: 消息通知,例如当文章更新时,将更新的文章推送出去,推送出去的文章,用户才能搜索到最新数据。
使用数据类型: List作为消息队列,List数据结构为QuickList,是由一个双向链表和listpack实现的。 image.png

1.2.3 计数

场景: 用户主页文章的点赞数、收藏、关注等数据的统计。
使用数据类型: hash存储数据,数据结构为dict。当每个槽位存储的数据很多时,访问数据会变慢,所以需要扩充槽位,迁移数据,但迁移的过程中不能影响用户的正常访问(避免阻塞用户请求)。
rehash: rehash是下图中将ht[0]的数据全部迁移到ht[1]的过程,当数据量小的场景下,可以快速第拷贝数据。但数据量特别大的场景下,会出现阻塞用户请求,因此出现了渐进式rehash。
渐进式rehash: 每次用户访问ht[0]中的数据时,将用户访问的数据少量迁移到ht[1]中,将整个迁移过程平摊到了所有的访问过程中。 image.png

1.2.4 排行榜

场景: 根据用户的积分变化,实时地变更用户的排名。结合dict,通过key操作跳表的功能。
使用数据类型: zset数据结构zskiplist。 image.png

1.2.5 限流

规定时间内的请求数为N,超过N时,则禁止访问。使用Redis中的incr每次加1,超过限制时禁止访问。

1.2.6 分布式锁

并发场景,要求一次只能有一个协程执行,执行完成后,其他等待的协程才能执行。使用redis的setnt实现,利用了两个特性。

  1. Redis是单线程执行命令。
  2. setnx只有未设置过才能执行成功。

1.3 Redis使用注意事项

1.3.1 大key、热key

大key的定义:
  • String类型:value的字节数大于10KB即为大key。
  • Hash/Set/Zset/list等类型:元素个数大于5000个或总value字节数大于10MB即为大key。
大key的危害

大key危害:读取成本高;容易导致慢查询(过期、删除);主从复制异常(太大了,备份耗时太长),服务阻塞,无法正常响应请求。

消除大key方法
  1. 拆分。将大key拆分为小key,例如将一个String拆分成多个String。

image.png 2. 压缩。将value压缩后写入redis,读取时解压后再使用。压缩算法有gzip、snappy、lz4等。通常情况下,压缩算法压缩率高、解压耗时就长,要选择合适的算法。(考虑到Redis主要用于读取数据,选择合适算法) 3. 集合类结构hash、list、set:(1)拆分:可以用hash取余、位掩码的方式决定放在哪个key中。(2)区分冷热:如榜单列表场景使用zset,只缓存前10页数据,后续的数据查询数据库。

热key的定义

用户访问一个Key的QPS特别高,导致Server实例出现CPU负载突增或不均的情况。热key没有明确标准,QPS超过500就可能被识别为热key。 image.png

解决热key的方法
  1. 设置Localcache。在访问Redis之前,在业务服务侧设置Localcache,降低访问Redis的QPS。LocalCache中缓存过期或未命中,则从Redis中将数据更新到LocalCache。Java的Guava、Golang的Bigcache就是这类LocalCache。

image.png 2. 拆分。将key:value这一个热Key复制写入多份,例如key1:value,key2:value,访问的时候访问多个key,但value是同一个,以此将qps分散到不同实例上,降低负载。代价是,更新时需要更新多个key,存在数据短暂不一致的风险。 image.png 3. 使用Redis代理的热key承载能力。字节跳动的Redis访问代理就具备热Key承载能力,本质上是结合了“热Key发现”、“LocalCache”两个功能。 image.png

1.3.2 慢查询场景

容易导致Redis慢查询的操作:

  1. 批量操作一次性传入过多的key/value,如mset/hmset/sadd/zadd等O(n)操作 建议单批次不要超过100,超过100之后性能下降明显。
  2. zset大部分命令都是O(log(n)),当大小超过5k以上时,简单的zadd/zrem也可能导致慢查询。
  3. 操作的单个value过大,超过10KB。也即,避免使用大Key。
  4. 对大key的delete/expire操作也可能导致慢查询,Redis4.0之前不支持异步删除unlink,大key删除会阻塞Redis。

1.3.3 缓存穿透、缓存雪崩

缓存穿透: 热点数据查询绕过缓存,直接查询数据库。
缓存雪崩: 大量缓存同时过期。
缓存穿透危害:
(1)查询一个一定不存在的数据。通常不会缓存不存在的数据,这类查询请求都会直接打到数据库,如果有系统bug或人为攻击,那么容易导致db响应慢甚至宕机。
(2)缓存过期。在高并发场景下,一个热key如果过期,会有大量请求同时击穿至db,容易影响db性能和稳定。同一时间有大量key集中过期时,也会导致大量请求落到数据库上,导致查询变慢,甚至出现数据库无法响应新的查询。
减少缓存穿透:
(1)缓存空值。如一个不存在的userID。这个id在缓存和数据库中都不存在。则可以缓存一个空值,下次再查缓存直接反空值。
(2)布隆过滤器。通过bloom filter算法来存储合法Key,得益于该算法超高的压缩率,只需占用极小的空间就能存储大量key值。
避免缓存雪崩:
(1)缓存空值。将缓存失效时间分散开,比如在原有的失效时间基础上增加一个随机值,例如不同Key过期时间, 可以设置为10分1秒过期,10分23秒过期,10分8秒过期。单位秒部分就是随机时间,这样过期时间就分散了。 对于热点数据,过期时间尽量设置得长一些,冷门的数据可以相对设置过期时间短一些。
(2)使用缓存集群,避免单机宕机造成的缓存雪崩。

2. 我学习Redis

2.1 Redis数据类型

2.1.1 String类型

一个key最大能存储512MB,redis中的string可以包含任何数据。

原理:动态字符串,内部结构类似ArrayList。采用预分配冗余空间的方式减少内存的频繁分配,内部为字符串分配的空间一般都高于实际长度,当字符串长度<1MB时,扩容方式是直接加倍,如果>1MB,一次扩容只扩1MB,直到扩大到512MB。

设置key及值:set key1 "malong"
查看长度:strlen key1
增加字符串内容:append key1 "student" (返回数字--表字符长度)
获取值:get key1 (返回"malongstudent" )
当value值为正数时:set key2 1; 类型仍为字符串类型
自增:incr key2;
自减:decr key2;
增加3: incrby key2 3;
减少3:decrby key2 3;
字符串截取:getrange key1 0 4;( 从0开始到4下表,返回"malon" );下标范围:0 到 -1
范围内更改值:setrange key1 6 teacher; (key1--malongstudent)
取值:get key1; 返回值 malongteacher, 从下标6开始替换为teacher;

2.1.2 List类型

单key多value的存储容器,存储数据时分左右存储,从左或右(内部类比队列)

内部类似链表结构,如果键不存在,创建新的链表;如果键已经存在,新增内容;如果值全部移除,对应的键也消失。

原理:底层是一个快速链表的结构,列表元素较少时,使用连续的内存存储压缩列表;当数据量较多时,改成快速链表,也就是使用双向指针串联起来使用,减少内存的碎片化。

存取操作(push、pop、range)

lpush:从左侧存入数据——》栈结构   
rpush:从右侧存储数据——》队列结构   
lrange:范围内查看数据 使用方式:lrange key start end  

存储数据:lpush list1 1 2 3 4 5;   
取数据(取得范围):lrange list1 0 -1;   
取得值:(1)"5",(2)"4",(3)"3",(4)"2",(5)"1"   
取值(弹出了):lpop list1; 返回值:"5"   

存储数据:rpush list2 1 2 3 4 5;   
取数据(取得范围):lrange list2 0 -1;   
取得值:(1)"1",(2)"2",(3)"3",(4)"4",(5)"5" 
取值(弹出了):lpop list2; 返回值:"1" 
右侧弹出:rpop list2; 返回值:"5"
查看list长度:llen list1 
获取某个位置值:lindex list1 index; (lindex list1 2) 
删除list1中n个value的值:lrem list1 n value (lrem list1 1 4, 删除了list1中1个值为4的数据) 
截取操作:ltrim list1 0 2; (list1[5,4,3,2,1]) 
截取后:list1[5,4,3] 
插入元素:linsert listx after 1 0; 在listx中的值为1后插入值为0; 
原始值listx[6,5,4,3,2,1] 
插入后:lrange listx 0 -1; listx[6,5,4,3,2,1,0] 
6之前插入7:linsert listx befare 6 7; 
插入后:listx[7,6,5,4,3,2,1] 
也可以插入中间/任何位置,某个数据的前后即可。 
插入列表头部简单方式:lpushx listx value; 
插入列表尾部简单方式:rpushx listx value; 
更改某个位置数据值:lset listx 4 7; 更改列表listx中第4个位置的值为7;

2.1.3 Hash类型

每个hash可以存储2^32-1个键值对

原理:底层实现结构与HashMap一样,是“数组+链表”的结构,第一维hash的数据位置碰撞时,将碰撞元素用链表串接起来,不同的是,redis字典的值只能是字符串,而且二者rehash方式不同。Java的hashmap是一次全部rehash,耗时较高,redis为了优化性能,采用渐进式rehash策略。具体:同时保留新旧两个hash结构,然后逐步搬迁,搬迁完成后再取而代之。

存数据:hset hash_name key_name value (hset hash1 key1 "malong") 
取数据:hget hash_name key_name (取值先确定某个hash,再通过key取值) 
查看hash1中所有的key值:hkeys hash1 
查看hash1所有的value值:hvals hash1 
批量存数据:hmset hash1 key3 value3 key4 value4 
批量取数据:hmget hash1 key3 key4 
删除数据:hdel hash1 key1 
查看长度:hlen hash1 
一次性获取hash1中所有key和value: hgetall hash1 
判断某个key是否存在:hexists hash1 key4 (存在返回1,否则0) 
判断是否存在,存在则执行失败,不存在则执行成功:hsetnx hash1 key4 "value4" 
hincrby对整数的增加操作:hincrby hash1 key1 1

2.1.4 Set类型

存储无序不重复的元素集。

原理:类似HashSet,通过哈希表实现,相当于所有的value都是空的;通过计算hash的方式快速排重。

set1中存数据:sadd set1 1 2 3 4 3 2 1 
实际存了 1 2 3 4 (重复的去了) 
查看set1大小:scard set1 
查看所有数据:smembers set1 
移除数据1:srem set1 1 (返回的是移除的数据个数) 
移除数据2和3:srem set1 2 3 
判断是否包含value数值:sismember set1 value (在返回1,否则0) 
随机取n个数值:srandmember set1 n 
随机删除n个数值:spop set1 n 
将set1中的数值3移动到set2: smove set1 set2 3 
支持交集、并集、差集 
两个集合交集:sinter set1 set2 
两个集合差集:sdiff set1 set2 (存在set1 但不在set2的元素) 
两个集合并集:sunion set1 set2

2.1.5 有序集合Zset类型

redis的有序集合和set一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数可以重复。

Set:HashMap>

Zset:HashMap>

创建zset增加元素(增加4个元素) 每个元素值都包含<分数,value> 
:zadd zset1 60 xiaoma 70 musa 80 xiaozhang 
查看元素 根据范围查看 
正序查看 :zrange zset1 0 -1 
逆序查看:zrevrange zset1 0 -1 
查看分数:zrange zset1 0 -1 withscores 
对score范围查询 
查询分数在【60,100】的人 :zrangebyscore zset1 80 100 
查询分数在(60,100)的人:zrangebyscore zset1 (60 (100 
逆序:zrevrangebyscore 
查看范围并增加偏移量 类似分页查询 
查看60-100中从m开始n个人:zrangebyscore zset1 60 100 limit m n 
删除musa值:zrem zset1 musa 
统计元素个数:zcard zset1 
统计指定分数内的个数:zcount zset1 70 100 
查询某个人分数:zscore zset1 musa 
查询某个人的索引值:zrank zset1 musa 
逆序查询某个人索引值:zrevrank zset1 musa

2.1.6 GEO

GEO 实际内部结构是zset,它可以将用户给定的地理位置信息存储起来,将二维的经纬度数据映射到一维的整数,便于计算两点之间的距离。

原理:映射算法,将地球看成一个二维平面,划分成一系列正方形的方格,所有地图坐标都被放置于唯一的方格中,然后进行整数编码,编码越接近的方格距离越近。

添加经纬度数据(a):geoadd key 经度 纬度 名字 (支持存储多个 ) 
:geoadd geo1 86.08 44.31 a 
: geoadd geo1 116.40 39.90 b 
查看成员经纬度数据:geopos geo1 a 
查看数据元素 
:zrange geo1 0 -1zrange geo1 0 -1 withscores 返回的值为输入经纬度值经过geohash算法转化的结果 
求距离 支持单位 m(米)、km(千米)、mi(英里)、ft(英尺) 
计算ab距离:geodist geo1 a b km 
进行哈希编码 得到编码结果:geohash geo1 a 
=====使用较多场景 查找附加距离目标====== 
计算指定位置附加具体距离内的目标 多增加几个元素数据 
在集合geo1中以经度 126.53 纬度45.79 为中心 半径为500km 返回集合内内满足的地点 
:georadius geo1 126.53 45.79 500 km 
同时返回具体的距离:georadius geo1 126.53 45.79 500 km withdist 
同时返回经纬度:georadius geo1 126.53 45.79 500 km withcoord 
同时返回哈希编码:georadius geo1 126.53 45.79 500 km withhash

2.1.7 bitmap(位图)

BitMap就是一个byte数组,元素中每个bit用来表示元素在当前节点对应的状态,实际上底层也是通过对字符串的操作来实现,对应开发中boolean类型数据的存取---签到场景(0/1)

原理:位数组是自动扩展的,可以直接得到字符串的ASCII,是为整存零取,也可以零存整取,如果对应字节是不可打印字符,会显示该字符的十六进制。

比起字符串,节省空间。

读取操作 
设置某个位置的值为0/1 :setbit key index 0/1 
以ASCII表中m的二进制值为例(0110 1101) 
: setbit m 1 1; 
: setbit m 2 1; 
:setbit m 4 1; 
: setbit m 5 1; 
: setbit m 7 1; 
获取某个位置的值:getbit key index; 
:getbit m 1 
获取整个key的值:get key (零存整取 直接过的二进制字符数组对应的字符串) 
: get m 也可整存零取 
统计m中几个1:bitcount m 
设置字符串:set key1 hello 
统计字符串中含有几个1:bitcount key1; 对hello中每个字符的二进制中计算1 
支持参数 字符的起始位置m到终止位置n:bitcount key1 m n;

2.1.8 HyperLogLog(基数统计)

Redis的基数统计,非常节省内存的统计方式,它是一个基于基数估算的算法,但并不绝对准确,标准误差是0.81%。

原理:最大占用12KB的存储空间。当统计数比较小是,使用稀疏矩阵存储,占用空间很小;在变大到超过阈值时,转变成稠密矩阵,占用12kB。

算法:给定一系列的随机整数,记录低位连续0位的最大长度k,通过k可以估算出随机数的整数量N。

UV与PV统计 
UV(unique viewer):统计用户的访问量;用户访问,某个用户某天访问一个网站 无论访问几次 一个用户只统计1次 
PV(page viewer):页面的访问量(点击次数) 单个用户访问几次就统计几次。
增加数据 pfadd 
统计数据 pfcount (去重) 
增加用户user1:pfadd uv user1 
查看用户user1:pfcount user1 
统计uv:pfcount uv

2.1.9 Stream

预习:消息的发布与订阅 发布pub订阅sub
创建两个客户端 实现发布与订阅
1)首先订阅频道a:subscribe a
2)频道发布消息:publish a "welcom here"
消息一旦发布 订阅者便可以收到发布的消息。

pub与sub存在的问题:重启或网络中断后查看不了发布的历史消息,而stream可以持久化,解决了该问题。

Stream允许消费者等生产者发送新的数据,引入了消费者组的概念,组之间数据是相同的(前提设置的偏移量一样),组内的消费者不会拿到相同的数据。

原理:与redis的pub/sub不同,pub/sub多个客户端是收到相同的数据,而stream的多个客户端是竞争关系,每个客户端收到的数据是不同的。

生成消息命令 
添加消息:xadd 频道 消息ID msg 消息内容 
频道a中添加消息:xadd a * msg welcom here 
返回的是消息ID(时间戳+顺序) 顺序指的是该毫秒下产生的第几条消息 
* 表示随机生成一个ID 
查看a频道有多少消息:xlen a 
查看所有消息:xrange a - + 
删除某条消息:xdel a 消息ID 
订阅消息命令 
读取消息:xread streams 频道 消息IDxread streams a 0-0 (不知道具体id时 0-0 输出shzu中所有消息)
:xread count 1 streams a 0-0 只读取1条消息 
: xread block 0 streams a $ 阻塞等待消息的返回 代表从尾部接收

2.2 Redis配置

配置文件redis.config

2.2.1 网络相关

1)bind 127.0.0.1 绑定IP地址(只能访问服务端的地址)当前的Redis服务只能被绑定的IP访问

2)protect-mode yes 开启保护模式

当bind没有配置且登录不需要密码时,启动保护模式(只能被本地访问)

3)port 6379 端口配置

4)timeout 0 客户端超时时间 0表示一直保持

5)tcp-keepalive 300 单位是秒 每300秒去检查一次客户端是否健康,避免服务端阻塞

6)tcp-backlog 511 队列数量(未完成握手和已完成握手的)

2.2.2 通用相关

7)daemonize no 后台运行开关 (也称为守护进程)

改为yes 后 重启redise验证

redis启动 指定配置文件的方式

: redis-server /root/myredis/redis.conf

8)pidfile /var/run/redis_6379.pid 当守护进程开启时,写入进程id的文件地址

9)loglevel notice 四种级别 , notice对应生产环境

10)logfile "" 日志存储位置

11)databases 16 初始化数据库数

2.2.3 缓存有效期和过期策略

通常其他缓存问题

有效期-TTL(time to live)

作用:节省空间和数据弱一致性,有效期失效后保证数据的一致性。

过期/淘汰策略:当内存使用达到最大值时,需要使用某种算法来决定清理掉哪些数据,以保证新数据的存入。

FIFO:First in First out,先进先出,判断被存储的时间,距离当前最远的数据优先被淘汰。

LRU:least recently used ,最近最少使用的。记录的是最近一次使用的时间,判断被记录的时间,距离当前最远的数据优先被淘汰。

LFU:least frequently used,最不经常使用的,在一段时间内记录被使用的次数,使用次数最少的优先被淘汰。

Redis缓存策略(maxmemory-policy)

缓存淘汰策略 maxmemory-policy 
allkeys-所有键值数据 volatile-只删除设置了过期时间键值 
noeviction:默认值 不删除策略 达到最大内存限制时 如果需要更多内存 直接返回错误信息。 allkeys-lru:所有key通用 优先删除最近最少使用的key; 
volatile-lru:优先删除最近最少使用的数据(局限于设置了有过期时间的key) 
allkeys-random:所有key通用,随即删除一部分key; 
volatile-random:随即删除一部分key(局限于设置了有过期时间的key); 
volatile-ttl: 优先删除剩余时间(ttl)短的key (局限于有过期时间的key);

由于LRU算法需要消耗大量内存,所以采用了近似LRU算法,并且是懒处理。

算法原理:采用随机采样法淘汰元素,首先给每个key增加一个额外24bit的字段,记录最后被访问的时间戳。当内存超过maxmemory时,随机采样出5个key(通过maxmemory-samples设置),采样范围取决于是allkeys还是volatile,淘汰掉最旧的key,如果仍然超出,继续采样淘汰。

算法分析:采样范围越大,越接近LRU,Redis-3.0中增加了淘汰池,进一步提升了效果。淘汰池是一个大小为maxmemory-samples的数组,每次一淘汰循环中,新随机出的key会和淘汰池中的所有key列表融合,淘汰掉最旧的key,剩余的key依旧放在淘汰池中等待下一次循环。

2.3 Redis持久化

持久化,将数据(如内存中的对象)保存到可永久保存的存储设备中。

方式一RDB:在指定的时间间隔内对数据进行快照存储,先将数据集写入临时文件,写入成功后,再替换之前的文件,采样二进制压缩存储,是一次的全量备份。

方式二AOF:以日志文本的形式记录服务器所处理的每一个数据更改指令,然后通过重放来恢复数据,是连续的增量备份。

2.3.1 RDB(Redis DataBase)

可通过命令触发(手动)|| 自动触发

RDB在redis.conf中的配置 
1)save time n 在time秒内写入n条 即触发快照 
2)dbfilename dump.rdb 默认保存文件 
3)dir ./ 保存路径 
4)stop-writes-on-bgsave-error yes 如果持久化出错 主进程是否停止写入 
5)rdbcompression yes 是否压缩 
6)rdbchecksum yes 导入时是否检查

命令触发

1)save 会阻塞当前Redis服务器,直到持久化完成,线上应该禁止使用。
2)bgsave 该触发方式会fork一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候。

自动触发

1)根据我们的save m n 配置规则自动触发;save m n 表示在m秒内数据更改n次就触发save命令。 在redis.conf配置文件中 如下: image.png 2)从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会触发bgsave; 3)执行debug reload 时(先将数据存储rdb文件 清除缓存 再从文件加载数据) 4)执行shutdown时,如果没有开启aof,也会触发。
恢复方式:将备份文件(dump.rdb)移动到安装目录并启动服务即可。

2.3.2 AOF(Append Only File)

会记录存储执行的命令,根据记录命令可以推出执行的结果 aof对命令重写进行压缩,例:当一个整数自增命令执行n次时,对记录的n条数据命令使用增加n的命令 进行压缩,使得记录的命令数据变少,但结果一样。

日志的形式记录服务器所处理的每一个更改操作,但操作过多,aof文件过大时,加载文件恢复数据会非常慢,为解决这个问题,Redis支持aof文件重写,通过重写aof,可以生成一个回复当前数据的最少命令集。

整个流程分为两步:1)命令的实时写入;2)对aof文件的重写

命令行写入流程:命令写入-->追加到aof缓存-->同步到aof磁盘(考虑到IO性能增加了缓冲)

2.4 Redis管道与事务

2.4.1 管道

管道技术是客户端提供的,与服务器无关。服务器始终使用收到-执行-回复的顺序处理消息。而客户端通过对管道的指令列表该表读写顺序,节省大幅IO时间,指令越多,效果越好。

管道可以将多个命令打包,一次性的发送给服务器端处理。

管道测试:redis-benchmark (-P)

2.4.2 事务

一个成熟的数据库,一定要支持事务,以保障多个操作的原子性。同时,事务还能保证一个事务中的命令依次执行而不会被其他命令插入。

所有事务的基本用法,都是begin、commit、rollback。

redis事务的指令是,multi、exec、discard,虽然可以使用DISCARD取消事务,但是不支持回滚。

当输入MULTI命令后,服务器返回OK表示事务开始成功,然后依次输入需要在本次事务中执行的所有命令,每次输入一个命令服务器并不会马上执行,而是返回“QUEUED”,这表示命令已经被服务器接收暂时保存起来了,最后输入EXEC后,本次事务中的所有命令才会被依次执行。

事务错误处理:

  1. 语法错误,全不执行。
  2. 运行错误,出错后仍然继续执行。
事务开始:multi 
然后输入多条指令,返回Queued 
执行:exec (一次执行上述指令) 
multi后输入多条指令 但还未执行exec 并想撤销这些命令时 执行:discard 
出错情况 
执行时 中间有条命令错误时:其他正确的命令依然执行 
执行前 有命令语法错误 被检测出来时:全部均不执行