Go To My HomePage

Redis使用详解

一、NoSQL简介

NoSQL分类

  • 键值(Key-Value)存储数据库:这一类数据库主要会使用到一个hash表,如Redis、Oracle BDB
  • 列存储数据库:通常是用来应对分布式存储的海量数据,键仍然存在但是他们指向了多个列,如HBase、Riak
  • 文档型数据库:该类型的数据模型是版本化的文档,比如JSON,允许之间进行嵌套,如MongoDB

非关系型数据库特点

  • 数据模型比较简单

  • 对于数据库性能要求较高

  • 不需要高度的数据一致性

二、Redis简介

以key-value形式存储,不一定遵循传统数据库的一些基本要求(非关系的、分布式的、开源的、水平可扩展的)

优点:对数据高并发读写

​ 对海量数据的高效率存储和访问

​ 对数据的可扩展性和高可用性

缺点:无法做太复杂的关系模型

Redis单线程:指处理我们的网络请求的时候只有一个线程来处理【文件刷盘等用的是多线程】

Redis单线程的好处

  • Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽,单线程实现比较简单
  • 单线程避免了不必要的上下文切换和竞争条件以及加锁释放锁操作

  • 使用多路I/O复用模型,非阻塞IO

三、Redis的安装

第一步:准备工作【解压tar包,创建Redis相关目录】

tar -zxvf redis-5.0.2.tar.gz 
mkdir /opt/redis
mkdir /opt/redis/conf
mkdir /opt/redis/data

第二步:编译redis

#进入解压后的tar包执行
make 
#执行结束之后进入src目录
cd src
make install PREFIX=/opt/redis

第三步:移动配置文件到conf目录

cp redis.conf /opt/redis/conf

额外配置:

  • redis开机自启
vim /etc/rc.local
#加入
/opt/redis/bin/redis-server /opt/redis/conf/redis.conf
  • 配置数据保存目录
vim /opt/redis/conf/redis.conf
#修改
dir ./    --->    dir /opt/redis/data/

四、Redis基本通用命令

命令 说明
KEYS [pattern] 查找出匹配的key【生成环境禁止使用,数据量太大阻塞生成环境】
DBSIZE 统计key总数【使用的是redis的内部计数,并不是全部扫描,生产可用】
EXISTS key [key …] 检查key是否存在【存在返回1,不存在返回0】
DEL key [key…] 删除key【成功删除返回1,不存在此key返回0】
EXPIRE key seconds 设置过期时间
TTL key 查看剩余的过期时间【-2代表已不存在,-1代表永不过期】
PERSIST key 取消key的过期设置
TYPE key 查询key的类型

五、Redis数据类型及其使用

注意:redis操作下标都是闭区间的

字符串【String】

String的值类型可以为字符类型、数字类型、bit类型

String类型是包含很多中类型的特殊类型,并且是二进制安全的。比如序列化的对象进行存储,比如一张图片进行二进制存储,比如一个简单地字符串,数值等等。

使用场景:缓存、计数器、分布式锁等

常用命令格式 描述
{ GET key },{ MGET key [key …] } 得到String
{ SET key value },{ MSET key value [key value …] } 设置String
{ SETNX key value },{ MSETNX key value [key value …] } 如果不存在则设置
SET key value XX 如果存在则设置
SETEX key second value 设置过期时间
{ INCR key },{ INCRBY key increment } 自增操作
{ DECR key },{ DECRBY key decrement } 自减操作
GETSET key value 得到就值设置新值
APPEND key value 追加字符串
STRLEN key 得到字符串长度,内部存有计数
GETRANGE key start end 得到指定长度的value
SETRANGE key offset value 设置指定偏移量字符串内容

哈希【Hash】

使用场景:存储具有一定结构化的数据

常用命令格式 描述
HGET key field 得到key对应field的value
HSET key field value 设置key对应field的value
HDEL key field 删除key对应field的value
HEXISTS key field 判断key的field是否存在
HLEN key 获取指定key的filed数量【内部计数,生成环境可用】
HMGET key field [field …] 批量获得hash的field对应的value
HMSET key field value [field value …] 批量设置hash的field和value
HINCRBY key field increment 增加指定increment的对应key的field
HGETALL key 获取key对应所有field和value【生成环境慎用】
HVALS key 返回key对应的所有value【生成环境慎用】
HKEYS key 返回key对应的所有field【生成环境慎用】
HSETNX key field value 不存在此key对应的field则设置

列表【List】

列表为有序、可重复结构。可指定位置插入和删除、也可从左右插入和弹出(模拟栈结构)

常用命令格式 描述
LPUSH|RPUSH key value [value …] 从列表的左|右插入元素
LINSERT key BEFORE|AFTER value newValue 在list指定的值前|后插入新元素【时间复杂度O(N)】
LPOP|RPOP key 从列表左侧|右侧弹出一个元素
LREM key count value 根据count值,从列表删除所有等于value的值【时间复杂度O(N)】
【count>0,从左到右删除count个】
【count<0,从右到坐删除count个】
【count=0,删除所有value相等的值】
LTRIM key start end 按照索引范围保留list,删除大链表有用【时间复杂度O(N)】
LRANGR key start end 获取列表指定索引范围内的值,数值为负则从右往左取值【时间复杂度O(N)】
LINDEX key index 获取列表指定索引的值,数值为负则从右取值【时间复杂度O(N)】
LLEN key 获取list长度【内部计数,生成环境可用】
LSET key index newValue 设置指定位置的值【时间复杂度O(N)】
BLPOP|RLPOP key timeout LPOP/RPOP的阻塞版,timeout为0则表示永不阻塞

使用技巧:

  • LPUSH + LPOP = Stack
  • LPUSH + RPOP = Queue
  • LPUSH + LTRIM = Capped Collection【固定容量集合】
  • LPUSH + BRPOP = Block Queue

集合【Set】

Set无序、无重复、有集合间操作。

常用命令格式 描述
SADD key element [member …] 向集合key添加元素
SREM key element 删除集合key中的element元素
SCARD key 查询集合元素的个数【内部计数,生成环境可用】
SISMEMBER key element 集合中是否存在element元素
SRANDMEMBER key 随机得到一个元素
SMEMBERS key 获取集合所有元素【慎用】
SPOP key 随机弹出一个元素
{ SDIFF key [key …] }、{ SDIFFSTORE destination key [key …] } 返回/存储一个集合的全部成员,该集合是所有给定集合之间的差集
{ SINTER key [key …] }、{ SINTERSTORE destination key [key …] } 返回/存储一个集合的全部成员,该集合是所有给定集合的交集
{ SUNION key [key …] }、{ SUNIONSTORE destination key [key …] } 返回/存储一个集合的全部成员,该集合是所有给定集合的并集

使用技巧

  • SADD = Tagging

  • SPOP/SRANDMEMBER = Random Item

  • SADD + SINTER = Social Graph【共同关注、有着相同兴趣等】

有序集合【ZSet】

ZSet有序、无重复、包含分值与元素,有集合间操作。

使用场景:排行榜

常用命令格式 描述
ZADD key score member [score member …] 向集合添加元素
ZREM key member [member …] 移除集合中的元素
ZSCORE key member 得到元素的分数
ZINCRBY key increment member 增长元素的分数
ZCARD key 获得集合中元素的个数
ZRANK key member 成员按分值递减(从小到大)排列的排名
ZRANGE key start stop [WITHSCORES] 按score值递增(从小到大)排序,WITHSCORES返回分数
ZRANGEBYSCORE key max min [WITHSCORES] 返回分数范围内数据,按score值递增(从小到大)排序,WITHSCORES返回分数
ZCOUNT key min max 统计得到分数在min和max之间的元素个数
{ ZREMRANGEBYRANK key start stop }、{ ZREMRANGEBYSCORE key min max} 按照排名/分数范围删除元素
ZREVRANK key member 成员按分值递减(从大到小)排列的排名
ZREVRANGE key start stop [WITHSCORES] 按score值递增(从大到小)排序,WITHSCORES返回分数
ZREVRANGEBYSCORE key max min [WITHSCORES] 返回分数范围内数据,按score值递增(从大到小)排序,WITHSCORES返回分数

六、Redis高级功能

慢查询日志

Redis-Life-Cycle

  • 慢查询发生在第三阶段
  • 客户端超时不一定是慢查询导致,但慢查询可导致客户端超时

慢查询队列简介

  • 先进先出的队列
  • 固定长度
  • 长度不够时,丢弃最早记录
  • 保存在内存中

慢查询设置

  • slow-max-len【慢查询队列长度,默认128】

  • slowlog-log-slower-than【慢查询的阙值(微秒,1000000 微秒=1 秒),默认10000(10毫秒),建议为1000(1毫秒)】

更改慢查询参数建议使用CONFIG SET parameter value方式,而不是更改redis.conf文件重启redis

慢查询命令

常用命令格式 描述
slowlog get n 获取慢查询队列一条记录
slowlog len 获取慢查询队列长度
slowlog reset 清空慢查询队列

查询结果示例

Redis-Slow-Examples

Pipeline

Redis-Process

一般情况下客户端发送一条命令到Redis,Redis处理结束返回结果,即 N条命令 = N次网络时间 + N次计算时间

PipeLine将命令打包发送给客户端,Redis处理完返回结果,PipLine是一个异步处理方式,并不等待Redis返回。

Pipeline(N条命令) = 1次网络时间 + N次计算时间

/*
 * Jest执行参考
 */
long oldPipeline = Instant.now().toEpochMilli();
for (int i = 0; i < 100; i++) {
    Pipeline pipeline = jedis.pipelined();
    for (int j = 0; j < 100; j++) {
        pipeline.sadd("S" + i * 100 + j, j + "");
    }
    //pipeline.sync();异步不接受返回结果
    pipeline.syncAndReturnAll(); //异步接受返回结果
}
long nowPipeline = Instant.now().toEpochMilli();
System.out.println(nowPipeline - oldPipeline);

/**
  * Spring Data Redis参考
  */
// 批量插入
redisTemplate.executePipelined(new RedisCallback<Void>() {
  @Override
  public Void doInRedis(RedisConnection connection) throws DataAccessException {
    String keyPrefix = "pipeline-";
    for (int i = 0; i < 10000; i++) {
      String key = keyPrefix + i;
      connection.set(key.getBytes(), String.valueOf(i).getBytes());
    }
  }
});

// 批量获取
List<Object> data = redisTemplate.executePipelined(new RedisCallback<String>() {
  @Override
  public String doInRedis(RedisConnection connection) throws DataAccessException {
    String keyPrefix = "pipeline-";
    for (int i = 0; i < 10000; i++) {
      String key = keyPrefix + i;
      connection.get(key.getBytes());
    }
  }
});

data.stream().forEach(System.out::println);

发布订阅

常用命令格式 描述
PUBLISH channel message 将信息 message 发送到指定的频道 channel
SUBSCRIBE channel [channel …] 订阅给定的一个或多个频道的信息
PSUBSCRIBE pattern [pattern …] 订阅一个或多个符合给定pattern的频道
UNSUBSCRIBE [channel [channel …]] 取消订阅

Bitmap

常用命令格式 描述
SETBIT key offset value 对 key所储存的字符串值,设置或清除指定偏移量上的位(bit)
GETBIT key offset 对 key所储存的字符串值,获取指定偏移量上的位(bit)
BITCOUNT key [start] [end] 计算给定字符串中,被设置为 1的比特位的数量
BITOP operation destkey key [key …] 对一个或多个保存二进制位的字符串 key进行位元操作,并将结果保存到 destkey上
operation 可以是 ANDORNOTXOR 这四种操作中的任意一种
BITPOS key bit [start] [end] 返回位图中第一个值为 bit的二进制位的位置

HyperLogLog

实质:用String类型实现,不能取出具体值,有错误率

作用:极小的空间实现独立数量统计

常用命令格式 描述
PFADD key element [element …] 将任意数量的元素添加到指定的HyperLogLog
PFCOUNT key [key …] 计算HyperLogLog有多少值
PFMERGE destkey sourcekey [sourcekey …] 将多个HyperLogLog合并为一个HyperLogLog

Geo

Geo(地理信息定位):存储经纬度,计算两地距离,范围计算等

Geo是使用ZSet实现

常用命令格式 描述
GEOADD key longitude latitude member [longitude latitude member …] longitude:经度,latitude:维度,member:标识
GEOPOS key member [member …] 返回经纬度
GEODIST key member1 member2 [unit] 返回两个给定位置之间的距离
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count] 返回指定范围内的数据
ZREM key member 删除成员

七、Redis持久化

RDB【Redis Database】

​ RDB是Redis用来进行持久化的一种方式,是把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里。

触发机制

  • save

    手动触发,同步命令,会阻塞线程

  • bgsave

    手动触发,fork出一个子进程,异步命令,不会阻塞线程【阻塞仅仅会发生在fork出子进程的阶段】

  • 自动

    save 900 1            #900秒改变1个就生成rdb文件
    save 300 10           #300秒改变10个就生成rdb文件
    save 60  10000        #60秒改变10000个就生成rdb文件
    

    一般情况下建议关闭自动策略

  • 全量复制

    从节点执行全量复制操作的时候,主节点会自动触发bgsave命令生存rdb文件并发送给从节点

  • debug reload

    在执行debug reload重新加载redis的时候,也会自动触发bgsave

  • shutdown

    默认情况下执行shutdown命令,如果没有开启AOF持久化功能,就会自动执行bgsave

RDB配置参数

命令格式 描述
rdbcompression 压缩RDB文件,默认yes
rdbchecksum RDB文件是否进行校验,默认yes
dbfilename dump.rdb RDB文件名【可使用dump-端口号.rdb区分不同的redis实例】
dir ./ RDB文件存储的目录
stop-writes-on-bgsave-error bgsave出现错误时是否停止写入,默认yes

AOF【Append-Only File】

​ AOF是一个追加写入的日志文件从而实现持久化的方式,生成的AOF文件是可识别的纯文本文件。Redis默认使用RDB持久,开启AOF持久化需要设置appendonlyyes

AOF文件生成策略

  • always 不丢失数据,每次更新记录数据就进行io操作

  • everysec 可能会丢失1s数据,但io小

  • no 不启用AOF

AOF重写

​ AOF支持AOF文件重写(从内存中读取的数据,并非读取上次的AOF文件进行重写)。AOF重写可以减少硬盘占用、加速恢复速度。

Aof-Rewrite

AOF配置参数

命令格式 描述
appendonly 是否开启AOF,默认no
appendfilename 生成AOF文件明【可使用appendonly-端口号.aof区分不同的redis实例】
appendfsync 刷盘策略,默认everysecond
dir 保存文件的目录,默认./
no-appendfsync-on-rewrite AOF重写过程中是否禁止append操作,默认no允许append
auto-aof-rewrite-min-size 进行AOF时文件最小尺寸,默认64mb
auto-aof-rewrite-percentage 下次进行AOF操作时的增量,默认100
aof-load-truncated AOF文件结尾不完整,Redis重启忽略不完整记录,默认yes

命令

  • bgrewriteaof 手动进行AOF重写操作
  • aof_current_size 查看AOF当前尺寸

redis-cli info Persistence可以查看统计信息aof_current_size【当前AOF文件大小】和aof_base_size【上次重写AOF文件大小】

RDB与AOF比较

命令 RDB AOF
启动优先级
体积
恢复
数据安全性 丢数据 根据策略决定
轻重

混合持久化

Redis 4.0 开始支持 rdb 和 aof 的混合持久化(默认开启),这样做的好处是可以结合 rdb 和 aof 的优点, 快速加载同时避免丢失过多的数据。缺点 aof 里面的 rdb 部分就是压缩格式不再是 aof 格式,可读性差。

  • 配置开启混合持久化aof-use-rdb-preamble yes

  • 命令开启混合持久化config set aof-use-rdb-preamble yes

开启混合持久化时,aof rewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。aof 文件内容会变成如下

+------------------------+
|                        |   
|                        |   
|          RDB           |   
|         FORMAT         |   
|                        |   
|                        |   
|                        |   
+------------------------+
|                        |   
|        AOF             |   
|       FORMAT           |   
+------------------------+

持久化相关优化

fork操作优化

  • 控制redis实例最大可用内存:maxmemory

  • 合理配置Linux内存分配策略:vm.overcommit_momory=1(默认0,当内存少时fork阻塞不进行)
  • 降低fork频率:例如放宽AOF重写自动触发时机,不必要的全量复制

子进程

  • cpu

    开销:RDB和AOF文件生成,属于CPU密集型

    优化:不做CPU绑定,不和CPU密集型服务部署

  • 内存

    开销:fork内存开销,使用了linux的copy-on-write【父进程未发生改变的内存页,不进行copy-write】

    优化:echo never > /sys/kernel/mm/transparent_hugepage/enabled【不分配大内存页】

  • 硬盘

    开销:AOF和RDB文件写入,可以结合iostat,iotop分析

    优化:不和高硬盘负载服务部署一起,no-appendfsync-on-rewrite设置为yes

八、Redis主从复制原理和优化

  • 一个master可以有多个slave
  • 一个slave只能有一个master
  • 数据流向是单向的,master到slave

主从实现两种方式

  • 在从机上执行slaveof masterIp masterPort,此命令是异步。slaveof no one结束从属关系。

  • 修改redis配置文件

    slaveof masterIp masterPort               #配置主从
    slave-read-only  yes                      #从节点只读
    

主从状态查看info replication

全量复制和部分复制

redis4后使用psync2实现复制使redis重启也可使用部分同步,还为解决在主库故障时候从库切换为主库时候使用部分同步机制。redis从库默认开启复制积压缓冲区功能,以便从库故障切换变化master后,其他落后该从库可以从缓冲区中获取缺少的命令。该过程的实现通过两组replid、offset替换原来的master runid和offset变量实现:

  • 第一组:master_replid和master_repl_offset

    如果redis是主实例,则表示为自己的replid和复制偏移量; 如果redis是从实例,则表示为自己主实例的replid1和同步主实例的复制偏移量。

  • 第二组:master_replid2和second_repl_offset

    无论主从,都表示自己上次主实例repid1和复制偏移量;用于兄弟实例或级联复制,主库故障切换psync

判断是否使用部分复制条件:如果从库提供的master_replid与master的replid不同,且与master的replid2不同,或同步速度快于master; 就必须进行全量复制,否则执行部分复制。

以下常见的主从切换都可以使用部分复制:

  • 一主一从发生切换,A->B 切换变成 B->A
  • 一主多从发生切换,兄弟节点变成父子节点时
  • 级别复制发生切换, A->B->C 切换变成 B->C->A

全量复制开销

  • bgsave时间
  • RDB文件网络时间
  • 从节点清空数据时间
  • 从节点加载RDB时间
  • 可能的AOF重写时间

当从库与主库断开时间过长导致自己的偏移量不在master_repl_offset允许的范围之内,会触发全量复制

主从相关参数配置

##############从库##############

slaveof <masterip> <masterport> 
#设置该数据库为其他数据库的从数据库

masterauth <master-password>
#主从复制中,设置连接master服务器的密码(前提master启用了认证)

slave-serve-stale-data yes
# 当从库同主库失去连接或者复制正在进行,从库有两种运行方式:
# 1) 如果slave-serve-stale-data设置为yes(默认设置),从库会继续相应客户端的请求
# 2) 如果slave-serve-stale-data设置为no,除了INFO和SLAVOF命令之外的任何请求都会返回一个错误"SYNC with master in progress"

slave-priority 100
#当主库发生宕机时候,哨兵会选择优先级最高的一个称为主库,从库优先级配置默认100,数值越小优先级越高

slave-read-only yes
#从节点是否只读;默认yes只读,为了保持数据一致性,应保持默认


##############主库##############

repl-disable-tcp-nodelay no
#在slave和master同步后(发送psync/sync),后续的同步是否设置成TCP_NODELAY假如设置成yes,则redis会合并小的TCP包从而节省带宽,但会增加同步延迟(40ms),造成master与slave数据不一致假如设置成no,则redis master会立即发送同步数据,没有延迟
#前者关注性能,后者关注一致性

repl-ping-slave-period 10
#从库会按照一个时间间隔向主库发送PING命令来判断主服务器是否在线,默认是10秒

repl-backlog-size 1mb
#复制积压缓冲区大小设置

repl-backlog-ttl 3600
#master没有slave一段时间会释放复制缓冲区的内存,repl-backlog-ttl用来设置该时间长度。单位为秒。

min-slaves-to-write 3
min-slaves-max-lag 10
#设置某个时间断内,如果从库数量小于该某个值则不允许主机进行写操作,以上参数表示10秒内如果主库的从节点小于3个,则主库不接受写请求,min-slaves-to-write 0代表关闭此功能。

主从配置问题

maxmomory不一致导致丢失数据

数据结构参数优化只有优化了主机,从机未配置导致内存不一致,数据错误或丢失

九、Redis Sentinel

  • 主观下线:Sentinel根据配置条件,发现redis节点达到故障标准,则此Sentinel认为此redis节点下线
  • 客观下线:当Sentinel中认为此redis客观下线的总数达到配置阙值,则认为此节点客观下线

安装Sentinel

配置sentinel.conf文件

#没有开启bind和密码的情况下,保护模式默认被开启。只接受来自环回IPv4和IPv6地址的连接。拒绝外部连接
bind 127.0.0.1 192.168.1.1
protected-mode no

#端口
port 26379

#是否守护进程模式运行
daemonize no

#pid以及日志文件位置
pidfile /var/run/redis-sentinel.pid
logfile ""

#工作目录
dir /tmp

#监控的master
#mymaster指此主从组的名称【Sentinel可以监控多个主从组】
#最后一个数字代码多少个sentinel主观认为此master宕机为客观宕机事实
sentinel monitor mymaster 127.0.0.1 6379 2

#当多少毫秒master不返回ping结果即认为主观宕机
sentinel down-after-milliseconds mymaster 30000

#master重新选举之后slave并发复制master数据的并发量
sentinel parallel-syncs mymaster 1

#故障转移超时时间,默认为3分钟
sentinel failover-timeout mymaster 180000

#当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会调用这个脚本
#如果脚本以“1”退出,则稍后重试执行(最多可执行10次),脚本的最长运行时间为60秒
#sentinel notification-script mymaster /var/redis/notify.sh

#当master因failover而发生改变,这个脚本将被调用,通知相关的客户端关于master地址已经发生改变的信息
#sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

#不允许使用SENTINEL SET设置notification-script和client-reconfig-script
sentinel deny-scripts-reconfig yes

Sentinel三个定时任务

  • 每10秒每个sentinel对master和slave执行info
    • 发现slave
    • 确认主从关系
  • 每2秒每个sentinel通过master节点的channel交换信息(pub/sub模式)
    • 通过__sentinel__:hello频道交互
    • 交互各节点的“看法”及自身信息
  • 每1秒每个sentinel对其他sentinel和redis节点执行ping
    • 心跳检查、失败依据

Master选举过程

第一步:Sentinel选举出leader

原因:只需要一个Sentinel完成故障转移

选举:通过sentinel is-master-down-by-addr命令都希望自己成为领导者

  • 每个做主观下线的Sentinel节点向其它Sentinel节点发送命令,要求它给自己投票
  • 收到命令的Sentinel节点如果没有同意其它Sentinel节点发送的命令,则同意投票否则拒绝
  • 如果该Sentinel节点发现自己的票数已经超过Sentinel半数,那么它将成为leader
  • 如果此过程未选出leader则等待一段时间继续选举

第二步:故障转移选举Master

  • 从slave节点中选出一个“合适”的节点作为新的master节点
    • 选择slave-prority最高的slave节点,如果存在则返回【一般不修改】
    • 选择复制偏移量最大的slave节点,如果存在则返回
    • 选择runId最小的slave节点
  • 对上面的slave节点执行slaveof no one命令让其成为master节点

  • 向剩余的slave节点发送slaveof命令,让它们成为master节点的slave节点,复制规则和paraller-sync参数有关
  • 更新对原来master节点配置为slave,并保存对其“关注”,当其恢复后命令它去复制新的master节点

手动下线master机器sentinel failover <masterName>

十、Redis Cluster

数据分布概论

Data-Distribution

分布方式 特点 典型产品
哈希分布 数据分散度高
Memcache
Redis Cluster
键值分布业务无关
无法顺序访问
支持批量操作
顺序分布 数据分散度易倾斜
BigTable
HBase
键值分布业务相关
可顺序访问
支持批量操作

哈希分布方式

节点取余分区

Delivery-Partition

进行取模运算,将余数相等的放入同一节点,简单易操作,增加节点时数据偏移,导致数据的前移达到80%,翻倍扩容可以使数据迁移从80%降到50%

一致性hash分区

Consistency-Hash

为系统中每个节点分配一个token,范围一般在0~2的32次方,这些token构成哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点,往往一个节点会对应多个token。加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这些数据,常用于缓存场景

虚拟哈希分区

Slot-Partition

虚拟分槽使用哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。槽数范围远远大于节点数(redisCluster槽的范围是0~16383),每一个节点负责维护一部分槽以及所映射的键值数据

基本架构

cluster-architecture

  • 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽

  • 节点的fail是通过集群中超过半数的master节点检测失效时才生效

  • 客户端与redis节点直连,不需要中间proxy层.客户端连接集群中任何一个可用节点即可

  • redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->key

安装Cluster

修改redis配置文件

cluster-enable yes
cluster-config-file nodes-${port}.conf

原生安装

  • 启动所有节点

  • 执行redis-cli -p ${port} cluster meet ${ip} ${port}使节点相遇

  • 执行cluster addslots slot [slot...]分配槽

    start=$1
    end=$2
    port=$3
    for slot in `seq ${start} ${end}`
    do
      echo "slot:${slot}"
      /opt/redis/bin/redis-cli -p ${port} cluster addslots ${slot}
    done
    
  • 执行redis-cli -p ${port} cluster replicate ${nodeId}执行主从分配

集群命令安装

  • redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

    –cluster-replicas代表集群的每个主节点的从节点个数

    ruby安装已废弃

cluster配置参数

cluster-enable yes
cluster-config-file nodes-${port}.conf
cluster-node-timeout 15000
cluster-require-full-coverage yes 		#必须集群所有节点能提供服务才提供服务

集群伸缩

扩展集群

扩展步骤原理

  1. 对目标节点发送cluster setslot {slot} importing {targetNodeId}命令,让目标节点准备导入槽的数据
  2. 对源节点发送cluster setslot {slot} migrating {sourceNodeId}命令,让源节点准备槽数据的导出
  3. 源节点上循环执行cluster getkeyinslot {slot} count 命令,每次获取属于这个槽中键的个数
  4. 在源节点上执行migrate {sourceIp} {sourcePort} key 0 {timeout}命令,迁移指定的key
  5. 重复执行步骤3~4直到槽下所有数据完成迁移
  6. 向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点

Cluster-Expend

扩展执行步骤

  • 加入集群

    执行redis-cli -p ${port} cluster meet ${ip} ${port}将节点加入集群

  • 设置主从关系

    redis-cli -p ${port} cluster replicate ${nodeId}设置主从关系

  • 任意节点执行迁移槽命令,后续过程根据提示进行

    redis-cli --cluster reshard {ip}:{port}

  • 查看节点分配情况

    redis-cli -p {prot} cluster nodes | grep master

集群缩容

Cluster-Shrink

迁移槽命令和扩展集群的迁移命令相同,迁移完成之后使用redis-cli cluster forget {downNodeId}下线节点

  • 迁移数据

    redis-cli --cluster reshard {ip}:{port} --cluster-from {sourceNodeId} --cluster-to {targetNodeId} --cluster-slots {slotsNum}

  • 下线节点

    redis-cli --cluster del-node {ip}:{port} {shutdownNodeId}

客户端路由

使用cluster keyslot ${key}可查看key对应的hash值

moved重定向

Cluster-Move

ask重定向

Cluster-Ask

smart客户端

实现原理:追求性能

  1. 从集群中选一个可运行节点,使用cluster slots初始化槽和节点映射
  2. 将cluster slots的结果映射到本地缓存,为每个节点创建JedisPool
  3. 准备执行命令(使用CRC16计算key对应的槽,找到映射节点执行)

Smart-Client

public static void main(String[] args) {
    Set<HostAndPort> nodes = new HashSet<HostAndPort>();
    nodes.add(new HostAndPort("192.168.1.158", 7000));
    nodes.add(new HostAndPort("192.168.1.158", 7001));
    nodes.add(new HostAndPort("192.168.1.158", 7002));
    nodes.add(new HostAndPort("192.168.1.158", 7003));
    nodes.add(new HostAndPort("192.168.1.158", 7004));
    nodes.add(new HostAndPort("192.168.1.158", 7005));
    JedisCluster jedisCluster = new JedisCluster(nodes);
    jedisCluster.set("hello", "cluster");
    System.out.println(jedisCluster.get("hello"));
    jedisCluster.close();
}

批量操作

Redis主要提供了以下几种批量操作方式:

  • 批量get/set(multi get/set)
  • 管道(pipelining)
  • 事务(transaction)
  • 基于事务的管道(transaction in pipelining)

批量操作必须key在同一个槽,导致以上用法异常苛刻

方案一:传统的串行IO操作,也就说n个key,分n次串行操作来获取key,复杂度是o(n)

方案二:将mget操作(n个key),利用已知的hash函数算出key对应的节点,这样就可以得到一个这样的关系:Map<node, somekeys>,也就是每个节点对应的一些keys,这样将之前的o(n)的效率降低到o(node.size())

MuliCmd-Serial-Io

方案三:在方案二的基础上将串行取数据改为并行取数据,进一步提高效率

MuliCmd-parell-Io

方案四:通过redis自带的hashtag功能,强制一批key分配到某台机器上【不建议,大量数据会造成数据倾斜】

//使用{user}作为key,使key统一
jedisCluster.mset("{user}1001","zhangsan","{user}1002","lisi","{user}1003","wangwu");
List<String> users = jedisCluster.mget("{user}1001","{user}1002","{user}1003");

故障转移

故障发现

  • 通过ping/pong信息实现故障发现,当半数以上持有槽的主节点都标记某节点主观下线则为客观下线【向集群广播下线节点的fail消息】
  • 客观下线发送通知故障节点的从节点触发故障转义流程

故障恢复

  • 资格审查
    • 每个从节点检查与故障主节点的断线时间
    • 超过cluster-node-timeout * cluster-slave-validity-factor取消资格
    • cluster-slave-validity-factor默认是10
  • 准备选举时间
    • 最接近主节点的偏移量的从节点率先发起选举,稍后其他从节点发起选举
  • 选举投票
    • 收集票数大于N/2+1即为选举成功
  • 替换主节点
    • 当前从节点取消复制变为主节点(slave no one)
    • 执行clusterDelSlot撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽分配给自己
    • 向集群广播自己的pong消息,表明已经替换了故障从节点

集群运维问题

集群完整性

cluster-require-full-coverage yes默认为yes

  • 集群中16384个槽全部可用:保证集群完整性
  • 节点故障或者正在故障转移,集群不可使用

大多数情况下业务无法容忍,建议cluster-require-full-coverage设置为no

PubSub广播

任意节点发布消息所有节点都会订阅到消息,消耗带宽较多。JedisCluster只会订阅任意一个节点

数据倾斜

造成的原因:

  • 节点槽分配不均
  • 不同槽对应的键值数量差异较大【可能存在hashTag】
  • 包含bigkey
  • 内存相关配置不一致

使用redis-cli --cluster info {ip}:{port}可以查看key、slot分布情况

使用redis-cli --cluster rebalance {ip}:{port}进行数据平衡【慎用】

从机读写问题

在集群模式下从节点不接受任何读写请求

  • 命令会重定向到负责槽的主节点
  • readonly命令可以读取【连接级别】

数据迁移

官方工具不只能从单机向集群迁移,不支持断点续传,不支持在线迁移,单线程影响速度,不建议使用官方工具

十一、缓存设计与优化

缓存使用的成本

  • 数据不一致:缓存层和数据层有时间窗口不一致,和更新策略有关
  • 代码维护成本:多了一层缓存逻辑
  • 运维成本:例如Redis Cluster

缓存更新策略

redis里面存储的过期时间,都是绝对时间点,所以如果两台机器时钟不同步,那么超过的数据会全部删除。

  • slaves不会独立删除数据,而是等待master给它发送删除指令的时候,再删除数据
  • 如果slave当选为master的时候,会先淘汰keys,然后再成为master

  • 设置maxmemory-policy值指定算法

    更新策略 解释
    volatile-lru 过期的键使用LRU策略剔除,没有可删除对象则退回到noeviction
    allkeys-lru 所有键均使用LRU策略剔除,直到腾出足够空间
    volatile-lfu 过期的键使用LFU策略剔除,没有可删除对象则退回到noeviction
    allkeys-lfu 所有键均使用LFU策略剔除,直到腾出足够空间
    volatile-random 过期的键使用随机策略剔除,没有可删除对象则退回到noeviction
    allkeys-random 所有键均使用随机策略剔除,直到腾出足够空间
    volatile-ttl 剔除TTL最小的键,没有可删除对象则退回到noeviction
    noeviction 默认策略,不做任何事,返回写错误

    LRU【Least Recently Used】最近最少被使用

    LFU【Least Frequently Used】最不常用

  • 被动更新

    当客户端方位key的时候,主动检测这些key是否过期,过期就删除

  • 主动更新

    每秒检测10次以下操作,测试随机的20个keys进行相关过期检测,删除所有的过期的keys,如果有多于25%的keys过期,重复此操作

策略 一致性 维护成本
算法剔除 最差
被动更新 较差
主动更新

低一致性:最大内存和淘汰策略

高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底

缓存穿透&缓存雪崩&无底洞

缓存穿透

Cache-Miss

特点:

当缓存和数据库中都没有数据的时候,当查询Redis没有数据的时候,会继续查询数据库,数据库也没有数据,当大量查询请求发生或遭到恶意攻击时,这些访问全部透过Redis,并且数据库也没有数据,这种现象称为“缓存穿透”。

解决方案:

  1. 缓存空对象,storage返回一个空对象,将键存储在缓存层,下次请求此键之间返回空对象
    • 需要更多的键,建议设置过期时间
    • 缓存层和存储层数据“短期”不一致
  2. 布隆过滤器

缓存击穿

特点:

当Redis的热点数据key失效时,大量并发查询直接打到数据库,此时数据库负载压力骤增,这种现象称为“缓存击穿”

hotkey-expir

解决方案:

1.设置key值永不过期

2.使用互斥锁,查到后就回填缓存

缓存雪崩

特点:

缓存雪崩是指在短时间内,有大量缓存同时过期,导致大量的请求直接查询数据库,从而对数据库造成了巨大的压力,严重情况下可能会导致数据库宕机的情况叫做缓存雪崩。

crash

解决方案:

  • 随机设置key过期时间
  • 随机延时,让一部分查询先将数据缓存起来
  • 设置key值永不过期

无底洞问题

现象:增加节点机器性能没提升反而下降

Cache-Node-Add

解决方案参考批量操作

热点key的重建优化

Hot-Key

现象:热点key缓存重建过程过长导致浪费了不必要的资源

解决方案:

  1. 互斥锁【使用redis构建锁机制】

    String get(String key) {
        String value = redis.get(key);
        if(value == null) {
            String mutexKey = "mutex:key:"+key;
            if(redis.SetParams.setParams().ex(180).nx()) {
                value = db.get(key);
                redis.set(key,value);
                redis.delete(mutexKey);
            }else {
                Thread.sleep(1000);
                get(key);
            }
        }
        return value;
    }
    
  2. 永不过期

    • 缓存层面:没有设置过期时间
    • 功能层面:为每个value添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程取构建缓存
方案 优点 缺点
互斥锁 思路简单 代码复杂度增加
保证一致性 存在死锁的风险
永不过期 基本杜绝热点key重建问题 不保证一致性
逻辑过期时间增加维护成本和内存成本

十二、Redis布隆过滤器

布隆过滤器的原理

Bloom-Filter

  1. 首先需要k个hash函数,每个函数可以把key散列成为1个整数
  2. 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
  3. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
  4. 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中

优点:不需要存储key,节省空间

缺点:算法判断key在集合中时,有一定的概率key其实不在集合中,且无法删除

google-guava库实现了java版的布隆过滤器

Redis布隆过滤器

使用插件的方式部署

redis4.0之后支持使用插件的方式使用Bloom filters和cuckoo filters,redis4.0之前需手动使用代码的方式编写布隆过滤器,安装步骤参考下列链接

GitHub地址 使用文档

建议在conf配置文件中配置,不使用redis-server --loadmodule /path/to/rebloom.so启动

loadmodule /path/to/rebloom.so

布隆过滤器命令

命令 说明
BF.RESERVE {key} {error_rate} {size} 创建布隆过滤器,error_rate为错误率,size为预期数据大小
BF.ADD {key} {item} 添加item到指定布隆过滤器
BF.MADD {key} {item} [item …] 批量添加item到指定布隆过滤器
BF.EXISTS {key} {item} 判断item是否存在与指定布隆过滤器
BF.MEXISTS {key} {item} [item …] 批量判断item是否存在与指定布隆过滤器

JavaAPI

导入maven依赖

<dependency>
    <groupId>com.redislabs</groupId>
    <artifactId>jrebloom</artifactId>
    <version>1.1.0</version>
</dependency>

Api使用

public static void main(String[] args) {
    JedisPool jedisPool = new JedisPool("192.168.1.158", 6379);
    Client client = new Client(jedisPool);
    client.createFilter("main6", 10000, 0.000001);
    //批量插入数据
    for (int i = 0; i <100; i++) {
        String values[] = new String[100];
        for (int j = 0; j < 100; j++) {
            String value = "item" + (i * 100 +j);
            values[j] = value;
        }
        client.addMulti("main6", values);
    }
	//判断是否存在
    System.out.println(client.exists("main6", "item1"));
    System.out.println(client.exists("main6", "a"));
    jedisPool.close();
}

十三、Redis开发规范

BigKey处理

发现BigKey

  • debug object {key} 查看指定key的详细信息
  • redis-cli --bigkeys 扫描出BigKey【全表扫描,阻塞,建议从节点本地执行】

BigKey删除

场景:当key非常大时,delete命令执行十分缓慢,会发生阻塞【过期bigkey也是进行删除操作也会阻塞】

redis4.0之后:可以使用unlink命令进行后台删除,不阻塞前台

生命周期管理

  • 使用OBJECT IDLETIME {key}查看key的闲置时间

  • 过期时间不易集中

命令优化

有遍历需求可以使用hscansscanzscan代替【这些扫描命令在field较少时COUNT参数不会生效

必要情况下使用monitor命令监控,注意时间不要过长

Java客户端优化

参数名 含义 默认值 建议
testWhileIdle 是否开启空闲资源检测 false true
timeBetweenEvictionRunsMillis 空闲资源检测周期 -1 自选,也可使用JedisPoolConfig中的默认值
minEvictiableIdleTimeMillis 资源池中资源最小空闲时间 30分钟 自选,也可使用JedisPoolConfig中的默认值
numTestsPerEvictionRun 做空闲资源检测每次的采样数 3 自选,如果设置为-1则为全部做空闲检测

maxIdle需要设置为接近maxTotal

预估maxTotal方法的例子:

一次命令时间平均耗时1ms,一个连接QPS大约1000,业务期忘的QPS时50000,理论上maxTotal=50000/1000=50

十四、内存管理

内存消耗

内存统计

执行info memory命令可以查看内存信息

主要属性名 属性说明
used_memory 实际存储数据的内存总量
used_memory_rss redis进程占用的总物理内存
maxmemory 最大内存
maxmemory_policy 内存剔除策略
mem_fragmentation_ratio used_memory_rss/used_memory比值,表示内存碎片率

内存消耗划分

Memory-Used

  • 客户端缓冲区设置规则

    client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

    <class>:客户端类型,分为三种

    ​ (a)normal:普通客户端

    ​ (b)slave:用从节点用于复制,伪裝成客户端

    ​ (c)pubsub:发布订阅客户端 <hardlimit>:如客户使用的输出冲区大于hardlimit客户端会被立即关闭

    <soft limit> 和<soft seconds>:如果客户端使用的输出缓冲区超过了 <soft limit>并且持续了<soft seconds>,客户会被立即关闭

    • 普通客户端默认:client-output-buffer-limit normal 0 0 0
    • salve客户端默认:client-output-buffer-limit slave 256mb 64mb 60
    • pubsub客户端默认:client-output-buffer-limit slave 32mb 8mb 60
  • 复制缓冲区:用于slave和master断开重连时不进行全量复制保存偏移数据使用

    默认为repl-backlog-size 1mb

  • AOF缓冲区:AOF重写期间,AOF缓冲区,没有容量限制

内存回收策略

删除过期值

  • 惰性删除:访问key->expired dict->del key【先在过期表中找,发现过期删除key,返回null】
  • 定时删除:每秒运行10次,采样删除

超过maxmemory使用maxmemory-policy进行控制,参见缓存更新策略

十五、开发运维事项

Linux内核优化

vm.overcommit_memory

Redis建议vm.overcommit_memory = 1(影响fork操作)

立即生效:

永久生效:vm.overcommit_memory = 1写入到/etc/sysctl.conf文件中

含义
0 表示内核将检查是否有足够可用的内存。如果有足够可用的内存,内存申请通过,否则内存申请失败,并返回错误给进程
1 表示内核允许超量使用内存直到用完为止
2 表示内存绝不过量使用,即整个系统内存不能超过swap+50%的RAM值

swappiness

策略
0 Linux3.5及以上:宁愿OOM Killer也不用swap
Linux3.5及以下:宁愿swap也不用OOM Killer
1 Linux3.5及以上:宁愿swap也不用OOM Killer
60 默认值
100 操作系统会主动使用swap

建议:Linux3.5以上vm.swappiness = 1,否则vm.swappiness = 0

立即生效:echo {bestValue} > /proc/sys/vm/swappiness

永久生效:vm.swappiness = {bestValue} 写入到/etc/sysctl.conf

THP(Transparent huge page)

建议禁用,Centos7在/sys/kernel/mm/transparent_hugepage/enabled下设置为never即可

THP为大内存页时fork子线程时Copy-On-Write可能造成延迟

ulimit

建议将Redis启动用户的文件句柄限制调成10032,限制文件etc/security/limits

TCP backlog

建议将/proc/sys/net/core/somaxconn系统TCP backlog的限制设置为与Redis一样默认511

Redis安全问题

在配置文件中配置

#将命令更改为另一个字符串,原命令失效。如果字符串为空则表示禁用此命令
rename-command {cmd} {str}

附各种数据类型的内部编码:

Data-Encoding

附Redis云平台CacheCloud【一键部署、监控、运维、数据迁移工具等】 GitHub项目地址