Redis:Remote Dictionary Service
Redis特点:
- 基于内存,速度快
- 支持丰富的数据类型
- 支持事务
- 可设置过期时间
- 可持久化数据(异步操作flush到硬盘上保存)
- value可达1GB
- 单进程单线程
- 读写分离: Master用来插入写,Slave用来检索读
- 受内存限制
Redis适用场景:
- 会话缓存(购物车信息)
- 全页缓存(FPC) 最小速度加载页面
- 队列
- 排行榜/计数器
- 发布/订阅(聊天系统)
Redis的高并发:
- Redis是基于内存的,内存的读写速度非常快
- Redis是单线程的,省去了很多上下文切换的时间
- Redis使用了多路复用技术(epoll),可以处理并发连接
Redis基本数据结构:
数据结构 | 指令 |
string | mget、setnx |
list | lpush(异步队列放入事件)、lpop、blpop、 ltrim(定长链表,index可以是负数,-1表示倒数第一个元素,-2表示倒数第二个元素) |
hash | hset、hget、hgetall、hexists、hkeys、hvals hincrby:对单个子key进行计数 |
set | sdiff、smembers(获取状态)、sinter、scard(点赞)、 srem(消除踩)、sadd |
zset(SortedSet) | zadd(添加关注)、zscore、zrange(获取粉丝)、 zrangebysocre |
对象的底层实现:滑动验证页面
string | int 如果字符串对象保存的是整数值,且可以用long类型表示 embstr 如果字符串对象保存到是一个字符串值,且长度小于等于32字节 raw (SDS) 如果字符串对象保存到是一个字符串值,且长度大于32字节 |
list | ziplist 1. 保存的字符串对象的长度都小于64字节 & 2. 保存的元素数量小于512个 linkedlist 其他 |
hash | 同HashMap,数组+链表 ziplist 1. 保存的字符串对象的长度都小于64字节 & 2. 保存的元素数量小于512个 dict ht(hashtable) 其他 |
set | 内部相当于一个特殊的字典,字典中所有的value都是一个值NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收 interset 1.所有的元素都是整数 & 2.元素数量不超过512个 dict ht(hashtable) 其他 |
zset | ziplist(压缩列表) 1.元素成员的长度小于64字节 & 2.元素量小于128 skiplist+dict 其他 |
String扩容:String扩容都是加倍现有的空间,如果字符串长度超过1MB,扩容时一次只会多扩1MB。字符串最大长度是512MB
渐进式rehash策略:渐进式rehash会在rehash的同时,保留新旧两个hash结构,查询会同时查询两个hash结构,然后在后续的定时任务以及hash操作指令中,循序渐进地将旧hash的内容一点点迁移到新的hash结构中,当搬迁完成,会使用新的hash结构取而代之。
zset:一方面是一个set,保证内部value的唯一性,另一方面可以给每个value赋予一个score,代表这个value的排序权重
容器数据结构的通用规则:
- create if not exists
- drop if no elements
分布式锁:
Redis分布式锁不要用于较长时间的任务
有个稍微安全的方案是set指令的value参数设置一个随机数,释放锁时先匹配随机数是否一致,然后再删除key
setnx xxx true
expire xxx 5 //防止中间逻辑异常del没有被调用,产生死锁
(set xxx true ex 6 nx)
del xxx
锁冲突处理:直接抛出异常,通知用户稍后重试,sleep一会再重试,将请求转移到延时队列,过一会再试
分布式锁不安全场景:
假如在哨兵模式中 主节点获取到锁之后,数据没有同步到从节点主节点挂掉了,这样数据完整性不能保证,另一个客户端请求过来,就会一把锁被两个客户端持有,会导致数据一致性出问题。
同一把锁被两个客户端同时持有
Redlock的解决方式:
加锁时,它会向过半节点发送set指令,只要过半节点set成功,就认为加锁成功
释放锁使用del指令
延迟队列:
队列为空:使用sleep解决
阻塞读:在队列没有数据时,会立即进入休眠状态,一旦数据到来,立即醒过来。消息延迟几乎为零,用blpop/brpop替代lpop/rpop
Redis持久化:
redis持久化RDB和AOF - 乐晨的个人空间 - OSCHINA - 中文开源技术交流社区
一起看懂Redis两种持久化方式的原理 - SegmentFault 思否
名称 | RDB 快照(适合冷备) save 阻塞执行 bgsave fork子进程出来 在指定的时间间隔内生成数据集的时间点快照 |
AOF 日志追加 1.1引入(适合热备) 重构AOF文件:使用bgrewriteaof命令 |
过程 | 多进程Cow:
|
AOF 的默认策略为每秒钟 fsync一次,将内核缓存刷到磁盘
|
优点 | 只有一个备份文件,恢复大数据要快 RDB是内存数据二进制序列化形式,在存储上非常紧凑 |
就算出问题,最多丢失1秒内的更新数据 |
缺点 | 服务器故障丢失数据 子进程如果变大会耗时,造成客户端停止 |
AOF体积可能会很大,系统重启时会从AOF读命令,会很久 所以需要定期进行AOF重写,给AOF瘦身 |
Redis的主节点不会进行持久化操作,持久化操作主要在从节点进行,从节点是备份节点,没有来自客户端请求的压力
Redis4.0使用了混合持久化,将RDB文件的内容和增量的AOF日志文件存在一起。因此Redis重启时,可以先加载RDB内容,然后重放AOF日志,这样就可以代替AOF全量文件重放,重启效率会大幅提升
Jedis连接池:JedisPool
管理连接,缓存Jedis和redis server之间的连接
创建保存连接的容器
LinkedBlockingDeque> idleObject; //存放连接对象
Map,PooledObject> allObjects; //没有可用连接时创建连接
过程:Jedis使用JedisPoolConfig创建idleObject, startEvictor 自动回收空闲连接
JedisConfig config = new JedisConfig();
config.setMaxTotal(10);//最大连接数
config.setMaxIdle(1000);//最大空闲时间
config.setMaxWaitMillis(1000);//连接最大等待时间
config.setTestOnBorrow(true);//获取时是否校验连接
config.setTestOnReturn(false);//归还时是否校验连接
JedisPool pool = new JedisPool(config,......);
Jedis jedis = pool.getResource();
......
pool.returnResource(jedis);
获取一个连接
- 从idleObject队列中获取连接;
- 有空闲连接时直接返回,没有空闲连接时创建空闲连接
- 创建失败后继续阻塞等待
- 连接使用完毕,关闭连接
连接池会将归还的连接从allObjects中拿出来放入到idleObjects中,所以下一次从idleObjects直接获取。
jedisPool会在指定时间对连接池中空闲对象删除
将连接放入到idleObjects队列中,一旦放入,如果长时间不被使用将直接自动回收
为防止异常后jedis对象无法返回回收,用下列方式访问:
try(Jedis jedis = pool.getResource()){ //用完自动close
doSomething(jedis);
}
keys一次返回所有匹配的key,可以使用scan进行替换
Redis遍历所有key的两个命令 -- KEYS 和 SCAN_代码一天不写我浑森蓝廋的博客-CSDN博客_redis获取所以key
Redis写入Redis,移动数据:
Redis过期策略:
Redis定时扫描策略:
如果有大量key过期,给过期时间设置一个随机范围,不要全部在同一个时间过期
Redis定时任务:
Redis定时任务会记录在最小堆数据结构中,最快要执行的任务排在堆的最上方,在每个循环周期里,Redis会对最小堆里面已经到时间点的任务进行处理,处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。
Redis的Pipeline批量操作:客户端提供的,提高效率,节省网络延迟损耗
pipeline会打包客户端指令,将数据打包发送给服务器,无需等待指令的返回
管道的本质:客户端对管道中指令列表改变读写顺序,大幅节省IO时间
redis在处理所有打包数据之前,必须缓存所有指令的处理结果
Pipeline pipelined = jedis.pipeline();
pipelined.set(...);
Redis通信协议RESP(Redis Serialization Protocal)
是一种直观的文本协议,优势在于实现过程简单,解析性能好
Redis协议将传输的数据结构分为5种最小单元类型,单元结束统一加上回车换行符号\r\n
- 单个字符以"+"符号开头 +hello world\r\n
- 多行字符串以”$“符号开头,后跟字符串长度 $11\r\nhello world\r\n
- 整数值以":"符号开头,后跟整数的字符串形式 :1024\r\n
- 错误消息以"-"符号开头 -WRONGTYPE XXXX
- 数组以"*"号开头,后跟数组的长度 *3\r\n:1\r\n:2\r\n:3\r\n
- NULL $-1\r\n
- 空串 $0\r\n\r\n
客户端向服务器发送的指令只有一种格式,多行字符串数组封装发送
Redis排序:SortingParams
SortingParams sortingParams = new SortingParams();
sortingParams.desc();
SortingParams sortingParams = new SortingParams();
sortingParams.alpha();
sortingParams.asc();
sortingParams.limit(0,2);
Redis分布式处理
不同key采用一致性hash分片,将不同key分配到不同Redis服务器上
JedisSharedInfo jedisSharedInfo1 = new JedisSharedInfo("","");
JedisSharedInfo jedisSharedInfo2 = new JedisSharedInfo("","");
List list = new LinkedList();
list.add(jedisSharedInfo1);
list.add(jedisSharedInfo2);
SharedJedisPool pool = new SharedJedisPool(config,list);
SharedJedis sharedJedis = pool.getResource();
Redis高可用方案:
主从复制:
slaveof host port 让一个服务器成为另一个服务器的从服务器
增量复制:Redis同步的是指令流,主节点会将那些自己的状态产生的指令记录在buffer中,然后异步将buffer同步到从节点中,从节点一边执行一边反馈自己同步状态。buffer是有限的,buffer中可能会被后续指令覆盖掉
无盘复制:减少快照同步对系统的影响。生成快照是一个遍历过程,主节点会一边遍历内存,一边将序列化的内容发送到从节点。从节点收到内容存储到磁盘文件中,再进行一次性加载。
哨兵Sentinel:可以监听集群中的服务器,并在主服务器进入下线时,自动从服务器中选举出新的服务器
默认端口26379
- discover_master
- discover_slaves
Sentinel过程:Sentinel负责监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换成主节点。客户端连接集群时,会首先连接Sentinel,通过Sentinel来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向Sentinel要地址,Sentinel将最新的主节点告诉客户端
数据保护:哨兵无法保证消息完全不丢失,可以规定节点复制的延迟时间,如果超过时间则反馈该节点已断开,防止数据丢失
Codis:将众多小内存的Redis实例整合起来,是Redis集群方案之一
Codis上挂接的所有Redis实例构成一个Redis集群,当集群空间不足时,可以通过动态增加Redis实例来实现扩容需求
Codis实现原理:
Codis默认将所有key划分为1024个槽位,它首先将客户端传过来的key进行crc32运算计算hash值,再将hash后的整数值对1024这个整数进行取模得到一个余数,这个余数就是对应的key的槽位。
每个槽位都会唯一映射到后面的多个Redis实例之一,Codis会在内存中维护槽位和Redis实例的映射关系
针对多Codis实例,我们讲槽位关系存储在zookeeper中,Codis增加SLOTSSCAN指令,扫描出待迁移槽位的所有key,然后挨个迁移每个key到新的Redis节点。当Codis接收到位于正在迁移槽位中的key后,会立即强制对当前的单个key进行迁移,迁移完成后,再将请求转发到新的Redis实例中。
Codis会在系统比较空闲的时候观察每个Redis实例对应的slot数量,如果不平衡,就会进行自动迁移
hash = crc32(command.key)
slot_index = hash % 1024
redis = slots[slot_index].redis
redis.do(command)
Codis限制:
- 不再支持事务,事务只能在单个Redis实例中完成
- rename操作会很危险
- 为了支持扩容,单个key对应的value不宜过大(不超过1MB)
- 整体性能上要比单个Redis性能有所下降
- 增加了zookeeper运维的代价
Redis Cluster:
去中心化,每个节点负责的数据会不一样,多个节点相互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议交互集群信息
当大多数节点认为某个节点失联时,集群才认为某节点需要进行主从切换容错
Redis Cluster将所有数据划分为16384个槽位,比Codis的1024个槽位更精细,每个节点负责其中一部分槽位
客户端跳转:
当客户端向一个错误的节点发送指令之后,该节点会发现指令的key所在的槽位并不归自己管理,它会向客户端发送一个特殊的跳转指令并携带目标操作的节点地址,告诉客户端去连接这个节点并获取数据
迁移:
从源节点获取内容 --> 存到目标节点 --> 从源节点删除内容
提供了redis-trib可以让运维人员手动调整槽位的分配情况
Redis Cluster限制:
- 不支持事务
- Cluster的mget方法比Redis慢很多,被拆分成多个get指令
- Cluster的rename方法不再是原子的,需要将数据从源节点转移到目标节点
Redis缓存与Mysql数据库一致性
读:读redis没有,则会从数据库读redis,并写回redis
写:(并发不高)写mysql成功,再写回redis缓存
(并发高)写redis缓存,再写入mysql
Redis与MongDB的区别
事务、持久化、最大key、数据结构
Redis数据流处理流程:
Redis主函数redis.C文件中,主函数最终调用aeMain函数进入事件处理循环
void aeMain(asEventloop *eventloop){
eventloop->stop=0;
while(!eventloop->stop){
if(eventloop->before!=NULL){
eventloop->beforesleep(eventloop);//处理外部事件
}
aeProcessEvent(eventloop.AE_ALL_EVNET);
//文件定时器事件ProcessTimeEvents
//文件相关事件readQueryFromClient
}
}
readOnlyFromClient过程:
processInputBuffer--->processCommand--->call
唤起后lookupCommand会去redisCommandTable中找命令
Redis的启动方式:
- 直接启动
.
/redis-server
&
- 指定配置文件启动
.
/redis-server
/etc/redis/6379
.conf
- 使用redis启动服务配置开机自启动
Redis段页分离:
Redis内存回收策略
lru 最近最少使用 | ttl 删除存活时间最短的 | random 任意选择 | |
volatile 过期数据集 |
|||
allkeys 全体数据集 |
内存回收机制:
Redis可以使用jemalloc、tcmalloc管理内存
操作系统是以页为单位来回收内存的,这个页只要还有一个key在使用,则不会回收
虽然无法保证立即回收已经删除的key的内存,它会重新使用哪些尚未回收的空闲内存
近似LRU算法:
LFU:按最近访问频率进行淘汰
Redis3.0在算法中增加了淘汰池,淘汰池是一个数组,在每一次淘汰循环中,新的随机得出的key列表会和淘汰池中key列表进行融合,淘汰最旧的一个key之后,保留key列表待下一次循环
惰性删除:
unlink key 对删除操作进行懒处理,丢给后台线程来异步回收内存
flush异步化:flushdb async、flushall async,丢给后台线程异步回收
Redis位数介绍:redis位操作介绍 - 知乎
bitfiled有三个子命令:get,set,incrby
bitfield指令提供了溢出策略子指令 overflow,只影响接下来的下一个指令
- 折返wrap
- 失败fail,报错不执行
- 饱和截断sat
例如:bitfield w overflow sat incrby u4 2 1
Redis的位数组是自动扩展的,如果设置了某个偏移位置超过了现有的内容范围,就会自动将位数组进行零填充。
可以用来开发的功能包括:用户一年的迁到记录,在线状态等
HyperLogLog
主要解决统计问题,HyperLogLog提供了不精确去重方案,标准误差是0.81%,减少大数据量下set集合统计的空间浪费
HyperLogLog需要占据12KB存储空间,在存储计数较小时,将采用系数矩阵,当稀疏矩阵超过阈值时,才会一次性转为稠密矩阵(12KB)
- 增加计数:pfadd pfadd key xxx
- 获取计数:pfcount pfcount key
- 将多个pf计数值累加在一起形成一个新的pf值:pfmerge
HyperLogLog的实现原理:
给定一系列随机整数,低位连续零位的最大长度K,可以通过K估算出随机数的数量N。K与N的对数之间存在显著的线性相关性
为什么pf的占用时12KB?
HyperLogLog实现中用的是16384个桶,也就是2的14幂,每个桶的maxbits需要6个bit来存储,最大可以表示maxbits=63,于是总占用内存就是(2的14次幂*6 / 8)=12KB
Redis布隆过滤器:
- bf.add
- bf.exists
- bf.madd 需要添加多个元素
- bf.mexists 判断多个元素是否存在
- 自定义参数布隆过滤器:bf.reserve key error_rate(错误率,默认0.01) initial_size(预计放入的元素数量,当实际数量超过数值,误判率上升.默认100)
hash函数的最佳数量k = 0.7*(位数组长度l / 预计元素的数量n)
错误率f = 0.6185^ (位数组长度l / 预计元素的数量n)
错误率为10%时,一个元素需要平均指纹空间为4792个bit,大约为5bit
错误率为1%时,一个元素需要平均指纹空间为9.585个bit,大约为10bit
错误率为0.1%时,一个元素需要的平均指纹空间为14.377个bit,大约为15bit
Redis-Cell限流:
cl.throttle key 漏斗容量 漏水速率 可选参数
GeoHash:将二位的经纬度数据映射到一维的整数,将所有元素挂载到已条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。GeHash会继续对这个整数做一次base32编码变成一个字符串。
zset的value是key,score是GeoHash的52位整数值。通过zset的score排序就可以得到坐标附近的其他元素
p74
- geoadd key 经度 纬度 别名
- geodist 计算两个元素之间的距离
- geopos geopops company juejin 获取元素的坐标
- geohash 获取元素的经纬度编码字符串
- georadiusbymember 查询元素附近的其他元素,可以用withdist显示距离
georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc
- georadius 根据用户的定位计算附近的一些服务或建筑
注意:在集群中单个key对应的数据量不宜超过1MB,否则会导致集群迁移出现卡顿现象,建议对Geo数据单独部署Redis实例;
如果数据量过大,需要对Geo数据进行拆分(比如按省、市等维度)
Scan:优化key *
scan cursor整数值,key的正则表达模式,limit hint
事务:事务模型很不严格
Redis的单线程特性,可以保证“隔离性”,但Redis事务不具备原子性。因为事务队列中的指令失败,后面的指令会继续执行
- multi 事务开始
- watch key 加上乐观锁
- exec 事务执行,在执行exec命令前所有命令都缓存在一个事务队列中
- discard 事务丢弃
Redis分布式乐观锁watch:当事务执行时,Redis会检查关键变量自watch之后是否被修改了,如果有改动则返回null
PubSub 消息多播
- subscribe
- publish
pubsub的缺点:
改进:Disque专门做多播消息队列
Redis安全:
常见的NoSQL 几张图看懂列式存储_香飘叶子的技术博客_51CTO博客
特点:包括查询快,由于查询需要读取的blocks少;数据压缩比高,正因为同一类型的列存储在一起。Load快。 简化数据建模的复杂性。但是插入更新慢,不太适合数据老是变化,它是按列存储的
缓存击穿:热点数据过期,恰好大量并发请求过来
(解法:热点数据永不过期 / 当发现要过期了,异步进程进行缓存重建)
缓存穿透:用户大量请求在缓存和DB都没有,导致不断的查DB
(解法:DB查不到也缓存在DB中,或使用布隆过滤器)
缓存雪崩:大量key失效,此刻又有大量请求过来
(解法:1)设置不同的过期时间,防止同时失效;2)搭建高可用redis集群;3)设置多级缓存)