序言
本文主要分为两部分,第一部分是学习课程《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配合使用。
1.2.2 消息通知
场景: 消息通知,例如当文章更新时,将更新的文章推送出去,推送出去的文章,用户才能搜索到最新数据。
使用数据类型: List作为消息队列,List数据结构为QuickList,是由一个双向链表和listpack实现的。
1.2.3 计数
场景: 用户主页文章的点赞数、收藏、关注等数据的统计。
使用数据类型: hash存储数据,数据结构为dict。当每个槽位存储的数据很多时,访问数据会变慢,所以需要扩充槽位,迁移数据,但迁移的过程中不能影响用户的正常访问(避免阻塞用户请求)。
rehash: rehash是下图中将ht[0]的数据全部迁移到ht[1]的过程,当数据量小的场景下,可以快速第拷贝数据。但数据量特别大的场景下,会出现阻塞用户请求,因此出现了渐进式rehash。
渐进式rehash: 每次用户访问ht[0]中的数据时,将用户访问的数据少量迁移到ht[1]中,将整个迁移过程平摊到了所有的访问过程中。
1.2.4 排行榜
场景: 根据用户的积分变化,实时地变更用户的排名。结合dict,通过key操作跳表的功能。
使用数据类型: zset数据结构zskiplist。
1.2.5 限流
规定时间内的请求数为N,超过N时,则禁止访问。使用Redis中的incr每次加1,超过限制时禁止访问。
1.2.6 分布式锁
并发场景,要求一次只能有一个协程执行,执行完成后,其他等待的协程才能执行。使用redis的setnt实现,利用了两个特性。
- Redis是单线程执行命令。
- 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方法
- 拆分。将大key拆分为小key,例如将一个String拆分成多个String。
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。
解决热key的方法
- 设置Localcache。在访问Redis之前,在业务服务侧设置Localcache,降低访问Redis的QPS。LocalCache中缓存过期或未命中,则从Redis中将数据更新到LocalCache。Java的Guava、Golang的Bigcache就是这类LocalCache。
2. 拆分。将key:value这一个热Key复制写入多份,例如key1:value,key2:value,访问的时候访问多个key,但value是同一个,以此将qps分散到不同实例上,降低负载。代价是,更新时需要更新多个key,存在数据短暂不一致的风险。
3. 使用Redis代理的热key承载能力。字节跳动的Redis访问代理就具备热Key承载能力,本质上是结合了“热Key发现”、“LocalCache”两个功能。
1.3.2 慢查询场景
容易导致Redis慢查询的操作:
- 批量操作一次性传入过多的key/value,如mset/hmset/sadd/zadd等O(n)操作 建议单批次不要超过100,超过100之后性能下降明显。
- zset大部分命令都是O(log(n)),当大小超过5k以上时,简单的zadd/zrem也可能导致慢查询。
- 操作的单个value过大,超过10KB。也即,避免使用大Key。
- 对大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 -1
:zrange geo1 0 -1 withscores 返回的值为输入经纬度值经过geohash算法转化的结果
求距离 支持单位 m(米)、km(千米)、mi(英里)、ft(英尺)
计算a与b距离: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 频道 消息ID
:xread 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配置文件中 如下:
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后,本次事务中的所有命令才会被依次执行。
事务错误处理:
- 语法错误,全不执行。
- 运行错误,出错后仍然继续执行。
事务开始:multi
然后输入多条指令,返回Queued
执行:exec (一次执行上述指令)
multi后输入多条指令 但还未执行exec 并想撤销这些命令时 执行:discard
出错情况
执行时 中间有条命令错误时:其他正确的命令依然执行
执行前 有命令语法错误 被检测出来时:全部均不执行