Redis基础

# 使用场景

  • 高性能

    提高后续请求的响应时间

  • 高并发

    分流请求,将大部分请求路由至缓存处理

# 数据类型

# String

# 数据要求

键值可以为任何数据(最大为512M)【如jpeg图像或序列化的对象】

# 常用操作
# 增删改查
> set <key> <value>
OK

> get <key>
<value> / (nil)

> del <key>
(Integer) 1/(Integer) 0

> mset <key1> <value1> <key2> <value2> ...
> mget <key1> <key2>
# 字符串操作
> strlen <key>
> append <key>
> getrange <key> <pos1> <pos2> # 双闭区间截取(java中对应方法为开闭截取)
# 特殊操作
  • 数值自增

    > incr <key>
    > incrby <key> <increment>
    > incrbyfloat <key> <increment>
    

    若数据不能转换为数值,或运算结果超出Long.MAX_VALUE,则报错

  • 设置数据生命周期

> setex <key> <ttl> <val>
> psetex <key> <ttl> <val>

java.String.substring()方法为左闭右开区间截取

# 应用场景

多用于数据整体存取

  • 存储结构型或非结构型热点数据

    数据key命名规范:

    1. set <表名>:<主键名>:<主键值> <多字段JSON对象>
    2. set <表名>:<主键名>:<主键值>:<字段名> <字段值>(性能较优)
  • 用作RDBMS分表主键生成策略(分批生成,无需考虑并发问题)

# Hash

# 数据要求

底层为哈希表(field-value),哈希值只能为字符串

# 常用操作
# 增删改查
> hset <key> <field> <value>
(Integer) 1 / (Integer) 0 # 1表示field数目变化,0表示不变(不论失败与否)

> hget <key> <field>
<value> / (nil)

> hdel <key> <field1> [field2] ...
(Integer) 1 / (Integer) 0

> hgetall <key>

> hmset <key> <field1> <value1> <field2> <value2> ...
> hmget <key> <field1> <field2> ...
# 集合操作
> hlen <key>
(Integer) x
> hexists <key> <field>
(Integer) 1 / (Integer) 0

> hkeys <key>

> hvals <key>

> hsetnx <key> <field> <value>
(Integer) 1 / (Integer) 0 # 1表示成功,0表示失败
# 特殊操作
  • 数值自增
> hincrby <key> <field> <increment>
> hincrbyfloat <key> <field> <increment>
# 注意事项
  • 相比于String操作,内存与CPU消耗更低
  • Hash中每个key最多可以存储2^32-1个键值对,不可将Hash作为对象列表滥用
  • 当Hash中key未达到阈值或单个元素较小时,底层为Ziplist,按插入顺序进行紧密存储;若Hash中key过多,底层转换为哈希,数据改为无序排列,遍历所有field的效率很低,此时应将key进行分片存储
# 应用场景

多用于同类数据的归类整合与数据的局部更新

  • 存储购物车数据

    以用户id为key,针对每位用户创建Hash,并以商品id作为field,商品加车数量作为value进行存储

    同时额外维护商品信息Hash进行解耦,使用setnx更新商品信息

  • 存储抢购、限购时的商品数量

    以商家id为key,商品id作为field,商品剩余数量作为value进行存储

    超卖问题

# List

# 数据要求

底层为双向链表,元素必须为字符串;可实现双端队列操作

# 常用操作
# 增删改查
> lpush <key> <value1> [<value2> ...]
(Integer) x
> rpush <key> <value1> [<value2> ...]
(Integer) x

> lrange <key> <start> <end> # end为-1时遍历全部元素
> lindex <key> <index>
<value> / (nil)

> lpop <key>
<value> / (nil)
> rpop <key>
<value> / (nil)

> lrem <key> <count> <value>
(Integer) x
# 集合操作
> llen <key>
(Integer) x
# 特殊操作
  • 阻塞式出列
> blpop <key> [<key2> ...] <timeout>
> brpop <key> [<key2> ...] <timeout>

阻塞轮询场景下的异常处理

  1. brpoplpush将目标list中数据出队,并加入备份list
  2. 发生异常时,blpoprpush将备份list中数据还原回目标list;不发生异常时,blpop移除备份list中数据
# 注意事项
  • 作为链表,数据读取速度较慢
  • 每个key最多可以存储2^32-1个元素
# 应用场景
  • 存储朋友圈点赞顺序、社交网站关注列表信息等

    使用rpush添加点赞,lrem移除点赞,lrange遍历点赞名单,lindex获取点赞状态(用户是否已点赞)

  • 存储信息聚合类网站待推送博文等热点信息

    普通博文

    (对于每一关注者)使用rpush添加待推送博文,lrange遍历待推送博文

    大V博文

  • 存储多台服务器先后输出的日志等严格区分操作顺序的数据信息(可持久化

# Set

# 数据要求

底层为value值为空的Hash(Map<String, null>);查找元素复杂度为O(1)

# 常用操作
# 增删改查
> sadd <key> <member1> [<member2> ...]
(Integer) 1 / (Integer) 0 # 1表示元素数目变化,0表示不变(不论失败与否)

> smembers <key>

> srem <key> <member1> [<member2> ...]

# 集合操作
> scard <key>
(Integer) x
> sismember <key> <member>
(Integer) 1 / (Integer) 0

特殊操作

  • 移动元素

    > smove <src> <dest> <member>
    
  • 随机获取元素

    > srandmember <key> [<count>]
    
    > spop <key> [<count>]
    
    
  • 交并差集

    > sinter <key1> <key2> ...
    > sunion <key1> <key2> ...
    > sdiff <key1> <key2> ... # 具有方向性
    
    > sinterstore <dest> <key1> <key2> ...
    > sunionstore <dest> <key1> <key2> ...
    > sdiffstore <dest> <key1> <key2> ...
    
# 注意事项
  • 无法插入重复元素
# 应用场景
  • 存储资讯或新媒体类网站用户兴趣等随机推荐信息

    系统将热门分类信息存入Set,随机挑选出部分分类结合用户已关注信息合并推送给用户

  • 存储同类信息的关联搜索与深度关联搜索等

    共同关注:sinter

    我关注的人也关注TA:sismember(大V场景优化)

    可能关注:sdiff

  • 用于同类型全局数据的快速去重(如网站UV/IP访问量)与快速定位(如网站黑白名单)

  • 存储抽奖用户信息

    sadd添加抽奖用户,srandmember/spop进行抽奖

# ZSet

# 数据要求

底层为额外添加score字段的Set;查找元素复杂度为O(1)

# 常用操作
# 增删改查
> zadd <key> <score> <member> [<score2> <member2> ...] # score值在前,value值在后

> zrange <key> <start> <end> [WITHSCORES] # 按照score值从小到大进行排序
> zrevrange <key> <start> <end> [WITHSCORES]

> zrem <key> <member> [<member2> ...]

> zpopmin <key> [<count>]
> zpopmax <key> [<count>]
# 集合操作
> zcard <key>
> zcount <key> <min> <max>

> sinterstore <dest> <numkeys> <key1> <key2> ... [AGGREGATE SUM|MIN|MAX] # AGGREGATE参数表示交并差集操作后对符合条件元素score值的操作
> sunionstore <dest> <numkeys> <key1> <key2> ... [AGGREGATE SUM|MIN|MAX]
> sdiffstore <dest> <numkeys> <key1> <key2> ... [AGGREGATE SUM|MIN|MAX]
# 特殊操作
  • 根据score操作
> zrank <key>
> zrevrank <key>

> zscore <key> <member>

> zincrby <key> <increment> <member>

> zrangebyscore <key> <min> <max> [WITHSCORES] [LIMIT <offset> <count>]
> zrevrangebyscore <key> <max> <min> [WITHSCORES] [LIMIT <offset> <count>]

> zremrangebyrank <key> <start> <end> [WITHSCORES]
> zremrangebyscore <key> <min> <max> [WITHSCORES]
# 注意事项
  • 能够插入相同元素,并导致该元素score值更新(返回(Integer) 0说明元素数目不变)
  • score值作为整数,上限为2^64-1;作为浮点数,可能会有精度丢失
# 应用场景
  • 存储排行榜(如热门直播、好友亲密度、热销商品、微博热搜等)数据并进行组合统计(可持久化

  • 存储基础+增值类服务(如云盘、游戏或观影VIP等)的用户会员(体验会员)信息,或限时服务(如投票、私密讨论、限期优惠券等)信息)

    • 限时服务:将过期时间作为score,利用排序功能区分业务处理(如中止服务)的先后顺序,同时过期服务进行移除,并按照不同时间段逐级提升(如将一小时内即将逾期的服务加入ZSet进行处理,全部处理完后再加入下一小时即将逾期的服务)(可持久化
    • 限次服务(无需按服务排序):使用String,通过setex uid:appid 60 1初始化使用次数(如60s内使用x次),若get次数达到x次上限,则停止提供服务,特定时间段过后key过期,重新初始化计数(无需持久化
  • 基本延时消息队列(将消息发布时间戳作为score)

# 其他应用场景

  • 存储社交类应用所接收的用户消息等基于时间顺序的数据信息(可持久化

    先判断该消息是否在置顶Set中,选择置顶List普通List,先移除List中的相同key,再重新插入;也可使用ZSet,score值维护消息最后发送的时刻

同时使用String或Hash计数器另行维护消息数目,与List进行同步更新

# Bitmap

应用场景

  • 统计日活用户等海量数据

setbit 2020:1021 1 1记录id(id可进行偏移)为1的用户登录;bitcount 2020:1021 0 -1统计日活用户量(O(n))

# 通用命令

# 基本操作
# key相关
> type <key>
> exists <key>
> del <key>

> keys <pattern>

> rename <key> <newKey>
> renamenx <key> <newKey>

> sort <key>
# 数据库相关
> select <dbIndex>

> move <key> <dbIndex> # 剪切操作
# 扩展操作
# Key时效性控制
> expire <key> <seconds>
> pexpire <key> <milliseconds>
> expireat <key> <timestamp>
> pexpireat <key> <ptimestamp>

> ttl <key> # -2表示已过期,-1表示永不过期
> pttl <key>

> persist <key>
(Integer) 1 / (Integer) 0

# 持久化

bgsave原理

  • fork
  • cow

# 事务

> MULTI
> EXEC/DISCARD

> WATCH

#

# 读操作

  • # 缓存穿透

    策略:缓存空值

  • # 缓存雪崩

    策略:为缓存key过期时间添加额外的随机增量

  • # 缓存击穿

    • 本地锁(❌)

      根据缓存命中结果决定是否进入同步代码块,且在同步代码块头部再次判断缓存是否命中(双重检查);在退出同步代码块前写入缓存,保证在下一线程进入同步代码块时缓存已同步

    • # 分布式锁

      > SET LOCK_KEY 1 PX <MILLISECONDS> NX
      # 如果成功获取锁
      > ... # 执行业务操作
      > DEL LOCK_KEY # 释放锁
      

      过期时间一般为:业务最大耗时✖️120%+平均网络耗时✖️110%(有隐患)

      改进

      释放锁之前判断锁是否过期,同时是否是自己所设置的锁(如LOCK_KEY值为UUID),通过lua脚本保证该流程为原子操作

      if redis.call('get', KEYS[1]) == ARGV[1]
      then
        return redis.call('del', KEYS[1])
      else
        return 0
      end
      

# 写操作

  • # 分布式锁

  1. 互斥/排他性:在任意时刻,只有一个客户端能持有锁(setnx
  2. 不会发生死锁:即使有客户端在持锁期间由于崩溃而没有主动解锁,也能保证后续客户端加锁成功(主动释放;过期自动释放)
  3. 容错性:只要大部分的Redis节点正常运行,客户端就能正常加锁和解锁
  4. 解铃还须系铃人:加锁和解锁过程必须由同一个客户端执行,客户端A不能对客户端B所加的锁进行解锁(将value赋值为requestId,代表加锁客户端的唯一请求标识)
# Redisson

底层通过lua脚本实现所有操作的原子性

  • RLock(可重入锁)

    RLock.lock(超时时间)

    • 锁的自动续期

      若不指定锁对象的超时时间,则使用默认超时时间lockWatchdogTimeout为30s;一旦占锁成功,启动定时任务每10s(internalLockLeaseTime)执行一次续期脚本

    • RReadWriteLock(互斥锁/排他锁与共享锁)

      保证读操作能获取最新数据

      • 并发读:同时获取读锁
      • 并发写/并发读写(读-写或写-读):阻塞等待前一线程操作(占有读锁或写锁)成功
    • RSemaphore

      限量操作(通过对锁对象值的增减进行控制)

      实现分布式限流

      // RSemaphore s;
      if (s.tryAcquire()) {
        // ...
        // s.release();
      } else {
      	return "busy";
      }
      
    • RCountDownLatch

    主从/集群架构故障转移时的锁失效问题

    • Redlock多写
  • # 缓存一致性

    通用策略:根据数据时效性合理设置缓存过期时间,使所有的读操作最终以数据库为准(最终一致性)

    • # 双写(弃用)

      数据更新时先写数据库再覆盖缓存(数据查询时,操作步骤均为先读数据库再写入缓存)

      由于双写操作非原子操作,可能导致先更新DB的线程后更新缓存,从而造成缓存不一致

      注意事项:

      • 双写过程加锁
    • # Cache Aside(后删)

      数据更新时先写数据库再删缓存

      注意事项:

      • 后删过程加锁
      • 若写场景较多,且写入缓存值需经过计算获取,则可使用Cache Aside模式
      • 若第二次未能成功删除缓存导致缓存不一致:
        • 将删除失败的缓存key或**MySQL binlog日志数据(借助Canal)**存至MQ以进行延时重试
    • # 延迟双删
      public void write(String key, Object data){
      	redis.del(key);
      	db.update(data);
      	Thread.sleep(1000);
      	redis.del(key);
      }
      

      数据更新时先淘汰缓存后写数据库再延迟(~1s)删缓存

      注意事项:

      • 第一次删除缓存目的:❓
      • 第二次延时删除缓存的目的:确保该时刻其他线程的写缓存操作已经结束,删除缓存后不会再有其他线程写入脏数据
      • 主备分离场景下,延时时间需由主备同步延时时间决定
      • 第二次删除缓存时开辟新线程进行异步操作,提高工作线程(写线程)吞吐量
      • 同样存在Cache Aside模式中淘汰缓存失败所造成的缓存不一致问题
    • # 操作串行化
      • 写操作:先删缓存,再将更新数据库的操作放入有序队列
      • 读操作:若缓存未命中,读操作也放入有序队列

      若读请求大量积压:

      • 将任务队列水平拆分,提高并行度
        • 使用适当的Hash一致性算法保证相同请求路由至相同队列中
      • 合理配置限流与降级策略

    缓存一致性场景分类

    1. 低并发操作
      1. 基础数据(菜单分类等):时效容忍度高,可设置较长的缓存过期时间等待数据稳定;也可借助Canal订阅MySQL binlog方式实现缓存数据同步
      2. 用户纬度数据(订单数据、用户信息数据等):并发低,可设置合理的缓存过期时间等待数据稳定
    2. 高并发操作

# 删除策略与淘汰算法

# 删除策略

  • 定时删除策略
  • 惰性删除策略:获取数据时若发现该数据已过期则进行删除
  • 定期删除策略:周期性(每隔100ms)随机抽查存储空间(随机、重点抽查

可能导致缓存雪崩,需灵活配置不同数据的过期时间

# 淘汰/逐出算法

# 最大可使用内存(60%~70%)
> maxmemory
# 每次随机获取待删除数据的个数
> maxmemory-samples
# 待删除数据的逐出算法
> maxmemory-policy
# maxmemory-policy
  • 检测易失数据
    • volatile-lru
    • volatile-lfu
    • volatile-ttl
    • volatile-random
  • 检测全表数据
    • allkeys-lru
    • allkeys-lfu
    • allkeys-random
  • 禁用
    • no-eviction

通过INFO信息中的keyspace-hitskeyspace-misses属性值判断当前Redis服务的性能,以选择合适的调优策略

# 高可用

# 主从

# 工作流程
  1. 建立连接

    1. slave保存master的ip与端口信息(slaveof <ip> <port>
    2. slave根据master信息创建连接master的socket,定时发送ping指令
    3. master保存slave的端口信息(replconf listening-port <port>
  2. 数据同步

    1. slave向master发送psync2 ? -1请求数据同步

    2. master执行bgsave尝试建立快照,同时建立存储增量数据的复制积压缓冲区

      master发送+FULLRESYNC <runid> <offset>,并将RDB文件通过socket发送给slave

    3. slave接收RDB文件和runid以及offset信息,清空本地数据并基于RDB执行数据恢复

      slave通过psync2 <runid> <offset>告知master数据同步完成(全量复制

    4. (若runid与offset核验通过)master将复制缓冲区中指令(aof指令逐字符地排布)以及更新后的offset(+CONTINUE offset)发送给slave

      (若runid与offset核验不通过)重新开始全量复制

    5. slave接收aof指令,保存新offset,并执行bgrewriteaof进行增量数据恢复

      slave告知master增量数据的运行id与offset,若master核验通过,则本次增量同步完(增量复制

    • 通过repl-backlog-size指令合理控制复制缓冲区大小,避免全量复制时间过长时复制缓冲区溢出导致的数据丢失(此时再次执行全量复制,产生死循环)

      最优复制缓冲区大小 = 2 * m-s平均重连时长 * m平均每秒写容量

    • 通过slave-serve-stale-data no使slave端只提供有限的写数据功能(不对外提供数据服务)

  3. 命令传播

    1. master定时向slave进行心跳检测
    2. slave向master发送REPLCONF ACK响应
    • INFO REPLICATION信息中的slave0等字段中维护距离上一个心跳包的时间间隔,正常值为1(或0)

    • 命令传播阶段的配置项

      • min-slaves-to-write <num>
      • min-slaves-max-lag <time>

      成功响应心跳的slave个数小于num,或所有slave的延迟都大于time时,强制关闭master的写功能,停止数据同步

      • repl-timeout

      若slave在给定时间没有响应,则master将其释放

      • repl-ping-slave-period

      心跳检测时间间隔(一般为上一项超时时间的1/10~1/5)

# 哨兵

  1. 监控(同步信息)
    1. 连接master获取info
    2. 进行cmd连接以进行后续的命令交换
    3. 根据info信息连接关联的slave或sentinel
    4. 在同时存在的多个sentinel节点间组建发布订阅网络
  2. 通知
  3. 故障转移
    1. 发现问题
    2. 竞选负责人
    3. 负责人优选新master
    4. 切换各节点主从关系

# 集群

  • Redis集群中设置16384个哈希槽,集群中的每个节点负责一部分hash slots
  • 存储key时,使用该key的CRC16值对16384取模以获取目标存储槽位
  • 节点间槽位可互相转移(并行转移),使得集群拓展性高

集群主从模式

Redis集群主从节点之间采用异步复制共享数据,因此不能保证强一致性,可能导致数据丢失

# 底层原理

# 线程模型

# File Event Handler(单Reactor单线程模型)

单线程内存操作:IO多路复用程序(采用单线程、非阻塞地)同时监听多个Socket,将读入的Socket事件放入队列中,由dispatcher依次分配对应的handler

  • 多个Socket
  • IO多路复用程序(事件分离器+IO处理器)
  • 事件分派器
  • 事件处理器
    • 连接应答处理器
    • 命令请求处理器
    • 命令回复处理器