Redis学习笔记

参考《Redis深度历险 核心原理与应用实践》

Redis

Remote Dictionary Service

Redis可以用来做什么(大部分都是对首页,或元素常用信息缓存)

短信登录验证

记录帖子的点赞数,评论数等(hash)

记录帖子标题,摘要,作者,封面信息,便于列表显示(hash)

记录点赞id列表(点赞数当作score可以进行排名),评论id列表方便去重计数(zset)

缓存热帖,减小数据库压力(hash)

6379(MERZ愚蠢)

image-20210425140755786

通过docker连接操作redis

docker exec -it redis(redis容器名) redis-cli (进入redis客服端)

shutdown (关闭redis) exit(退出redis客服端)

string(字符串)

当字符串小于1M时扩容直接翻倍,若大于1M每次增加1M,最大256M

set ket value(设置单个) get key(获取单个) exists key(判断是否存在) del key(删除指定键)

mset key1 value1 …(设置多组) mget key1 …(同时获取多组)

setex key 过期时间 value(设置有过期时间的值) setnx key value(如果键不存在才创建)

若存储值为数字 incr key(自增) incrby key offset(加等于offset) (注意自增最大为Long.MAX,否则会直接报错,底层对64位数字使用int编码存储为long)

append key value(附加指定数据) strlen key(判断字符串的长度)

list(列表)

双向指针链表,插入删除非常快,索引比较满

有左右之分的(l,r表示操作方向,有对应的反向操作)

rpush key value …(右添加元素) lpop key(弹出左边元素)

没有左右之分的(l纯粹表示list)

llen key(查看列表长度) lindex key index(查看指定下标的数据,效率低) lrange key start stop(列出范围数据,索引可以是负数,类似python) ltrim key start stop(只保留范围内的,同样可以使用负数) lrem key index(按下标删除)

hash(字典)

值只能是字符串,采样渐进式rehash策略,不阻塞服务,会保留就hash,慢慢迁移到新的hash位置,最终使用新的hash,回收旧hash

hset name key value(设置对象对应属性值) hget name key(获取对象对应属性值) hgetall name(获取对象所有键值对)

hlen name(查看属性数量) hmset name key value …(同时设置多组值) hmget name key …(同时获取多组值)

hincrby name key offset(支持数字属性自增,注意没有自增1的指令)

set(集合)

sadd key value value…(可以同时向集合添加多个值) smembers key(查看对应所有值) sismember key value(查看值是否在集合) scard key(查看指定集合元素个数) spop key(随机弹出一个元素)

zset(有序列表)

可以为每个value设置一个score设置排序权重,使用跳跃列表+hash实现

zadd name score value(添加值并设置权重) zrange name start stop(查看指定范围的内容排序输出,范围都可以使用负数)

zrevrange name start stop(与zrange类似,不过是逆序的,默认降序,这个升序排列) zcard name(查看指定键的值的数量) zscore name value(查看其分数) zrank name value(查看指定值的排名) zrangebyscore name min max(查看指定分数范围的值)

zrem name value(移除指定元素)

规律:hash,list 长度使用xlen set,zset长度使用xcard

容器型数据结构通用规则

对于list,set,hash,zset等集合容器,没有操作直接创建,容器为空自动销毁

过期时间

所有对象都可以设置过期时间,以对象为单位,若设置完过期时间,再次使用对于的set会覆盖过期时间(只是set部分键值也会覆盖)

ttl key(查看对象的过期时间,不过期返回-1)

Jedis(Java端工具)使用

创建资源池

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(RedisInfo.MAX_TOTAL);//设置最大连接数
config.setMaxIdle(RedisInfo.MAX_IDEL);//设置最大空闲连接数
config.setTestOnBorrow(true);//连接时查看有效性  默认不查看
config.setTestOnReturn(true);//空闲时查看有效性  默认不查看
jedisPool=new JedisPool(config,RedisInfo.URL,RedisInfo.PORT);//设置连接信息
Jedis jedis = jedisPool.getResource();//从连接池获取资源

关闭资源池

jedis.close();//关闭单个
jedisPool.close();//关闭资源池

分布式锁

set key value ex time nx 设置指定键值(ex time)在指定时间内过期(防止长时间不释放锁),nx没有才设置(分布锁的功能,设置失败相当于已经有人获取锁)

del key(使用完毕删除,相当于释放锁)

无法解决超时问题(需要自动续期解决)

set的值是一个随机数,释放时判断是否一致,一致再删除,防止其他线程错误释放锁

注意匹配与删除不是原子操作,可以使用Lua脚本(Lua使用协同式多线程,直到任务执行完才切换CPU)

获取分布式锁失败

1,直接抛出异常,通知用户稍后再试

2,sleep一会,稍后再试

3,将请求移到延时队列,过一会再试

可以使用list当作简单消息队列

lpush入队列,rpop出队列

队列为空:

1,睡眠读,读取为空,睡眠一会儿在读

2,阻塞读,使用blpop,brpop进行阻塞获取 blpop name time(必须指定超时时间)(注意若阻塞时间太长,会使连接变为空闲连接被关闭)

使用zset创建延时队列

内容是value,时间是score,轮询zset获取到期的任务进行执行(可以使用多线程提高可用性,不过要考虑并发问题确保任务不会执行多次)

位图

使用比特位表示布尔值(类似BitSet)

setbit name offset 0/1(设置对应位的值) getbit name offset(获取对应位的值)

(实际操作的是字符串可用直接使用 get name获取,当然也可也使用 set name批量设置)

bitcount key 【start】 【end】 (查看指定位图的1的个数,也可指定范围)

bitpos key 0/1 【start】 【end】 (查看第一个0/1的位置可以指定搜索范围)

清除查看数据库大小

DBSIZE查看数据库大小 FLUSHALL清除所有数据 FLUSHDB只清除当前使用DB的数据(默认有16个数据库) 都可以在后面添加async后台异步执行

杂项

randomKey(随机返回一个存在的key) rename key newKey(给键重新命名) type key(查看指定键的数据类型)

expire key 时间(设置键的过期时间) persist key(取消过期时间)

HyperLogLog

用于统计非重复元素数量,效率高,但是不精确(误差0.81%还可以接受)适合粗略统计大量人数

开始使用稀疏矩阵,空间占用很小,逐渐变大超出阈值才转换为稠密矩阵(占用12KB)

pfadd key value(添加指定元素,会自动去重) pfcount key(查看对应的元素个数) pfmerge des tar(把后面的元素合并到前面的)(注意只能用于计数,无法判断是否存在)

布隆过滤器

若布隆过滤器判断某个值不存在,一定不存在,若判断存在,不一定存在(存在很小的误差,但是效率很高)

bf.add name value(添加元素) bf.exists name value(查看元素是否存在) bf.madd name value …(一次添加多个) bf.mexists name value …(一次判断多个)

bf.reserve name error_rate initial_size(定制布隆过滤器,错误率越低空间越大,若超过初始大小错误率会上升,太大浪费空间)(默认直接添加自动创建的,error_rate是0.01,initial_size是100)

原理(维护一个bit数组)

添加元素时,会使用多个hash函数对key进行hash,获取索引设置bit数组对应位为1

判断存在时,同样获取所有索引,若索引对应值一个不为1就不存在,都为1有很大可能存在

应用

爬虫使用布隆过滤器对爬取过的URL去重

邮箱的垃圾邮件过滤功能,因此会有部分误判,把正常邮件放入垃圾邮件目录(概率很低)

使用zset限制用户规定时间内操作的次数(滑动窗口)

使用当前时间当作score,加入zset,判断操作是否过于频繁,先使用ZREMRANGEBYSCORE name min max(移除时间外的数据),再使用zcard key(查看操作数量)判断是否违规 注意因为要存储每条数据对大量(10s 100W次)的操作就不合适了

漏斗限流

维护一个数,每次请求数量时,判断上次到这次的时间,按比例减少该数,再判断加上请求数量是否超过限制,超过限制操作,没有加上请求数量(只维护一个参数,跟限制数量没有关系)

redis使用cl.throttle进行漏斗限流 cl.throttle key 容量 减少数 单位时间(注意 减少数/单位时间=漏水速度) 请求数量

返回值含义

image-20210425185721646

GeoHash

GeoHash将二维坐标映射为一维整数,然后使用zset存储,value是元素的key,score是GeoHash后的52位整数(zset score存储的是浮点数,不过对于该值还是可以存储)若要获取坐标可以将,score还原为二维坐标

geoadd key 横坐标 纵坐标 name(添加地理位置) geodist key name1 name2(计算两点的距离)

geopos key name(查看指定位置的坐标)

GEORADIUSBYMEMBER key name r 单位 count 数量 排序(获取位于指定位置一定范围内的,可以指定个数,指定排序)

GEORADIUS key 经度 维度 距离 单位(查看与指定坐标相差指定距离的位置,参数与上面几乎一致)

注意,地理位置操作实际就是操作zset,可以直接对key进行zset操作(之所以需要上面的操作是需要做一些转换,使用上面的函数,自动进行转换了)

scan

keys rex(使用正则表达式查看所有键值) 缺点:1使用遍历算法效率低,2没法限制显示的数量

scan 游标 match REX count num(num单次遍历的槽数,不一定一次返回完毕,通过游标查找)注意num是单次扫描槽数不是限定数量

image-20210425192540374

默认游标从0开始,每次查询会返回新的游标,注意就算没有查询到数据,也不一定没有数据了(扫描的槽不存在对应数据),需要查看游标是否为0

image-20210425193152891

内部所有的键通过数组加链表存储(类似老版hashmap)每次扩容大小空间加倍,槽位就是数组的一项

scan采样高位进位遍历

scan采样高位进位遍历rehash后的槽位是相临的

image-20210425193747739

高位进位扩容,直接在高位分别添加0/1,进行二倍扩容,扩容不阻塞用户操作,短时间保留扩容前与扩容后hash,会去扫描新旧两个hash

遍历到110后面会直接从0110->1110向后遍历(防止重复遍历)

Redis是单线程

除了Redis,Node.js,Nginx都是单线程,所以要警惕使用复杂度太高的操作,可能会引起Redis卡顿

Redis使用非阻塞IO与事件轮询(多路复用,使用select轮询类似使用NIO)实现高性能

指令队列与响应队列

Redis会为每个客服端分配一个指令队列,存储客服端的指令,然后顺序执行,同样会为每个客服端关联一个响应队列,通过响应队列把指令结果返回给客服端

定时任务

为了保证定时任务,会把定时任务放到最小堆的数据结构中,每个循环周期会先查看定时任务是否需要执行,需要,执行,不需要查看最近的时间,select会把timeout设置为该时间,然后安心阻塞select(Node.js与Nginx也是类似的)

Resp(可以根据协议使用Socket通信)

Redis的序列化协议(Redis Serialization Protocol),直观的文本协议,优点:实现简单,解析性能好

传输5种最小的数据类型,单元结束统一使用 \r\n

单行字符串使用+开头 +hello world\r\n

多行使用$开头,后跟字符串长度 $11\r\nhello world\r\n

整数以:开头后跟整数的字符串形式 :1024\r\n

错误消息以-开头 -WRONGTYPE OpenXXX

数组以*开头后跟数组长度 *3\r\n:1\r\n:2\r\n:3\r\n 数组【1,2,3】

注意null使用$-1\r\n表示 空串使用$0\r\n\r\n表示

客服端,服务端信息传递

客服端只会向服务端发送多行字符串数组 服务端向客服端响应是多种格式的组合

Redis持久化机制

快照:全量备份,二进制序列,存储紧凑

AOF日志:增量备份,内存修改指令文本,AOF长期执行会变的很大,在数据库重启时重放,时间会很长,需要定期重写清理AOF日志

快照原理

使用子进程进行备份,父进程继续处理请求,原来共享数据页,若父进程修改复制一个数据页(4K)修改,子进程不受影响,继续文件IO备份,虽然随着父进程的修改越来越多的数据页被复制,但不会超过原来内存的2倍(而且冷数据比较多,且大部分是访问操作都被分离的概率比较小)

AOF

只记录对内存修改的指令,先执行指令再将日志存盘

AOF重写

使用bgrewriteaof对AOF日志进行瘦身

原理:使用子进程对内存进行遍历,转换为一系列Redis操作指令,序列化到一个新的AOF日志文件,再将序列化过程发生的增量AOF日志追加到新的AOF日志文件中,追加完毕替换旧的AOF文件

Redis4混合持久化

混合持久化使用快照与AOF日志,AOF日志不再是全量日志,而是自持久化到下次持久化内容,重启时,先加载快照信息,再重放AOF日志

Redis管道(默认写一个读一个等待一次)

使用管道,通过对管道内指令改变读写顺序大幅节省IO时间,管道内的指令越多,效果越好(因为默认指令必须先向服务器发送指令,再接受响应,管道可以修改这一顺序)

对于get key,写几乎没有耗时,写入发送缓冲区就返回了,而read比较耗时,要等消息经过网络路由到目标机器,再送回读缓冲区才返回,这才是网络真正的开销,对于管道,连续的写几乎没有耗时,之后第一个读会等待网络来回开销,然后所有的消息都送回到了读缓冲区,后续读瞬间就返回了

Redis事务

multi开始事务 exec提交事务 discard丢弃事务(实际是丢弃事务队列的指令)

原理:开启事务后指令不直接执行先放到事务队列,一旦接受到exec执行事务队列,一次性返回所有结果(Redis单线程,没有并发问题)

Redis事务没有原子性,中间出错,正确的依旧生效,只是保证隔离性(顺序不被其他指令打扰)

事务网络IO较多可以使用Pipeline减少网络IO次数

Pipeline pipeline = jedis.pipelined();
pipeline.multi();
pipeline.set("name","ok");
pipeline.exec();
//pipeline.discard();

WATCH(乐观锁)

watch 变量(必须在事务开启之前调用,不允许在事务中调用) unwatch(取消观察全部变量,观察变量会累加)

监控一个变量(开始事务开始监控),提交事务时会判断变量是否被事务外改变,若改变事务提交返回null,事务内的指令不执行(注意若事务队列为空正常执行也是返回null)

消息机制

subscribe name …(订阅某个消息) publish name msg(发布消息) psubscribe rex …(使用正则订阅多个消息,有新的符合通道添加,若发送消息也能接受到)

缺点:没有持久化机制,后来注册的无法接收前面的消息

使用32位Redis

若使用32位的Redis,指针开销会减小一半,若不超过4G可以考虑使用,若内存不足可以考虑增加实例来解决

ziplist(字节数组)

对于list,hash,zset若元素较少时,redis为了节约内存空间会使用ziplist结构来存储(注意没有set),超出阈值才恢复默认数据结构(3.0以后list使用quicklist不再使用ziplist与linkedlist)

垃圾回收

操作系统以页为单位回收内存,页上只要有一个key未被删除,它就不会被回收,但是可以被Redis复用(使用第三方内存管理工具)

info

查看Redis所有信息 可以指定一项查看例如:info memory(查看内存使用信息)

可以使用的name server:服务运行环境参数 clients:客服端相关信息 memory:内存统计信息 persistence:持久化信息 stats:通用统计信息 replication:主从复制相关信息 cpu:cpu使用情况 cluster:集群信息 keySpace:键值对统计信息

CAP

C:一致性 A:可用性 P:分区容错性

最终一致性

Redis的主从复制是异步进行的(不满足一致性),客服端修改完后立即返回(满足可用性),Redis保证最终一致性,从节点会努力追赶主节点,最终保持一致(例如网络断开,主从不一致,但网络重新连接时,从节点会努力追赶保持一致)

增量同步

Redis同步的是指令流,主节点会把对自己状态产生修改的指令记录到本地buffer中(环形数组),异步把buffer同步到从节点,从节点一边执行同步来的指令,一边反馈执行到哪了,若网络状况不好,可能导致环形数组后面的指令覆盖前面的,从节点无法直接通过指令流来同步

快照同步

把数据快照到磁盘再传递给从节点,从节点接收完毕,全量更新,期间主节点会继续把修改操作写入本地buffer,若发送指令覆盖会再次执行快照同步(推荐设置合适的buffer长度否则可能陷入,快照同步的死循环),当有新节点加入时必须进行快照同步

无盘复制

主服务器直接通过套接字,将内容发送到从节点(主节点一边遍历,一边把序列化的内容传递给从节点),从节点还是与以前一样,全部存储到磁盘再一次性加载

过期策略

Redis会把设置了过期时间的key都放到一个独立的字典,删除策略有定时遍历与惰性删除(用到时判断是否过时才删除)

定时扫描

每秒扫描10次,并非全部扫描,而是随机在过期字典中选择20个删除已经过期的,若已过期的超过1/4会再次这样操作,而且对单次操作限时25ms,因此若大量的key同时过期会造成卡顿(注意从节点不会定期扫描,主节点删除时会在AOF日志中添加对应的del指令)

内存淘汰策略

若内存超出设置的maxmemory会执行下列的策略

noeviction:不执行写服务,(可以使用del)读可以正常进行(默认)

volatile-lru:在设置了过期时间的键中选,对key使用LRU策略

volatile-ttl:在设置了过期时间的键中选,ttl时间越小的越先被淘汰

volatile-random:在设置了过期时间的键中选,随机删除

allkeys-lru:对所有key使用LRU策略

allkeys-random:对所有key随机删除

Redis的LRU

redis使用的LRU算法是近似的LRU,为每个key增加一个24bit的字段存储最后访问时间戳,在写入时若发现超出maxmemory就随机采样5个(采样范围看是使用的volatile-lru还是allkeys-lru)移除最旧的,若仍超出反复操作(惰性删除)

Redis3以后使用淘汰池,使用一个数组(淘汰池),每次随机新的key融合,淘汰最旧的,保留其他的到下一个循环

unlink

先断开指定key的连接,后台异步回收值(避免del删除大对象导致单线程卡顿)(注意若value很小会直接删除,取消异步处理,例如string不会异步删除) unlink key …(惰性删除键值)删除键的过程都是同步的

缓存穿透

一般缓存都是按key查找若不存在就去查询数据库,恶意请求,大量查询不存在的key,因为查找为空默认不会缓存,每次都直接查询数据库。

解决方案:1查询为空也进行缓存 2对查询的空key进行记录过滤

缓存雪崩

大量缓存同时失效,给后台带来巨大压力。

解决方案:1使用多级缓存(不同缓存失效时间不同) 2不同的key设置不同的过期时间使缓存失效时间尽量均匀

为什么不使用map做缓存

map只能做本地缓存,多个实例会出现缓存不一致情况,redis是分布式缓存

删除方案

1,定时删除(需要CPU一直执行,一到时间,最大限度挤出内存CPU忙CPU不友好)

2,惰性删除(到时间不删除,查询若到时间才删除,CPU友好,不过占内存)

3,定期删除(上面的折中)

hash攻击

如果hash函数存在偏向性,可以利用这种偏向性进行攻击,使hash链表长度极不均匀,查找(大部分修改操作都依赖操作)效率急速下降,服务器被hash查找拖垮

string内部

capacity与len都采用的泛型,为了方便在字符串长度较小时,使用更小的short,byte表示,存储长度是为了方便获取长度

int编码:对于64位有符号整数会使用int编码(把对象转为long存储),可以使用自增操作,Redis在启动时会建立[0,10000)缓存,处于缓存的对象直接指向共享对象,键对应的值不占额外空间

embstr编码:对于长度<=44字节使用该编码,SDS(Simple Dynamic String)结构体嵌入RedisObject对象中,内存分配连续

raw编码:当长度>44字节。采用raw编码,SDS与RedisObject不再在一起,内存分配不连续

list内部

ziplist使用连续数组存储少量信息(没有双向指针节约空间),还存储了最后一项的位置方便反向遍历

考虑到前后指针的内存占用使用quicklist代替ziplist与linkedlist

quicklist是ziplist和linkedlist的混合体,将linkedlist切分,每一段使用ziplist(默认单个8k)让其存储紧凑,多个ziplist使用双向链表串连起来

hash内部

类似老版Java hashmap

set内部

当存储的都是整数,且数量小于512时使用intset(实际就是一个有序数组)

底层也是字典不过所有的value为null

zset内部

内部使用一个hash字典(方便查找)与一个跳跃列表(方便排序)实现

当hash表被删除的稀疏(元素个数少于数组长度的10%)时,会自动缩容

跳跃列表:先顺序存储,第一层每个相连,第二层隔一个相连,第三层隔3隔相连,查找时先找最高层,若超过目标值下一层直到找到目标,或移到最底层还没有找到(skiplist默认分了64层),实际为了效率,并未强制保持这个连接规律,而是使用随机层决定,处于第一层50%,第二层25%依次类推,连接指针存储跨度信息,方便计算排名(找到对象累加即可)

image-20210513140452077

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值