目录
5种基础数据结构
1.string(字符串):可以存int,string,byte[]。相当于ArrayList,数组保存字符,有数据冗余,长度小于1M时扩容是扩一倍,长度大于1M时,扩容最多是1M,最多是512M。刚创建时len和capacity一样大,因为大部分情况不会append。如果保存的是整数,那么可以做累计,set age 30, incr age。最大最小是long的最大最小值。
字符串append:字符串使用更多内存,
整数共享:尽量用整数
整数精度:大概保证16~,17-18位会丢失精度,可以把 整数转换为string,取出后转换为BigDecimal
操作指令有:set,get,del,mset(设置多个值),mget
2.list(列表):相当于LinkedList,是一个链表。不是简单的linkedlist,当数据量比较小时是一个ziplist,压缩列表。当数据比较多时是quicklist,链表+ziplist。可以用作队列,右进左出。也可以用作栈,右进右出。
指令有:rpush,rpop,lpush,lpop。阻塞读写blpop,brpush,brpop,plpush可作为阻塞消息队列,但是阻塞可能会断连接,捕获后要做处理。
3.hash(字典):相当于HashMap。Redis为了追求高性能,使用渐进式rehash策略,在rehash时保留新旧两个hash结构,查询会查两个hash结果,然后在定时任务和操作指令中,渐渐地将旧的hash内容一点点迁移到新的hash结构中,迁移完后会用新的hash结构取而代之。当hash迁移完最后一个元素之后,旧的数据结构会被自动删除,内存被回收。
扩容:当元素个数大于第一维数组时开始扩容。hash的存储消耗要高于单个字符串。如果key存的是整数也可以做累计。
指令:hset,hget,hlen,hgetall
4.set(集合):相当于HashSet,内部的键值是无序和唯一的,所有的value都是null。当最后一个元素被移除时,数据结构自动删除,内存被回收。
指令:sadd,smembers(获取所有值),sismember(查询某个值是否存在),scard(获取长度),spop(弹出一个)
5.zset(有序集合):相当于SortedSet和HashMap的结合体。唯一,有序,value保存值,score用来排序。内部是跳表结构。当最后一个元素被移除时,数据结构自动删除,内存被回收。
指令:zdd,zrange(按score排序列出zrang books 0 -1),zrevrange(按score逆序列出),zcard(大小),zscore(获取指定value的score),zrank(value的排名),zrangebyscore(根据分值区间遍历),zrem(删除)
应用场景:延迟队列
容器型数据结构通用规则,包括list,set,hash,zset:
1.create if not exist
2.drop if no elements
数据结构总结:
高级数据结构
1.HyperLogLog: 用来做去重计数,可以用在计算UV(用户访问量)。不是很精准,但也不是很离谱,标准误差是0.81%,最大存储空间是12KB。指令有pfadd,pfcount,pfmerge
2.BloomFilter:布隆过滤器,会有误判,如果判断没有那么就一定没有,判断有可能没有。下载插件: https://github.com/RedisBloom/RedisBloom。精确的可以用RoaringBitMap。
10亿,错误率1%,6个hash方法大概1G内存
3.GeoHash:地图结构,保存地点的经纬度,计算两个地点的值,某个地点附件有哪些公司之类的
4.位图结构
分布式锁
setnx设置一个值是否成功,只有一个会成功。Redis2.8以后加入了超时的扩展参数,超时是原子性的:set lock:88 true ex 5 nx。可重入性可用ThreadLocal来存储当前锁的计数。
集群锁失效问题:如果是Sentinel集群,加锁后发生了failover,主节点换了,这时候就有可能被其他客户端获得锁。解决这个问题可以引入Redlock算法,需要多个实例配合,使用“大多数机制”,加锁时需要大多数节点set成功,比较复杂。
大海捞针指令
keys指令:模糊匹配。缺点:一次性打印查出所有的key,当key很多时会阻塞线程,复杂度是O(n)。指令:keys *
scan指令:复杂度也是O(n),游标分步进行扫描不会阻塞线程,limit参数指定返回的最大长度(不确定,可大可小)。是扫描key的槽位(所有的key都保存在槽上,类似于hashmap),高位进位加法来做遍历(从高位开始遍历,可以避免槽扩容缩容造成漏扫描)。返回的第一个值是游标,0代表遍历完了。
渐进式rehash
java的hashmap中的rehash是新建一个数组,然后把所有的元素搬到新的数组下面,当hashmap元素特别多就会出现卡顿现象。redis使用的是渐进式rehash:会保留新旧两个数组,然后定时任务以及后续对hash的指令操作中渐渐地把旧元素搬到新数组,当所有数组都搬到新数组,删除旧数组。所以会有同时查新旧has的情况。
线程IO模型
1.阻塞IO:一般是对于read方法,read会传入一个参数n,最多读取n个字节后返回,如果一个字节都没有,线程会卡住,直到数据到了或者连接关闭。write方法一般不会阻塞,除非是缓存区满了,才会阻塞。
2.非阻塞IO:解决阻塞IO读等待的问题,读缓存区有多少就读多少,写缓存区有多少空间就写多少,读写完成后立即返回告知实际读写多少。返回后就可以继续做其他的事情。
3.事件轮询(多路复用):调用操作系统提供给用户的API,输入是读写描述符列表read_fds&write_fds,输出是可读可写事件。每次循环来调用这个方法,在timeout时间里如果有事件就立刻返回,然后处理这些事件,没事件的话会等待tiemout时间内返回,这样不断循环,所以叫做事件轮询。比较简单是调用select函数,现代操作系统是使用epoll(linux),kqueue(FreeBSD和macosx)。用来解决非阻塞IO中不知道读写时间何时到来的问题,非阻塞IO每次读写完立即返回,但是不知道下一次何时读写。
定时任务:除了IO线程外,redis需要处理定时任务,如果一直处理IO任务,那么定时任务得不到处理。定时任务会记录在最小堆的结构里,每次处理完最小堆的任务后会把即将要处理的任务的还需要的时间记录下来,这个时间就是IO线程的timeout时间了。如果下一个定时任务没那么快开始,那么timeout就会大点,处理IO线程的等待时间就会大点(没有IO事件时等待)
指令队列:每个客户端关联一个指令队列,指令排队进行顺序处理,先到先服务。
响应队列:服务端为每个客户端关联一个响应队列,将指令的返回结果返回给客户端。
持久化
1.rdb:snapshot快照,持久化时会产生一个子进程,基于Copy On Write机制,子进程看到的数据一直不会变,如果持久化时有数据修改,那么会copy页面给到主进程使用,每个页面大小是4K。一开始内存不会增大,随着父进程修改操作持续进行,内存才会慢慢增大,最大应该也不会达到2倍,因为很多是冷数据。
数据文件目录(rdb和AOF):dir "/Users/bballchan/data/redis/logs"
rdb:
文件名:dbfilename "dump6379.rdb"
手动触发
save:当前线程阻塞创建rdb文件
bgsave:另外起一个线程创建rdb文件
自动触发:
save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存
save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存
save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存
2.AOF原理:保存操作指令,首先进行参数校验和逻辑处理,没问题就才会保存到AOF日志中。为了防止指令在内核缓存区丢失,redis会定期调用fsync函数,将指定文件的内容强制从内核缓存刷新到磁盘。一般是每1秒调用一次函数。
文件名 : appendfilename filename
appendonly true # 开启日志
appendfilename "appendonly.aof"
# appendfsync always 每条命令都写
# appendfsync everysec 每秒写一次(默认这个)
# appendfsync no...... 不写日志
3.混合模式:重启时,用rdb来恢复内存可能会丢失很多数据,用AOF的话又会慢很多。Redis4.0的混合持久化,将rdb和增量的AOF文件存在一起,重启时先加载rdb内容,然后重放增量的AOF日志。
管道:减少网络的来回,将多个写或者多个读一起发送到网络传输。给读写带了很大的性能提升。
redis-benchmark -t set -P 2 -q . -P参数开启并行
事务
事务:只有隔离性,即在一个事务中串行化,不被打扰。但是事务中的命令失败不会回滚,也不会终止执行,后面的命令一样会执行。不具有原子性。
multi:开启事务,exec:执行事务,discard:事务丢弃。watch:是一种乐观锁,读取前调用watch使用场景:读取后乘以倍数后执行事务,如果值被改变过,则事务执行失败。
Redis为什么不适合做数据库
1. 没有SQL,做不了表的关联查询,完成不了复杂的逻辑计算
2. 数据保存在内存里,没有足够大的空间保存数据
3. 弱事务机制,没有回滚。
AOF为什么不像其他数据库一样写预写式日志,而是反过来先执行指令
1.Redis是弱事务机制,不能提前判断指令是否能执行成功,某个指令执行失败也不影响后面指令的执行,如果先写日志的话这个指令可能是无效的,没必要记录,记录的话会增加AOF文件的负担。
2.当出现机器宕机或者突然断电,MySQL等数据库因为是基于磁盘来保存数据的,有可能某个事务只执行了一半,有些已经落库有些没有,机器重启后就可能出现脏数据,这时就需要用undo,redo日志来做事务的恢复。而Redis是基于内存来保存数据,重启后内存数据都没有了,重新读日志加载数据也不会出现只有一半事务的脏数据。
Redis为什么不支持回滚?
1. Redis只会因为语法错误,或者命令用在了错误的类型上(比如在String类型上用了incr),这些失败不能通过回滚解决问题,这些错误是在编程阶段就应该被发现,一般不会出现在生产环境中。
2. 因为不用考虑回滚,所以Redis实现事务可以简单,保持了redis的快速执行。
消息队列--PubSub
广播消息,一个生产者消费,多个消费者接受消息,因为不可靠,很少应用的场景
Redis作为消息队列为什么不是100%可靠?
1.因为只有消费监听的时候,生产者的消息才会被消费掉,如果没有消费者,消息直接丢失
2.PubSub不会做持久化,宕机重启消息就丢失了
内存回收机制
不总是将空闲内存立即归还给操作系统,而是采用分页回收的,如果一个页上有一个key还在使用就不会回收。可以用flushdb命令去清空整个redis库,慎用。
内存分配算法:用第三方内存库实现,有jemalloc(facebook)、tcmalloc(google)、libc
默认有16个库,可以用select 选择
集群
同步方式:主从同步与从从同步
增量同步:使用buffer环来保存增量指令流,如果同步缓慢可能会出现指令的覆盖
快照同步:使用gbsave生成快照文件到磁盘,然后传送到从节点,是很耗时的IO操作。当快照文件生成或者传输慢的话,增量文件可能会被覆盖,然后又不得不重新生成快照文件,这样就是陷入一种死循环当中。新节点必须先完成一次快照同步,然后才能进行增量同步。
无盘复制:Redis2.8.18开始支持无盘复制,加快文件的传输,主节点不生成快照文件,同步套接字把快照内容发送到从节点,从节点依然是先把内容存储到文件,然后一次性加载。
Sentinel(哨兵):
1.监控主从节点的健康,当主节点宕机时,选择一个最优的从节点切换为主节点。同时Sentinel也是高可用集群部署。
2.消息丢失问题:主从同步时不保证消息不会丢失,但是最大可能保证不丢失。用两个参数限制主从延迟过大。是否在复制由下面参数决定
min-slaves-to-write 1 #最少一个从节点在进行复制,否则停止对外服务。
min-slaves-max-lag 10 #单位秒,如果10秒内没有收到从节点的反馈,就意味着从节点同步不正常。
Codis:相当于是Redis的代理。默认槽位是1024,key做hash找到节点。如果节点多可以设置为大点。非官方工具,支持官方新功能有延迟。需要代理定位目标节点。
Cluster:官方集群工具,去中心化,划分为16384个槽位(16K),槽位信息同步到客户端,可以直接定位key所在的节点,有机制纠正客户端与服务端的槽位信息
消息通信Stream
借鉴Kafka设计,把消息保存在一个消息链表,将所有消息串联起来,每个消息有ID对应内容,有持久化,重启后消息还在。可以现在限制链表的长度,超过长度就干掉老的消息。
每个Steam可以挂消费组,组内多个客户端是竞争关系消费消息,消费一个消息,就移动游标,组内客户端一般不会重复消费消息。
Info指令
1.Redis每秒执行多少指令:redis-cli info stats | grep ops
2.查看连接了多少客户端:info clients
3.拒绝了多少连接等:info stats
4.查看复制缓冲区积压大小:
repl_backlog_active:0
repl_backlog_size:1048576 . # 复制环形缓冲区大小,多节点共享,如果复制慢要相应增大
repl_backlog_first_byte_offset:476314
repl_backlog_histlen:724267
过期策略
过期时间的key放入一个独立的字典中
1.惰性删除:客户端访问过期key,对过期时间做检查,如果过期就删除。
2.定时删除:每秒10次扫描,从过期字典中随机选出20个key删除,如果过期key的比例超过1/4则继续取出20个key做删除。默认一次最多扫描不超过25ms。
大量key超时的影响:如果短时间内大量key过期,则需要扫描key次数会上升,加上删除key释放内存也占用CPU,可能会造成卡顿延迟。如果客户端连接超时时间小于25ms,还可能造成客户端连接的超时。
防止大量key超时:设置的过期时间乘以一个随机数
从节点的过期策略:没有维护过期策略,需要主节点在AOP文件增加一条del指令,同步到所有的从节点,从节点才会删除这个key。如果del指令没有及时同步到从节点,主从节点的数据会不一致,主节点没有了,从节点还在
内存熔断限流策略
当Redis内存超出限制,内存数据会跟磁盘产生频繁的交换,这时Redis性能会急剧下降,相当于不可用了,生产环境是不允许出现的。
当内存超出限制,有策略可以让Redis继续提供读写服务:
1.noeviction:默认策略。不会继续写服务(del可以),保证不会丢失数据,但线上服务不能持续进行。
2.volatile-lru:尝试淘汰设置过期的key,最少使用的优先淘汰。这保持持久化化数据不会丢失。
3.volatile-ttl:跟上面类似,但是淘汰ttl最少的key
4.volatile-random:随机淘汰设置过期的key。
5.allkeys-lru:所有key,最少使用的优先淘汰,包括没有设置过期的key
6.allkeys-random:随机淘汰所有key
异步操作
异步删除:也可以说是惰性删除,比如unlink key 会把这个key放到一个同步队列,另起一个异步线程做内存回收,采用引用计数法,如果计数为0则回收内存。异步清空:flushdb async,flushall async
异步写AOP文件:也是把写文件的操作放到异步队列
Redis安全
1.修改默认端口
2.连接设置鉴权
3.使用spiped安全通信
参考:《Redis深度历险:核心原理与应用实践》