REDIS 笔记
REDIS
Redis 虽然是多线程的,但是实际上还是单线程:
单线程:底层核心操作。
多线程体现在:网络协议解析,同步数据上。
因此,Redis 本身仍然是线程安全的。
数据类型
flushall: 清除所有的 key
String 字符串(用来存储单个的 key-value 键值对):
扩容机制
最大长度
get、set、mget、mset、del。 实例: 1. set aa xxx、get aa 2. mset aa xx bb xx cc xx、mget aa bb cc、del aa 备注: set 单独设置、mset 批量设置、del 删除
incr xx 自增、decr xx 自减。 实例: incr a、decr a、incrby a 10(自增步进值)、decrby a 10(自减步进值) 备注: incr 自增 1、decr 自减 1、incrby: 自增步进值、decrby: 自减步进值
setnx 存在则不创建,反之创建、set a ex 10 创建带有存在时间的 key。ex: 秒 | px: 毫秒。 实例: setnx a xxx、set a (ex 10)|(px 1000)
Hash 散列(用来存储非序列化的对象):
rehash 机制。
序列化,反序列化。
hset、hget、hmget、hgetall。 实例: hset userinfo name bb age cc base 99、hget userinfo name、hgetall userinfo 备注: hset 键 file1 xx field2 xx(当已经设置过键,再次设置时,不会覆盖,而是新增一个 field)、hget 键 field(hget 每次只能获取对象的一个键)、hgetall 键(直接获取整个对象的所有键)
hlen、hincrby、del。 实例: hlen userinfo、hincrby userinfo aa 备注: hlen xx(获取 key 的长度)、hincrby key filed inc(将指定的对象字段自增一次)
List 列表(存储数组):
链表存储、反向查找、遍历。
lpush、rpush、lpop、rpop。 实例: lpush key xx xx xx xx lpop key、rpush key xx xx、rpop aa 备注: lpush key xx xx(左侧向数组添加一个或多个数据)、lpop aa(左侧删除一个数据)、rpush、rpop(同理)
lrange startIndex endIndex。 实例: lrange aa start end(-1: 最后一个) (获取指定返回内的数据列表, 左闭右闭)
Set 集合:
并集、交集、差集。
sadd、smembers、srem index、spop random index、smember key xx。 sadd key xx xx xx(设置集合)、smembers key(返回集合的列表)、 srem key field (删除指定数据内容的集合数据)、smember key field(查看 field 是否存在 key 中)
sinter、sdiff、sunion。 sinter key1 key2 key3: 求三个 key 的交集 sdiff key1 key2 key3: 求三个 key 的差集 sunion key1 key2 key3: 求三个 key 的并集
sorted set 有序集合:
zadd、zrange、zrangebyscore、zrem、zcard、zrank、zcount、zrecvrank
补充: setnx test 1; // 互斥锁标志位,不太安全,可以通过 expire 完善,但是无法保证线程的原子性,或者将 setnx 与 expire 进行 lua 脚本的编写来保证其线程的原子性。 set aa 123 ex 10 nx; // 相当于 setnx 的的升级版,互斥锁标志位,且可以保证线程的原子性。 exists key; // 给 redis 中的某一个 key 设置超时时间。 hexists key field; 查看 hashkey 中的某一个字段是否存在。
有序集合的详细介绍
二分查找的实现:
二分查找其实是针对有序的数组,进行对半拆分的方式查找。如: [1,2,3,4,5,6] // 0 + 3 >> 2 // 得到一个索引。该索引的值对应需要查找的值,是大还是小,大了就将该索引减 1,作为 end,左边一半的起始位置作为 begin。
性能影响比较大的,如: 数据库 IO,最好放在最后处理。先执行逻辑层的操作,耗时的 IO 最好按照顺序放在最后处理。
单表的话,并且执行的 IO 操作是在程序的最后一行,可以不使用事务,反之需要使用事务。
一个微服务对应一个 redis db。前缀的层级不要太多。且前缀最好使用全大写,名称使用小写。
redis 在初始化的时候是否可以统一的加上前缀。
短信验证码采用 string 进行存储。键: pirefix + phone 值: random six code。
注册时用户时,需要在数据库中新增字段表示该用户是否因为异常情况被锁定过。
压力测试工具: Jmeter5.0、truncate table: 清除 mysql 表中的数据,而不是删除表。
lua 脚本可以保证 redis 的原子性,可以让 redis 的操作在高并发的场景下,保证单线程的方式执行。
setnx 互斥锁标志位、set key value ex sec nx // ex: 秒、px: 毫秒、nx: 当键不存在时,才会插入(最好的解决互斥标志位方案)。
自旋锁的概念。(两个线程同时加一个值的场景)。当两个线程同时操作一个资源时,发现一个资源被占用,则当前资源 while 自旋判断资源是否使用完毕,使用完毕后再次执行。
可重入锁:较为复杂。可查看视频。
is_valid 的效果: 相当于 status,对业务表中的单条数据最好加上该字段,表示当前数据的状态是什么,如: 占用、锁定、使用中等。与: createdAt、updatedAt 是一样的优先重要级。
正确的状态与错误的状态要设置为全局的 model。
redis 存的是非持久化、相对简单类型的数据,复杂类型的数据需要存储时,需要采用 redis 的 hash 列表来进行存储,持久化数据仍然是需要存储于 db 中。如:粉丝、关注场景,关注与非关注的单条数据任何是需要存储到 db 中的,但是 关注的列表,可针对具体的用户,存储到 redis 中,存储时,key 对应用户的 id,value 对应关注者的 id 列表。在如: 抢单的高并发时,使用 db 来进行逻辑的操作可能会存在问题,但是如果采用 redis 的 hash 进行复杂数据的存储,则就可以解决该问题了。场景想象为:高并发下抢购代金卷的时候。可重入锁解决场景为: 高并发模式下解决一个用户只能购买一张代金卷的情况。
Feed 流的介绍
timeline、rank
timeline:表示以发表时间为主键,进行倒序排列(最保险,每个一关注者都能看到作者发布的 feed)。
rank: 表示以每个 feed 的被点赞阅读程序进行排序。
存储,写入:
存储: mysql、nosql
mysql 存储少量的复杂性数据是没问题的,大量数据时,对文字等单一类型表数据也是没问题的。但是对文字加图片视频等数据,采用 nosql 如 monngodb 效果更佳。
写入: 推送(写扩散)、拉取(读扩散)、推拉结合。
推送(写扩散): 例如: 某一个专题发布了一条 feed,发布完成后,所有关注该专题的关注者都会在 redis 中保存一个 set 集合,该集合中存储的就是纂体发布的 feed id,这么做的好处是,方便关注者获取数据的及时性以及有效性。不好的地方时,暂用空间。当该专题拥有亿万级别的数据时,那么每发布一条 feed,数据流都是非常大的。
拉取(读扩散): 与推送相反,当专题发布一条数据时,只会在该专题中维护一个集合,保存该专题所有的 feed id,当关注者需要获取专题的 feed 时,自己主动去读取专题的 feed。好处是占用空间小。缺点是:响应速度慢。
推拉结合: 以上的推送和拉取都有自身的好处和缺陷,但是把他们融合时就变为以下这样:
当专题的粉丝数量为: 万级及以下时,采用写扩散。
当专题的粉丝数量为: 大于万级以上时,采用读扩散。
存储 feed 的时候,采用有序集合进行存储: userid、feedid、Date().now(); 存储 feed 时,采用以下方式进行存储(db 与 redis 配合存储才行):
新建一张 feed 表,外键关联 userid 当[用户|专题]发布一条 feed,则先存储于 db 中。
获取发布 feed 作者的粉丝列表,为每个粉丝|关注者都创建一条 feed pool, 采用有序列表进行存储。存储的格式如下: key(userid) value(Date.now()) field(feedid)。zadd user1(用户 id) 10(rank 值: Date.now()) 2(field 字段名称)。
用户签到功能
bitmaps 高阶数据类型。
数据类型介绍。
是一种二进制存储模型,用来存储大量数据的。存储的数据比较简单,只能存储 0 或者 1。
指令操作。
设置数据: setbit user:sign:20220120 1 1、setbit user:sign:20220120 2 1、setbit user:sign:20220120 3 1、setbit user:sign:20220120 4 1、setbit user:sign:20220120 5 1、setbit user:sign:20220120 10 1。 使用说明:setbit key(键,采用前缀分段存储) index(索引位置) value(值)
获取数据: getbit user:sign:20220120 0|1|2。 使用说明:getbit key(键) index(索引)
数据统计:bitcount user:sign:20220120 统计数据。 使用说明:bitcount key(键) // 只能统计值为 1 的一共有多少个。无法指定值进行统计。
查看具体值的首位在第几位: bitpos user:sign:20220120 0|1。 使用说明: bitpos key(键) value(值) // 查看 value 第一次出现的位置索引是多少。
查看一定区间内的值: bitfield user:sign:20220120 get [u(无符号位)|i(有符号位)]3(end) 0(begin)。 使用说明: bitfield key(键) get [u(无符号位:常用)|i(有符号位)] 3(结束区间位置) 0(起始索引位置)。 // 低版本 redis 不支持该指令。最新版本的 redis 均支持该指令操作,2016 年的 redis 版本是不支持的,官方规定的是 redis3.x 之后的才能支持。
地理位置附近的人
采用 GEO 指令进行存储,存储的值为经纬度。GEOADD、GEOGET 等,缺点是占用空间,在存储时,仍然是采用的 有序集合(sorted set )存储的,且存储的值为经纬度的和。
采用 GEOHASH 指令进行存储,存储时,不占用空间。且存储的值是通过特殊处理的,占用空间小,可通过存储值得前几位字符串判断是否在附近的门店。
缓存策略
redis 的使用是配合 mysql 的,在实际的业务中,先走缓存,在走数据库。当然,也并不是所有的业务都是这么走的。有些接口是这么走的。常见于 hash 模式下存储。
缓存的异常的三大场景:缓存击穿、缓存穿透、缓存雪崩。
缓存击穿:常见于高并发的 API,如抢购商品,当商品在 redis 中的失效时间过期了时,那么在过期的一瞬间,甚至短时间内,还会有大量的接口涌入,此时,redis 中也已经不存在该商品了,因此就会出现缓存击穿的场景。(当 redis 中不存在数据,那么应该走 db 数据库,保证 API 的可用性。) 解决办法: key 值得永不过期 (redis 不是垃圾桶,不推荐)。 逻辑接口判断 (通过逻辑接口判断该商品是否已经不存在)。服务降级 将商品的过期时间设置为大于活动时间,如:1 天,当数据过期时,采用逻辑判断是否过期。
缓存穿透: 当 redis 的 key 在过期时,走到数据库的查询,数据库内部也是空的,那么此时就是缓存穿透。API 的接口此时是不可达的。这种也是一个漏洞,当利用该漏洞进行大量的数据请求将会直接导致数据库挂掉。 解决办法: 布隆过滤器、服务降级、当 redis 的数据存在,查询数据库数据存在值时,则更新 redis 数据,若查询数据库不存在值时,则将 redis 的数据设置为空(必做)。
缓存雪崩: 当大量的 key 的过期时间一致,且在高并发 API 的情况下,一大批的 key 突然过期,那么此时的接口将全部走到数据库层面,此时的情况就叫做缓存雪崩。 解决办法: 将 key 值的过期时间不设置为全部一样。服务降级()
备注: 以上就是缓存策略的三种异常情况,总结下来就是,高并发的接口一定是要优先缓存,在走 db,当然也并不是每一个接口都要这么做。 redis 不是垃圾桶,设置的 key,大多数情况下都需要设置过期时间。缓存的数据为空时,查询的 db 数据若为空,或者存在值,那么一定要更新缓存。哪怕缓存存的值为一个空值,若存的值为 null 值时,那么将该值的时间设置为 60S,过期后请求再次进来,会走 db 刷新缓存。不考虑 key 的失效时间以及原子性的情况下,redis 即使在高并发的接口模式下,也不会出现问题。
大数据的存储
redis 时采用 tcp 网络应答的方式进行存储和设置值的,当插入大量的数据时,每插入一个值,都必须要等到该值插入完成之后,反馈一个结果,才能继续插入下一个值。
结合 1 的表述,当大量的数据需要插入时,采用逐行插入,效率会比较低下。
采用 pipeline 插入,使用该种方式插入大量的数据,效率是会比较高的。(pipeline 去掉了逐行插入中的,插入一条数据等待下一条数据返回后才能继续插入新的数据这种方式,可直接一次连接,多次插入,不再需要插入后的等待。)
Memory useage xxx 查看 key 所占用的 b 有多少位。 /8 表示有多少个字节。
数据持久化:
RDB:生成配置后的 rdb 日志文件。rdb 采用的是定时生成日志文件,在 xx.conf 中配置,
优点是开销不大,生成数据并不是实时的,本地磁盘的 IO 操作性较小。生成的日志文件采用二进制存储,空间占用较小,保证数据的持久性。
缺点是存在数据空窗期,因为 rdb 采用的是定时生成日志文件。日志采用的是的二进制存储,因此日志不好阅读。默认在生产环境的配置:15 分钟记录一次。
RDB 也可以手动记录。分为以下五种:
修改配置文件。 save 5 1: 5S 内当存在 key 的改动,则生成快照(文件名称为: .rdb 文件)。当服务挂掉后,再次开启链接 redis,则 db 中的数据会重新生成。
手动保存。执行: BGSAVE(bgsave)。非阻塞保存,开启子进程进行保存。
手动保存。执行: SAVE(save)。阻塞式保存。保存时,会阻塞 get 或者 set 等数据操作。
shutdown,当客户端执行 shutdown 指令时,redis 会自动 阻塞式 save 执行数据持久化的保存。
主从节点复制。
开启 aof 之后,当 redis 服务因为异常情况挂掉时,再次重启 redis 服务时,redis 会读取 rdb 的日志文件,并重新将数据注入到主节点中去。
bind 0.0.0.0 // 访问ip限制
daemonize yes // 后台启动
logfile c://xxxx // 日志路径以及日志的文件名
dir c://xxx // rdb 以及 aof 数据文件的存储目录
dbfilename dump.rdb // rdb 数据文件名
save 5 1 // rdb 存储模式设置:5秒内有key值的改动则存储一次快照
appendonly yes // aof 数据持久化开启
appendfilename // aof 数据文件名称
appendfsync everysec(每秒重写-常用)|always(每次发生改变重写-生产模式不可用)|no(不主动同步,由操作系统自动刷新磁盘) // AOF备份日志文件机制。
no-appendfsync-on-rewrite no // 保持该值即可,不需要设置
auto-aof-rewrite-min-size 64mb // 当aof的大小大于多少时,进行重写
auto-aof-rewrite-percentage 100 // 百分比模式,设置值为当日志文件大于上次文件的百分之多少开始重写。
aof-load-truncated yes // 当启动连接时,若aof文件损坏,则不让启动成功。
requirepass 123456 // 设置连接密码
AOF: AOF 生成配置后的 .aof 日志文件。可于 rdb 同时进行使用。
aof 分为三种 trigger 存储方式。everysec 每秒备份、always 对每次的操作(set、get 等)进行读写、no: 由操作系统进行磁盘的刷新。不论是 aof 还是 rdb,在日志记录时都会于磁盘 io 进行数据处理,且每次都会刷重新磁盘。
优点: 数据的保存及时性、日志容易读取(日志采用非二进制存储)。
缺点:性能消耗较大。暂用内存较高。
aof 重写机制,由于 aof 的保存时效性较高,且保存的日志内容采用的是非二进制的方式存储的,因此,aof 的日志会大很多。此时就会涉及到 aof 的重写机制,重写机制是指,可以手动、或者自动(配置)重写,当 aof 的日志体积达到一定的增量时,将会采用新的日志文件进行记录,并且对之前的日志文件会做一层过滤的优化。如:连续 set 了同一个 key 值,本身直接保存最后一次的 set 就可以了,但是在没重写 aof 之前,在日志中是会连续记录多次的。因此当开启了 aof 时,重写一是有必要的。
AOF 与 RDB 的混合使用。
AOF 与 RDB 各有千秋,因此集成与上述的两种模式优点进行存储自然是最好的。
DBSIZE: 查看 db 内的 size 长度数据。
info memory: 查看内存的占用情况。