redis 应用场景
记录帖子的点赞数,评论数和点击数(hash)
记录用户的帖子id列表(排序),便于快速显示用户的帖子列表(zset)
记录帖子的标题,摘要,作者和封面信息,用于列表页展示(hash)
记录帖子的点赞用户id列表,评论id列表,用于显示和去重计数(zset)
缓存近期热帖内容,减少数据库压力(hash)
记录帖子的相关文章id,根据内容推荐相关帖子(list)
如果帖子id是整数自增的,可以使用redis来分配帖子id(计数器)
收藏和帖子之间的关系(zset)
记录热榜帖子id列表,总热榜和分类热榜(zset)
缓存用户行为历史,过滤恶意行为(zset,hash)
总的来说,
非强一致性(强一致性就要牺牲可用性)
无复杂逻辑计算
存储的数据量不大
docker方式
docker pull redis
docker run --name myredis -d -p 6379:6379 redis
实际安装
mkdir -p ~/redis ~/redis/data ~/redis/conf
docker run -p 6379:6379 --name redis5.0 -v /home/docker-redis/data:/data -v /home/docker-redis/conf:/etc/redis/redis.conf -d redis:5.0 redis-server /etc/redis/redis.conf/redis.conf --appendonly yes
基本使用
set name1 aaa
get name1
批量键值对
set name1 aaa
set name2 bbb
mget name1 name2 //返回一个列表
mset name1 aaa name2 bbb
mget name1 name2
expire name1 5 //5秒后过期
setex name 5 aaa //5秒后过期,等价于上面
链表结构(双向指针)
rpush books python java golang
lpop book
…
rpush books python java golang
rpop books
…
慢操作
rpush books python java golang
lindex books 1
lrange books 0 -1
ltrim books 1 -1
无序字典
数组+链表二维结构,第一维hash的数组位置碰撞时,就会将碰撞的元素使用链表串接起来
渐进式rehash
单独存储字符串,减少网络流量
hash结构的存储消耗要高于单个字符串
hset books java “think in java”
hset books golang “go”
hset books python “python cookbook”
hgetall books
hlen books //3
hget books java
hset books golang “learning go programming” //更新操作,返回0
hmset books java “effective java” python “learning python” golang “learning golang” //批量set
1,如果容器不存在就创建一个,比如rpush 没有就自动创建一个
2,如果容器没有元素就立即删除容器释放内存,lpop操作到最后一个元素,列表消失
过期时间
过期是以对象为单位的
setnx xxx true
…业务逻辑
del xxx
setnx xxx true
//这里可能出问题,因为不是一个事务
expire xxx 5
…业务逻辑
del xxx
redis2.8之后
set xxx true ex 5 nx //setnx和expire组合在一起的原子锁
…业务逻辑
del xxx
超时问题
redis分布式锁不要用于较长时间的任务
lua脚本可以保证连续多个指令的原子性执行,方案不完美
if reids.call(“get”, KEYS[1]) == ARGV[1] then
return redis.call(“del”, KEYS[1])
else
return 0
end
主从模式下会出现问题
使用redrocks
需要提供多个redis实例,加锁时向过半节点发送加锁指令
释放锁时需要向所有节点发送del指令
性能下降一些
需要高性能场景才使用
redis的消息队列没有ack
list,用rpush和lpush操作入队列,用lpop和rpop操作出队列
空队列会出现空轮询
1,让线程休眠一秒
2,blpop/brpop 替换上面的入队出队 //阻塞读
空闲连接会导致服务器主动断开连接,blpop/brpop抛出异常,所以客户端要捕获异常并重试
零存整取
byte数组,先低位后高位
setbit s 1 1
setbit s 2 1
…
get s //“he”
零存领取
setbit w 1 1
get bit w 1 //1
整车领取
set w h
getbit w 1 //1
zset + pipe 实现
不适合限制的量很大,浪费内存
地理位置Geo模块
寻找附近的xx
地理位置距离排序算法
geoadd # 增加
geoadd company 116.48105 39.996794 aaa
geoadd company 116.48105 39.996794 bbb
geoadd company 116.48105 39.996794 ccc
geoadd company 116.48105 39.996794 ddd 116.48105 39.996794 eee
geodist #距离
geodist company aaa bbb km # 单位可以是m,km,ml,ft(米/千米/英里/尺)
geopos # 获取任意元素的经纬度
geopos company aaa
geopos company bbb ccc
geohash # 获取元素的经纬度编码的字符串,base32编码,可以在http://geohash.org/${hash}上直接定位
geohash company aaa
geohash company bbb
georadiubymember #查询指定元素附近的其他元素,参数复杂
georadiubymember company aaa 20 km count 3 asc # 范围20公里以内最多3个元素按距离正排,不排除自身
georadiubymember company bbb 20 km count 3 desc
withcoord,withdist 显示距离,withhash
georadiubymember company aaa 20 km withcoord withdist withhash count 3 asc
georadius # 根据用户的定位来计算附近的车,餐馆等, 目标元素改成经纬度坐标值
georadius company 116.514202 39.905409 20 km withdist count 3 asc
keys 列出所有的key,可以使用正则字符串
keys *
keys aaa*
keys aaa*bbb
提供limit参数,限定服务器单次遍历的字典槽数量(约等于)
返回的结果可能会重复,需要客户端去重
遍历的结果可能不是最新修改后的
遍历是否结束要看游标是否为零
scan 0 match key99* count 1000
scan 13976 match key99* count 1000
scan 1996 match key99* count 1000
scan 11687 match key99* count 1000
因为key是hash+链表挂载,limit是遍历链表挂载的元素匹配后返回给客户端
遍历顺序采用高位进位加法来遍历,避免扩缩容时重复遍历
扩容时采用渐进式rehash
scan可以指定容器集合进行遍历
在平时的业务开发中,要尽量避免大key的产生(内存大起大落)
原理
第一种是快照,快照是全量备份,是内存数据的二进制序列化形式(rdb snapshot)
快照原理,多进程copy on write, cow机制来进行数据段页面的分离,然后父进程修改的是被分离的页面
第二种是AOF,AOF是连续的增量备份,是内存数据修改的指令记录文本,需要定期重写,给日志瘦身
AOF存储的是redis服务器的顺序指令序列
AOF重写,开辟子进程对内存进行遍历,写到新文件,追加操作期间的日志后替换原来的文件
fsync函数将内存缓存强制刷到磁盘, 配置1秒刷一次
主节点一般不进行持久化操作,从节点进行持久化
两个从节点可以保证数据不轻易丢失
事务模型不严格
multi
incr books
exec
原子性
不具备原子性,只是隔离的串行化操作
exec之前丢弃
discard
优化
pipeline 多次io压缩为单词io
pipe = redis.pipeline(transaction=true)
pipe.multi()
pipe.incr(“books”)
pipe.incr(“books”)
values = pipe.execute()
watch
乐观锁
监视变量,如果变量被修改了返回NULL
multi之前监视
消息多播
生产publish(“test”, “aaa”)
消费
p = client.pubsub()
p.subscribe(“test”)
阻塞消费者
p.listen()
模式订阅
subscribe aaa.image aaa.text aaa.log
publish aaa.image xxx
匹配一次订阅多个
psubscribe aaa.*
返回的消息结构
{‘patrern’: None, ‘type’: ‘subscribe’, ‘channel’: ‘aaa’, ‘data’: 1L}
data 一个字符串
channel 主题名字
type 消息类型
pattern 那种模式订阅
缺点
消费者断连会丢消息
redis停机重启消息也会直接丢弃
多播单独的项目 Disque
被stream(持久化消息队列)代替了
redis的ziplist是一个紧凑的字节数组结构
redis的intset是一个紧凑的字节数组结构
集群
cap原理概括: 当网络分区发生时,一致性和可用性两难全
保证最终一致性
主从同步与从从同步
增量同步
redis同步指令流,记录到本地内存buffer,一个定长的环形数组,满了会覆盖,所以主从同步会丢失
快照同步
bgsave 将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点
从节点接收完毕立即执行一次全量加载,加载之前先要将当前内存的数据清空,加载完毕后通知主节点继续增量同步
如果同步时间过长或复制buffer太小,都会导致同步期间的增量指令在复制buffer中被覆盖,
会导致快照同步后无法进行增量复制,再次发起快照同步,陷入死循环
需要配置一个合适的复制buffer大小参数
增加从节点
加入集群必须先要进行一次快照同步
无盘复制
redis2.8.18开始支持,主服务器直接通过套接字将快照内容发送到从节点,主节点一遍遍历内存,一遍将序列化的内容发送到从节点
wait指令
让一步复制变成同步复制
wait 1 0 第一个参数是从库的数量N,第二个参数是时间t(毫秒),t为0时如果发生网络分区,主从同步无法继续进行,wait指令会永远阻塞
redis服务器不可用
代理分片
将所有的key默认划分为1024个槽位,crc32运算计算哈希值,再将hash后的整数值对1024这个整数进行取模得到一个余数,这个余数就是对于key的槽位
三个redis节点组成,相互连接,特殊的二进制协议相互交互集群信息
每个节点负责一部分槽位
槽位定位算法
默认对key值使用crc32算法进行hash得到一个整数值,然后用这个整数值对16384进行取模来得到具体槽位
允许嵌入tag来允许用户强制某个key挂在特定槽位上
迁移
redis-trib可以手动调整槽位的分配
槽为单位迁移,一次性获取源节点槽位的所有key列表,再每个key进行迁移
源节点获取内容—>存到目标节点—>从源节点删除内容
迁移影响服务效率
容错
提供了一个参数cluster-require-full-coverage可以允许部分节点故障,其他节点还可以继续提供对外访问
网络抖动
cluster-node-timeout,表示当某个节点持续timeout的时间失联时,才可以认定该节点出现故障
cluster-slave-validity-factor作为倍乘系数来放大这个超时时间来宽松容错的紧急程度,为零不会抗拒网络抖动,大于1主从切换变为松弛系数
可能下线与确定下线
集群采用gossip协议来广播自己的状态以及自己对整个集群认知的改变,比如失联节点数达到大多数,通知其他节点下线并立即对该节点进行主从切换
不支持事务
槽位迁移感知
moved纠正槽位
asking临时纠正槽位
重试超过次数会抛出异常
支持多播的可持久化的消息队列
消息链表,可持久化.xadd指令追加
每个stream可以挂多个消费组
xgroup create 创建消费组
状态变量pending_ids
xadd
xdel
xrange 自动过滤已删除
xlen
del
xadd aaa * k v //*表示自动生成id
独立消费
xread
生产环境中是不允许redis出现交换行为的,redis提供maxmemory来限制内存超过期望大小
当实际内存超出maxmemory时,redis提供了几种可选策略来决定如何腾出空间
noeviction 默认策略,可读不可写
volatile-lru 尝试淘汰设置了过期时间的key,最少使用的key优先被淘汰,没有设置过期时间的key不会被淘汰
volaile-ttl 尝试淘汰key剩余寿命ttl小的值,越小越优先被淘汰
volatile-random 随机淘汰过期key集合中随机的key
allkeys0lru 尝试淘汰所有最少使用的key优先被淘汰
allkeys-random 尝试随机淘汰所有key
redis使用近似lur算法(因为lru算法耗内存)
给每个key增加一个额外的小字段,长度为24bit,也就是最后一次被访问的时间戳
懒惰处理
算法是随机采样出5个(可以配置)key,然后淘汰掉最旧的key
大key异步删除,小key直接删除
unlink key
清空数据库
flush async
aof 刷数据到硬盘也是异步处理
源码
字节数组的形式
struct SDS {
T capacity; //数组容量
T len; //数组长度
byte flags; //特殊标识位
byte[] content; //数组内容
}
不同长度的字符串使用不同的结构体表示
长度短 emb
长度>44 raw
redis 对象头结构体
struct RedisObject {
int4 type; //4bits
int4 encoding; //4bits
int24 lru; //24bits
int32 refcount; 4bytes
void *ptr; //8bytes, 64-bit system
} robj;
struct SDS {
int8 capacity; //1byte
int 8 len; //1byte
int flags; //1byte
byte[] content; //内联数组,长度为capacity
}
embstr = malloc(redisObj + SDS)
大于64字节使用raw
struct RedisDb {
dict* dict; //all keys
dict* expires; //allexpired keys
…
}
struct zset {
dict* dict; //allvalues
zskiplist* zsl;
}
两个hashtable(新旧)进行迁移
数组+链表
struct dictEnty {
void* key;
void* val;
dictEntry* next;
}
struct dictht {
dictEntry** table; //二维
long size; //第一维数组的长度
long used; //hash表中的元素个数
}
渐进式rehash
扩容,执行指令后触发小步搬迁,或者服务器定时任务触发
查找过程
key映射成整数
hash函数
siphash
hash攻击
存在偏向性会导致hash第二维链表长度极不均匀
扩容
正常情况下hash表中元素的个数等于第一维数组的长度时就会开始扩容,扩两倍
如果正在做bgsave,为了减少内存页的过多分离(cow),元素个数达到第一维数组长度的5倍才会强制扩容
zset和hash容器对象在元素个数较少的时候,采用压缩列表(ziplist)进行存储
压缩列表是连续的内存空间
debug object books
以你为是连续存储,所有不适合占据内存太大,因为重新分配内存和拷贝内存就会有很大的消耗