redis-note-1

191 阅读24分钟

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

主要的内清空算法:

  1. LRU(Least Recently Used) 最近最久使用(只考虑时间)
  2. LFU(Least Frequently Used)最近最少使用(需要计数)
  3. FIFO

LRU 算法实现2个方式(java)

  1. 继承LinkHashMap(java)
  2. HashMap + 双向连表(注意并发问题)

RedisObject-SDS

RedisObject是redis存储value的最底层的数据实现。key指针是指向sds,value指针指向RedisObject.

redis-redisobject.jpg

  • 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最后一次被访问的时间
};

数据结构类型\编码类型

redis-object_encoding.jpg

数据类型: 上面的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

redis-linkedlist.jpg

  • zipList:特殊编码的连续内存块组成的顺序型数据结构=>类似数组
  • quickList:快速链表是 zipList 和 linkedList 的混合体。它将linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。

redis-quicklist.jpg

连表的实现一搬就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处理的过程

redis-hashtable.jpg

  1. rehash扩容实现方式: 2个hash表

Redis采用了渐进式rehash的方案。它会同时保留两个新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿(单线程)现象rehashidx记录扩容的进度,渐进完成数据迁移

  1. 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;

redis-skiplist.jpg

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)

复杂度分析

  1. get/insert/delete复杂度约lgN(level约等于lgN);复杂度约等于lgN+当前level节点个数
  2. 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号库

redis-redisdb.jpeg

过期key的删除

  • 惰性删除: get操作的检查ttl,超时返回nil;
  • 定期删除: redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除

内存淘汰策略

当redis使用内存达到最大配置内存|或者内存不足时,触发的内存淘汰算法;

maxmemory 100mb
maxmemory-policy volatile-lru #默认是noeviction,即不进行数据淘汰,推荐volatile-lru

redis内有2个hash,1个存值,1个存ttl

  1. volatile-lru:从设置expire的key集合中LRU
  2. allkeys-lru:从所有的key集合中做LRU
  3. volatile-random:从设置expire的key集合中random
  4. allkeys-random:从所有key集random
  5. volatile-ttl:从设置expire的key中优先回收存活时间(TTL)较短的键
  6. 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

集群

集群主要考虑的问题:

  1. 高可用(ha)
    1. 主从切换
    2. 主从监控(failover)
  2. 可扩展
    1. 扩容
    2. 分区

主从复制

www.redis.cn/topics/repl…

slaveof 192.168.1.1 6379

当Slave启动后,会从Master进行一次冷启动数据同步,由Master触发BGSAVE生成RDB文件推送给Slave进行导入,导入完成后Master再将增量数据通过Redis Protocol同步给Slave。之后主从之间的数据便一直以Redis Protocol进行同步

增量|全量同步

  1. master机器不要做rdb,在salve做rdb,aof
  2. 主从结构的拓扑使用链,不使用图(master<-slave<-slave; 未验证)

cow的过程

借助buffer记录操作指令

Sentinel

  1. sentinel-failover

www.redis.cn/topics/sent…

单独进程redis-sentinel;监控主从完成failover;Redis Sentinel需要至少部署3个实例才能形成选举关系(1主2从..)

=>HA(high avaliable)问题,即主从+failover

Cluster

www.redis.cn/topics/clus…

  • 将数据分散到多个节点上(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不命中.

  1. 缓冲中key不存在/过期(看是否造成redis卡顿),会查询数据库,并发量大时数据库存在危险;
  2. 数据没有热备,上线启动数据冲击数据库;
  • 过期时间离散分布,处理好缓存一致性
  • 做好数据预热(热备):一般使用在初次访问预热

穿透场景: key既不命中redis,也不命中mysql

  1. 黑客使用缓存和数据库都不存在的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服务器崩溃,原来应该命中缓存的需求全部请求数据库,并对数据库造成压力

  1. 内存中大面积key失效(ttl)造成卡顿/集群宕机/redis卡顿/redis热备/

解决

  1. 集群+HA:redis集群健壮(cluster),主从切换+哨兵sentinel
  2. 监控:做好缓存的监控,主从切换
  3. 内存策略:合适的内存淘汰测试,key过期删除机制
  4. redis做好持久化工作,便于数据恢复,宕机重启(AOF+RDB)
  5. 关键业务的限流/缓存分级/

RateLimiter源码实现

缓存-数据库双写时的一致性

  1. 编码维护的 spring-cache提供的模式(最经典的缓存+数据库读写的模式);就是 Cache-Aside Pattern;
  • 的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存
  • 但是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集群的解决方案:

  1. redis-cluster基于hashlot算法实现分片
  2. codis基于代理实现,现在支持3.0

ps