redis
基本数据类型
- string:缓存k/v,分布式锁、session/token单点、预热数据
- hash:存储结构化的信息=>
控制不同维度的config - list:微博粉丝列表,评论列表,(通过lrange
分页查询)、简单队列 - set: 赞/踩、标签、共同好友,并交集的
集合计算 zset: 排行榜,多元素排序(orderbook)
redis高级数据结构
- bitmap: 实现BloomFilter
- HyperLogLog: 不精确的去重计数,PV,UV统计
- Geo: 存储地理信息
- Pub/Sub: 消费生产者场景
- Stream:
高级应用,这块是可以多学习研究应用...
- neural-redis 主要是神经网络的机器学,集成到redis
- RedisSearch 主要支持一些富文本的的搜索
- RedisBloom 支持分布式环境下的Bloom过滤器
BloomFilter
适合
缓存击穿的场景(使用大量不存在的key频繁击穿redis访问db数据库)
- 黑客攻击
- 活动热点数据;
应用:预热(拦截缓存不存在的数据)
实现方式:多个hash + bitMap,多一个key进行多次hash计算;误判: 存在的可能判断为不存在,但是不存在的key一定是不存在的=> 预热;存在的问题:1. 存在的数据存在误判率 2. 缓存删除/刷新困难(可以试试bitmap);
内存分配算法
jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好;
LRU
主要的内清空算法:
- LRU(Least Recently Used) 最近
最久使用(只考虑时间) - LFU(Least Frequently Used)最近
最少使用(需要计数) - FIFO
LRU 算法实现2个方式(java)
- 继承LinkHashMap(java)
- HashMap + 双向连表(注意并发问题)
RedisObject-SDS
RedisObject是redis存储value的最底层的数据实现。key指针是指向sds,value指针指向RedisObject.
dictEntry:Redis给每个key-value键值对分配一个dictEntry,里面有着key和val的指针,next指向下一个dictEntry形成链表(是用来解决hash冲突);链地址法:数组+琏表实现=>对比java的HashMap;sds:键key“hello”是以SDS(简单动态字符串simple dynamic string)存储,SDS是存储String的数据结构.RedisObject:值val“world”存储在redisObject中。redis常用5中类型都是以redisObject来存储的;而redisObject中的type字段指明了Value对象的类型,ptr字段则指向对象所在的地址。- type是值的类型:REDIS_STRING,REDIS_LIST,REDIS_HASH,REDIS_SET,REDIS_ZSET,
- encoding:底层的实际编码实现
// value
typedef struct Redisobject{
unsight type; // 值的类型->5种
unsight encoding; // 值的编码,如下图
void *ptr // 值的空间地址
refcount // 引用计数
lru // 但前key最后一次被访问的时间
};
数据结构类型\编码类型
数据类型: 上面的5中; 编码类型: int,定长(embstr)/可变长(raw),hashtable,linkedlist,压缩琏表(ziplist),跳跃琏表(skiplist),intset;
=> 特定的数据类型没有关联一种固定的编码类型。
// 查看数据类型: type,查看数据编码:object encoding
redis> set number 1
redis> set hello world
# 查看值的编码类型
redis> object encoding number # => int
redis> object encoding hello # => embstr
redis> append hello "x100000000000000000"
redis> append number "x100000000000000000"
redis> object encoding number # => raw
redis> obecct encoding hello # => raw
# 查看数据类型, 特定的类型没有关联一种固定的编码
redis> type hello # => string,查看type
#embstr编码是通过调用一次内存分配函数来分配一块连续的空间, 而raw需要调用两次(空间发生了扩展)
数据类型type <===> 数据编码(实现)object encoding
String
String的数据类型type=string;编码类型是encoding=int|smbstr|raw.
最大长度(包括key、string类型的value)=>512M(半个G)
struct sdshdr {
int len; // buf中已占用空间的长度,即String的长度
int free; // buf 中剩余可用空间的长度
char buf[]; //’\0’空字符结尾,和C语言一样,数据空间,就是`byte数组`
}
空间预分配: 如果len<1M,则free=len,总的长度buf=2(len)+1;如果len>=1M,则free=1M,总的空间buf=len+1M+1 =>节省连续分配拼接导致的空间分配次数;即当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M; 1M分界,512M最大空间惰性释放: 字符串截取(sdstrim)后,多出的free空间不会立即释放 => 字符串截取后的空出的空间不会立即释放空间杜绝溢出: 空间赋值(sdscat)不会溢出,对比strcat(*dst,*src)不负责校验空间大小二进制安全:二进制安全(binary-safe:能直接操作byte)的,能存储任何数据str/音频bytes,不是根据\0判断字符结束的;
=> key和String类型的value都是sds存储,最大512M => INCR/DECR的val可转int64,最大值比数据库ROW_ID(mysql使用bigint做PK)
List
数据类型type=list;编码类型encoding =ziplist|linkedlist;底层实现是quicklist(快速列表,是ziplist压缩列表和linkedlist双端链表的组合)
# 双链便于排序
typedef struct listNode {
struct listNode *prev; // 前置节点
struct listNode *next; // 后置节点
void *value; // 节点的值
} listNode;
typedef struct list {
listNode *head; // 表头节点
listNode *tail; // 表尾节点
unsigned long len; // 链表所包含的节点数量
} list;
- LinkedList
- zipList:特殊编码的连续内存块组成的顺序型数据结构=>类似数组
- quickList:快速链表是 zipList 和 linkedList 的混合体。它将linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
连表的实现一搬就2中双联表|数组; 双联表+数组的结合算是redis一种创新了; => BLPOP/BRPOP阻塞命令,当队列使用 => 当list使用注意阻塞命令:复杂度是(N)的命令
Hash
数据类型type=hash,数据编码encoding=ziplist|hashtable
只有同时满足下面两个条件时,才会使用ziplist
- 哈希中元素数量小于512个
- 哈希中所有键值对的键和值字符串长度都小于
64字节
typedef struct dict {
dictht ht[2];//哈希表,ht[0]实际hash数据,ht[1]辅助计算rehash
int rehashidx;// =-1,则不进行rehashing
int iterators;//number of iterators currently running,目前正在运行的安全迭代器的数量
dictType *type; //字典类型->提供类型对应的操作函数
void *privdata;//私有数据-> dictType的函数的入参数
} dict;
// 哈希表
typedef struct dictht {
dictEntry **table; //哈希表数组(指针数组)
unsigned long size; //哈希表大小
unsigned long sizemask;//哈希表大小掩码,用于计算索引值,总=-1
unsigned long used; //该哈希表已有节点的数量
} dictht;
// hash节点
typedef struct dictEntry {
void *key;
union {void *val;uint64_t u64;int64_t s64;} v;
struct dictEntry *next;//开链法&头插入
} dictEntry;
// 字典类型 => 不同的类型对应不同的操作函数
typedef struct dictType {
// 计算哈希值的函数,默认使用MurmurHash2算法计算hash,结果随机性好
unsigned int (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;
// 1.计算哈希 hash = dict->type->hashFunction(k)
// 2.计算索引 index =hash & dict->ht[0].sizemask
// 4.rehash过程:由于hash表数据过多,空间需要扩展,并对所有数据重新hash处理的过程
- rehash扩容实现方式: 2个hash表
Redis采用了渐进式rehash的方案。它会同时保留两个新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿(单线程)现象。rehashidx记录扩容的进度,渐进完成数据迁移。
- hash冲突
开琏;插入的方式是使用的头插法;redis单线程不会出现多线程的场景 => Strings存在的功能Hash基本都能实现,所以Hash更适合做面向对象的缓存/统计; => HGETALL/HKEYS/HVALS 复杂度O(N)
set
数据类型是type=set,数据编码是encoding=intset|hashtable;
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; //集合包含的元素数量
int8_t contents[]; // 保存元素的数组
} intset;
=>SMEMBERS/SUNION/SUNIONSTORE/SINTER/SINTERSTORE/SDIFF/SDIFFSTORE等集合操作和返回所有元素SMEMBERS是O(N)的操作,可能存在阻塞,慎用
zset
数据类型type = zset,数据编码encoding=ziplist|skiplist
typedef struct zskiplistNode {
robj *redisObject; //值:o1,o2,O3
double score;//分值,从小到大排列:1.0,2.0,3.0
struct zskiplistNode *backward;//后退指针:BW
struct zskiplistLevel {
struct zskiplistNode *forward;//前进指针
unsigned int span;//跨度-前进指针所指向节点与当前节点的距离
} level[];//节点分层:L1..L5
} zskiplistNode;
// 跳跃表,每个节点维持多个指向其他节点的指针,查询复杂度O(logN),对比平衡树
typedef struct zskiplist {
struct zskiplistNode *header,*tail;
unsigned long length;// 表中节点的数量
int level;// 表中最大的的层数
} zskiplist;
zset排序规则
zset:先根据score(double)排序,再根据value(string)字典序排序。
=> ZRANGE/ZREVRANGE,返回所有,时间复杂度O(log(N)+M),M为本次返回的member数 => ZRANGEBYSCORE/ZREVRANGEBYSCORE,O(log(N)+M) => ZREMRANGEBYRANK/ZREMRANGEBYSCORE,O(log(N)+M)
复杂度分析
- get/insert/delete复杂度约lgN(level约等于lgN);复杂度约等于lgN+当前level节点个数
- get/insert/delete的算法过程
ps
type/encoding结合的作用:优化编码,特定的type不绑定特定的encoding,比如hash绑定zuplist或hashtable;1.元素少时为了节约内存,2.元素多时提高效率如何实现命令的判断(命令执行的过程)(DEL对所有的类型适用,SET只对String)- 类型校验:通过type判断命令是否支持,例如llen(key)->list支持
- 多态命令:encoding;选择命令特定的实现方式,选择ziplistlen()或者linkedlistlen()
- 引用计数 + lru
- BitMap实际是char数组
- HyperLogLogs只能用于计算一个集合中不重复的元素数量,set还会存储所有的元素;
- CONFIG GET/config set 修改配置
数据库和事务
struct redisServer{
redisDb *bd; // 数据库空间
int dbnum; // 数据库个数,默认0号库,数组空间
clients // 已经链接的客户端
}
struct redisClient{
rredisDb *db; // 客户端链接的db指针
}
// 数据库=> 每个库包含2个字典
struct redisDb{
dict *dict; // KV数据库: key-value
dict *expires // K的过期时间:key-unix时间戳(long)
}
redis> select 2 // 切换2号库
redis[2]> get num // 查询2号库
过期key的删除
惰性删除: get操作的检查ttl,超时返回nil;定期删除: redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除
内存淘汰策略
当redis使用内存达到最大配置内存|或者内存不足时,触发的内存淘汰算法;
maxmemory 100mb
maxmemory-policy volatile-lru #默认是noeviction,即不进行数据淘汰,推荐volatile-lru
redis内有2个hash,1个存值,1个存ttl
- volatile-lru:从设置expire的key集合中LRU
- allkeys-lru:从所有的key集合中做LRU
- volatile-random:从设置expire的key集合中random
- allkeys-random:从所有key集random
- volatile-ttl:从设置expire的key中优先回收存活时间(TTL)较短的键
- no-eviction:禁止删除数据,当内存不足时,写入会报错
理解: redis默认配置是
no-eviction,内存不足时报错让客户端自己发现;allKeys是全量,操作的应该是KV库,所以只支持lru/随机算法;volatile是设置过expire的key;操作的应该是KE库(key-expire),所以支持lru/随机/ttl;
volatile-ttl使用注意:
存在大量ttl相同的key,比如数据预热的场景;如果大量的key过期时间设置的过于集中,到过期的那个时间点,Redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,一般需要在expire上加一个随机值,使得过期时间分散一些.
redis持久化
包括RDB与AOF ;RDB是内存快照文件,AOF是追加日志;4.0默认RDB;现在是RDB+AOF;
RDB
定期将内存中的数据集快照写入磁盘;实际过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储,fork子进程在数据量大时可能影响正常redis服务(多核环境下影响会小)
# bgsave|save [senonds] [changes],每seconds检查数据变更,超过changes则RDB
save|bgsave 60 10000
copy-on-write
对于某个正在写的内存页(mem page);先复制一份交给主进程去write;原来的page页不修改交给fork进程来copy; write完成后,再通过指令同步当前page;(本质上是通过复制解决并发问题..)
AOF
Redis会把每一个写请求都记录在一个日志文件里;数据恢复没有RDB快
# 默认关闭
appendonly yes
# 日志fsync,no:OS决定同步时机;always:每个写命令都fsync,everysec:每秒进行fysnc
appendfsync no|always|everysec
# 通过指令触发aof的rewrite功能,优化没用的指令
bgrewriteaof
# rewrite支持命令合并
rewrite
=>每一次RDB快照和AOFRewrite都需要Redis主进程进行fork操作 =>append中途发生当机,使用redis-check-aof检查数据的一致性
事务|pipeline|script
pipeline
减少多个指令的RTT时间;要求各个命令互不相关,不相互依赖(类似happend-before)
事务
MULTI|EXEC|DISCARD|WATCH;不支持事务回滚和持久化
script
EVAL|EVALSHA
集群
集群主要考虑的问题:
- 高可用(ha)
- 主从切换
- 主从监控(failover)
- 可扩展
- 扩容
- 分区
主从复制
slaveof 192.168.1.1 6379
当Slave启动后,会从Master进行一次冷启动数据同步,由Master触发BGSAVE生成RDB文件推送给Slave进行导入,导入完成后Master再将增量数据通过Redis Protocol同步给Slave。之后主从之间的数据便一直以Redis Protocol进行同步
增量|全量同步
- master机器不要做rdb,在salve做rdb,aof
- 主从结构的拓扑使用链,不使用图(master<-slave<-slave; 未验证)
cow的过程
借助buffer记录操作指令
Sentinel
- sentinel-failover
单独进程redis-sentinel;监控主从完成failover;Redis Sentinel需要至少部署3个实例才能形成选举关系(1主2从..)
=>HA(high avaliable)问题,即主从+failover
Cluster
- 将数据分散到多个节点上(2主,每个主2个从..)
- 使用hashslot(哈西槽)算法;自动转发key到正确的分片
- 部分节点失效,自动failover(每个节点都支持主从)
- hash tags可以实现将指定的数据放在同一个分片; 事务/pipeline/script/部分业务需求.
index=(hashslot_lengh-1)& crc16(key);//16384
=> cluster=>就是扩容问题
集群的模式
hashlot(哈希曹)
redis集群不使用一致性hash管理集群;而是使用hashlot, 总计lenght = 2^32-1个曹, CRC(key) & (length-1)分布节点; 集群最大节点个数16384
内存优化:
尽量使用hash,而不是String描述对象; 使小的k-v存储更紧凑
单机redis存储上限:
- key个数=>2^32 = CRC(key) & (max.int-1);就是是曹的个数
- 内存占用的大小=>硬件物理内存
删除统一前缀的key
使用scan
性能总结
info
server,clients,memory,persistence,stats,replication,cpu,commandstats,cluster,keyspace
# Server
redis_version:3.2.10
redis_git_sha1:0
redis_git_dirty:0
redis_build_id:0
redis_mode:standalone
os:Amazon ElastiCache
arch_bits:64
multiplexing_api:epoll
gcc_version:0.0.0
process_id:1
run_id:9571354644a0837dc7b2ce77f7efa6e28c8988f0
tcp_port:6379
uptime_in_seconds:15106531
uptime_in_days:174
hz:10
lru_clock:7820064
executable:-
config_file:-
# Clients
connected_clients:193 //已经链接的客户端,支持的最大链接10000,(控制在<5K)
client_longest_output_list:0 //最长输出列表
client_biggest_input_buf:0 // 最大输入缓存
blocked_clients:0 //阻塞的客户端
# Memory // (默认Byte)
used_memory:41753320 //redis已经分配的内存;used_memory>可用最大内存(used_memory_rss),redis会与操作系统的Swap交换
used_memory_human:39.82M
used_memory_rss:47652864 // OS分配的内存,包含内存碎片
used_memory_rss_human:45.45M
used_memory_peak:49348312 //内存消耗的峰值
used_memory_peak_human:47.06M
used_memory_lua:38912 // lua脚本引擎的内存
used_memory_lua_human:38.00K
maxmemory:2241331200 //最大内存,一般为系统内存的45%
maxmemory_human:2.09G //内存淘汰策略
maxmemory_policy:volatile-lru //LRU内存淘汰算法
mem_fragmentation_ratio:1.14 // 内存碎片率 = used_memory_rss/used_memory
mem_allocator:jemalloc-4.0.3 // redis 内存分配的工具:libc、jemalloc、tcmalloc
# Persistence
loading:0
rdb_changes_since_last_save:175180486
rdb_bgsave_in_progress:0
rdb_last_save_time:1536217405
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:-1
rdb_current_bgsave_time_sec:-1
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
# Stats
total_connections_received:927991
total_commands_processed:2628710092
instantaneous_ops_per_sec:551
total_net_input_bytes:78939024381
total_net_output_bytes:94985461268
instantaneous_input_kbps:15.94
instantaneous_output_kbps:12.23
rejected_connections:0
sync_full:3
sync_partial_ok:0
sync_partial_err:0
expired_keys:3770799
evicted_keys:0
keyspace_hits:654476790
keyspace_misses:30590228
pubsub_channels:0
pubsub_patterns:9
latest_fork_usec:171
migrate_cached_sockets:0
# Replication
role:master
connected_slaves:2
slave0:ip=172.16.1.200,port=6379,state=online,offset=14602705035,lag=1
slave1:ip=172.16.1.246,port=6379,state=online,offset=14602706847,lag=0
master_repl_offset:14602706847
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:14601658272
repl_backlog_histlen:1048576
# CPU
used_cpu_sys:50107.83
used_cpu_user:24351.38
used_cpu_sys_children:0.00
used_cpu_user_children:0.00
# Cluster
cluster_enabled:0
# Keyspace
db0:keys=117422,expires=177,avg_ttl=349536033
keyspace_hits: # key查询命中
keyspace_misses: # key查询未命中
slowlog
#执行时间慢于xxx毫秒的命令计入Slow Log
slowlog-log-slower-than xxxms
#Slow Log的长度,即最大纪录多少条Slow Log
slowlog-max-len xxx
=> slowlog get number //查询 => slowlog reset // 重置
interview
- redis命令执行的过程(chapter08)
- 过期key的删除策略(chapter09)
- 集群如何实现高可用
- 数据持久化的方案
高性能分析
单进程单线程的内存结构化数据库,由C语言编写,官方提供的数据是可以达到10W+的QPS(每秒内Query次数,是一般数据库的10-100倍,mysql的QPS不要超过1W+)
从内存/连接2个角度..说明redis快..
- 完全是内存存储,数据结构化,读写大部分复杂度O(1),高级的数据结构进过专业设计
- NIO多路复用(epoll),减少网络IO时间
- 单线程,没有线程切换消耗和锁的问题,但可能存在线程阻塞(keys)
- 文件事件:IO复用;定时事件:timer
memcache
| 缓存 | 说明 |
|---|---|
本地缓存 | map,guava,memcached,Ehcache |
分布式缓存 | redis |
分级缓存 | jvm + redis |
redis与memcached的对比
- redis
- 支持5个数据结构,支持批量操作和事务
- redis可以持久化,2个持久化方案
- 支持集群;HA
- NIO + 单线程(epoll)
- lua,队列,功能丰富
- memcached
- 只支持K-V结构
- 虽然都是内存;但不提供持久化
- 不支持集群和主从同步功能
- 多线程模式
key不能超过250个字节,value不能超过1M字节,key 的最大失效时间是30天
雪崩/穿透/击穿
缓存击穿和缓存穿透
穿透:key不存在;直接穿透redis+mysql(都不会命中) 击穿:key失效;redis失效,查询走mysql,不走redis(命中mysql)
击穿场景:redis服务器正常,访问直接访问都到数据库,redis不命中.
- 缓冲中key不存在/
过期(看是否造成redis卡顿),会查询数据库,并发量大时数据库存在危险; - 数据没有热备,上线启动数据冲击数据库;
- 过期时间离散分布,处理好缓存一致性
- 做好数据预热(热备):一般使用锁在初次访问预热
穿透场景: key既不命中redis,也不命中mysql
- 黑客使用缓存和数据库都不存在的key查询,可以用来攻击数据库
- 接口参数校验/nginx访问控制频率
- 使用本地缓存拦截
- 缓存第一次访问的数据,避免重复访问数据库; 缓存无效的数据(key:null,并设置过期时间),这个方法要注意更新缓存.
- bloomFilter(布隆过滤器)-> 适合场景2处理,但需要提前热备数据
bloomFilter
利用高效的数据结构(bitmap)和算法快速(hash)判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return;
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
一个数据结构:bit向量,比hashmap占用空间小 k个散列函数:hash,计算速度块 判断结果:全为1则存在,有一个为0则不存在;
场景: 数据库id预热到bloom,接口使用-1访问时,会被bloom过滤; 缺点:存在错误率,删除数据困难
缓存雪崩
雪崩场景:redis服务器崩溃,原来应该命中缓存的需求全部请求数据库,并对数据库造成压力
- 内存中大面积key失效(ttl)造成卡顿/集群宕机/redis卡顿/redis热备/
解决
- 集群+HA:redis集群健壮(cluster),主从切换+哨兵sentinel
- 监控:做好缓存的监控,主从切换
- 内存策略:合适的内存淘汰测试,key过期删除机制
- redis做好持久化工作,便于数据恢复,宕机重启(AOF+RDB)
- 关键业务的限流/缓存分级/
RateLimiter源码实现
缓存-数据库双写时的一致性
- 编码维护的
spring-cache提供的模式(最经典的缓存+数据库读写的模式);就是
Cache-AsidePattern;
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。- 但是null一般不写缓存(自己实现的) =>不能解决缓存穿透的问题
锁-分布式
setnx
redission
keys,SMEMBERS指令的问题,scan指令
Redis的单线程的。当 KEYS 命令被用于处理一个大的数据库时,又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。
SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代
使用 SMEMBERS 命令可以返回集合键当前包含的所有元素,但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证.
doc.redisfans.com/key/scan.ht…
队列
rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来,
使用pub/sub主题订阅者模式,可以实现 1:N 的消息队列,消息消费后丢失,建使用rocketMQ
Redis如何实现延时队列:
使用zset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理.
集群扩展性+高可用HA
Redis Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。(主从 + Sentinal)
Redis Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
Redis cluster,并且是主从同步/读写分离,类似Mysql的主从同步,Redis cluster 支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node。
Sentinal
- 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
- 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
- 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
redis单线程的,现在服务器都是多核的,是不是很浪费
是的,单线程的,但是,我们可以通过在单机开多个Redis实例-cluster,每个实例是单线程的。(内存不足时一崩全崩)
redis主从同步过程
你启动一台slave的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些命令(cow时生成)都发给slave。
Redis线程模型
Redis 内部使用文件事件处理器file event handler,这个文件事件处理器是单线程的,所以Redis才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个Socket
- IO多路复用程序
文件事件分派器- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。事件处理器是单线程的。
热key
热key问题就是:突然有几十万的请求去访问redis上的某个特定key(单个key访问超过redis的QPS)
- 业务发现:某商品在做秒杀,那这个商品的key就可以判断出是热key
- 业务端代码统计
- redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可
处理:
- 多级缓存:jvm-cache
- redis-cluter 实现读写分离
- redis上层加loadbalance ??没见过开源工具??
codis对比redis-cluster
redis集群的解决方案:
- redis-cluster基于hashlot算法实现分片
- codis基于代理实现,现在支持3.0
ps
- [中文官网]www.redis.cn/
- [跳跃表]juejin.im/post/5b53ee…
- [redis设计与实现]redisbook.com/
- HashMap,LinkedHashMap的源码
- [RedisObject]juejin.im/collection/…
- mp.weixin.qq.com/s/jNVJHJOPU…
- [codis]github.com/CodisLabs/c…
- [twemproxy]www.oschina.net/p/twemproxy