简介
什么是Redis
有哪些公司在使用Redis
GitHub, Twitter, Stackoverflow, Alibaba, 百度,微博,美团,搜狐
Redis的八大特性
- 速度快
- 支持持久化
- 存在多种数据结构
- 支持多种语言
- 功能丰富
- 简单(代码量小,使用方便)
- 主从复制
- 高可用,分布式
速度快
官方给的指标是10w OPS(每秒十万次读写)
这么快的原因:
数据存放在内存中
使用C语言编写(大约五万行代码),C语言离操作系统较近
线程模型为单线程
持久化
Redis所有数据保存在内存中,对数据的更新将异步的保存到磁盘上。
多种数据结构
五大数据结构:
- Strings
- Hash Tables
- Linked Lists
- Sets
- Sorted Sets
其他新结构:
- BitMaps: 位图(本质是字符串,可以实现类似布隆过滤器的功能)
- HyperLogLog:超小内存唯一值计数(本质是字符串)
- GEO:地理信息定位(本质是有序集合,Redis-3.2后提供)
支持多种客户端语言
Java,PHP,python,Ruby,Lua,nodejs
功能丰富
- 发布订阅模式
- Lua脚本
- 事务
- pipeline(提高客户端并发效率)
典型应用场景
- 缓存系统
- 计数器(微博的转发数,评论数以及视频网站的播放数等)
- 消息队列系统
- 排行榜
- 社交网络(粉丝数,关注数,共同关注数)
- 实时系统(垃圾邮件处理系统,布隆过滤器)
安装Redis
Redis安装
Linux系统下安装Redis
1 | 1. 下载 |
可执行文件说明
可执行文件 | 说明 |
---|---|
redis-server | Redis服务器 |
redis-cli | Redis命令行客户端 |
redis-benchmark | Redis性能测试工具 |
redis-check-aof | AOF文件修复工具 |
redis-check-dump | RDB文件修复工具 |
redis-sentinel | Sentinel服务器(2.8以后) |
三种启动方法
最简启动
直接使用redis-server
动态参数启动
1 | redis-server --port 6380 # 默认端口是6379 |
配置文件启动
1 | redis-server configPath |
验证方法
1 | ps -ef|grep redis |
比较
- 生产环境推荐配置启动
- 单机多实例配置文件可以用端口区分开
简单的客户端连接
连接
1 | redis-cli -h 10.10.79.150 -p 6384 |
返回值
- 状态回复(如
ping
返回PONG
) - 错误回复(如
(error) WRONGTYPE Operation against
) - 整数回复(如
incr hello
回复(integer) 1
) - 字符串回复(如
get hello
回复"world"
) - 多行字符串回复(如
mget hello foo
返回"world" "bar"
)
Redis常用配置
属性 | 说明 |
---|---|
daemonize | 是否是守护进程(no默认|yes推荐) |
port | Redis对外端口号,默认为6379 7379对应手机按键上MERZ对应的号码,MERZ取自意大利歌女Alessia Merz的名字 |
logfile | Redis系统日志 |
dir | Redis工作目录 |
Redis API
通用命令
keys
eg:
keys *
遍历所有的key怎么用:
热备从节点
scan
dbsize
计算key的总数
exists key
检查key是否存在,存在返回1,否则返回0
del key [key …]
删除指定key-value,成功返回1,否则返回0
expire key seconds
ttl key
查看key剩余的过期时间persist key
去掉key的过期时间type key
返回key的类型(string,hash,list,set,zset,none)
命令 | 时间复杂度 |
---|---|
keys | O(n) |
dbsize | O(1) |
del | O(1) |
exists | O(1) |
expire | O(1) |
type | O(1) |
数据结构和内部编码
内部编码
字符串键值结构
key | value |
---|---|
hello | world |
counter | 1 |
bits | |1|0|1|1|1|0|1| |
{ “prodduct”:{ “id”: “3242” “name”: “test423” } } |
Value up to 512MB,up to 100k is recommended
场景
- 缓存
- 计数器
- 分布式锁
命令
1 | 1. 获取key对应的value O(1) |
1 | 1. key自增1,如果key不存在,自增后get(key)=1 O(1) |
1 | 1. 不管key是否存在,都设置 O(1) |
1 | 1. 批量获取key,原子操作 O(n) |
n次get = n次网络时间 + n次命令时间
1次mget = 1次网络时间 + n次命令时间
1 | 1. set key newvalue并返回旧的value O(1) |
1 | 1. 增加key对应的值3.5 O(1) |
复杂度总结
命令 | 含义 | 复杂度 |
---|---|---|
set key value | 是指key-value | O(1) |
get key | 获取key-value | O(1) |
del key | 删除key-value | O(1) |
setnx setxx | 根据key是否存在设置key-value | O(1) |
Incr decr | 计数 | O(1) |
mget mset | 批量操作key-value | O(n) |
实战
记录网站每个用户个人主页的访问量?
1
incr userid:pageview
缓存视频的基本信息(数据源在MySQL中)伪代码
1
2
3
4
5
6
7
8
9
10
11
12public VideoInfo get(long id){
String redisKey = redisPrefix + id;
VideoInfo videoInfo = redis.get(redisKey);
if(videoInfo == null){
videoInfo = mysql.get(id);
if(videoInfo != null){
//序列化
redis.set(redisKey, serialize(videoInfo));
}
}
return videoInfo;
}分布式计数器
incr id (原子操作)
Hash键值结构
key | field | value |
---|---|---|
user:1:info | name age Date |
Ronaldo 32 239 |
特点
是一个
value
为Map
的Map
field
不能相同,value
可以相同
API
所有以哈希为结构的命令都是以H
开头的。
1 | 1. 获取hash key对应的field的value O(1) |
1 | 1. 判断hash key是否有field O(1) |
1 | 1. 批量获取hash key的一批field对应的值 O(n) |
1 | 1. 返回hash key对应所有的field和value O(n) |
1 | 1. 设置hash key对应field的value(若field已存在,则失败) O(1) |
复杂度总结
命令 | 复杂度 |
---|---|
hget hset hdel | O(1) |
hexists | O(1) |
hincrby | O(1) |
hgetall hvals hkeys | O(n) |
hmget hmset | O(n) |
列表
特点
- 有序(根据插入顺序得到遍历顺序)
- 可以重复
- 左右两边都可以插入和弹出
API
列表的API都以L
开头
增
1 | 1. 从列表右端插入值 O(1) |
删
1 | 1. 从列表左侧弹出一个item O(1) |
1 | 3. 根据count值,从列表中删除所有value相等的项 O(n) |
1 | 4. 按照索引范围修剪列表 O(n) |
查
1 | 1. 获取列表指定索引范围所有item O(n) |
1 | 2. 获取列表指定索引的item O(n) |
1 | 3. 获取列表长度 O(1) |
改
1 | 1. 设置列表指定索引值为newValue O(n) |
其他
1 | 1. lpop阻塞版本,timeout是阻塞超时时间,timeout=0为永远不阻塞 O(1) |
TIPS
- LPUSH + LPOP = Stack
- LPUSH + RPOP = Queue
- LPUSH + LTRIM = Capped Collection
- LPUSH + BRPOP = Message Queue
实战
- TimeLine
实战
记录网站每个用户个人主页的访问量
1
hincrby user:1:info pageview count
缓存视频的基本信息(数据源在mysql中)伪代码
1
2
3
4
5
6
7
8
9
10
11
12public VideoInfo get(long id){
String redisKey = redisPrefix + id;
Map<String, String> hashMap = redis.hgetAll(redisKey);
VideoInfo videoInfo = transferMapToVideo(hashMap);
if(videoInfo == null){
videoInfo = mysql.get(id);
if(videoInfo != null){
redis.hmset(redisKey, transferVideoToMap(videoInfo));
}
}
return videoInfo;
}
单线程架构
Redis在同一时刻只会执行一条命令
单线程为什么这么快
纯内存
非阻塞IO
避免线程切换和竞态消耗
拒绝长(慢)命令
keys, flushall, flushdb, slow lua script, mutil/exec, operate big value(collection)
其实不是单线程
fysnc file descriptor
close file descriptor
Redis客户端
Java客户端:Jedis
获取Jedis
- 添加
Maven
依赖
1 | <!-- https://mvnrepository.com/artifact/redis.clients/jedis --> |
Jedis直连(TCP连接)
1
2
3Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.set("hello","world");
String value = jedis.get("hello");Jedis(String host, int port, int connectionTimeout, int soTimeout)
- host : Redis节点所在机器的IP
- port : Redis节点的端口
- connectionTimeout : 客户端连接超时时间(内部使用socket技术)
- soTimeout : 客户端读写超时时间
Jedis基本使用
Jedis连接池使用
简单使用
1 | // 初始化Jedis连接池,通常来讲JedisPool是单例的。 |
优点 | 缺点 | |
---|---|---|
直连 | - 简单方便 - 适用于少量长期连接的场景 |
- 存在每次新建/关闭TCP开销 - 资源无法控制,存在连接泄露的可能 - Jedis对象线程不安全 |
连接池 | - Jedis预先生成,降低开销使用 - 连接池的形式保护和控制资源的使用 |
相对于直连,使用相对麻烦,尤其在资源的管理上需要很多参数来保证,一旦规划不合理也会出现问题。 |
python客户端:redis-py
源码安装
1 | wget https://github.com/andymccurdy/redis-py/archive/3.0.0.zip |
简单使用
1 | import redis |
Go客户端
1 | c,err := redis.Dial("tcp", "127.0.0.1:6379") |
瑞士军刀Redis
慢查询
生命周期
- 慢查询发生在第三阶段
- 客户端超时不一定是慢查询导致的,慢查询只是导致客户端超时的一个可能因素
两个配置
slowlog-max-len
- 先进先出队列
- 固定长度
- 保存在内存内
slowlog-log-slower-than
- 慢查询阈值(单位:微妙,1毫秒等于1000微秒)
- slowlog-log-slower-than=0,记录所有命令
- slowlog-log-slower-than<0,不记录任何命令
配置方法
- 默认值
- config get slowlog-max-len = 128
- config get slowlog-log-slower-than = 10000
- 修改配置文件后重启(适用于未启动时)
- 动态配置
- config set slowlog-max-len 1000
- config set slowlog-log-slower-than 1000
三个命令
- slowlog get [n] :获取慢查询队列
- slowlog len :获取慢查询队列长度
- slowlog reset :清空慢查询队列
运维经验
- slowlog-max-len不要是指过大,默认10ms,通常是指1ms
- slowlog-log-slower-than不要设置太小,通常设置1000左右
- 理解命令生命周期
- 定期持久化慢查询
pipeline
什么是流水线
流水线就是一次网络连接里传输一批命令,节省网络传输时间
命令 | N个命令操作 | 1次pipeline |
---|---|---|
时间 | n次网络 + n次命令 | 1次网络 + n次命令 |
数据量 | 1条命令 | n条命令 |
注意:
- Redis的命令时间是微秒级别。
- pipeline每次条数要控制(网络)。
客户端实现 pipeline-Jedis
1 | // before pipeline |
1 | // after pipeline |
与原生M操作做对比
M操作是原子操作,pipeline是非原子操作。
使用建议
- 注意每次pipeline携带数据量
- pipeline每次只能作用在一个Redis节点上
- M操作与pipeline区别
发布订阅
角色
- 发布者(publisher)
- 订阅者(subscriber)
- 频道(channel)
模型
Redis server中有各个频道。
发布者向频道中发布消息
订阅者收到其所订阅频道的消息,只能收到订阅时刻之后的消息,之前的收不到。
需要注意的是,redis没有消息堆积的能力。
API
publish
1 | publish channel message |
eg:
1 | publish sohu:tv "hello world" |
subscribe
1 | subscribe [channel] # 一个或多个 |
unsubscribe
1 | unsubcribe [channle] # 一个或多个 |
其他
1 | 1. 订阅模式 |
发布订阅与消息队列
发布订阅是所有订阅者可以收到所有的消息
消息队列是订阅者要进行消息抢夺,一条消息只有一个订阅者能够抢到。
Bitmap
位图
b | i | g |
---|---|---|
01100010 | 01101001 | 01100111 |
相关命令
1 | 1. 给位图指定索引设置值 |
独立用户统计
- 使用set和Bitmap
- 总共1亿用户,每日5千万独立访问。
数据类型 | 每个userid只用空间 | 需要存储的用户量 | 全部内存量 |
---|---|---|---|
set | 32位 (假设userid用的是整型, 实际很多网站用的是长整型) |
50,000,000 | 32位*50,000,000=100MB |
Bitmap | 1位 | 100,000,000 | 12.5MB |
一天 | 一个月 | 一年 | |
---|---|---|---|
set | 200M | 6G | 72G |
Bitmap | 12.5M | 375M | 4.5G |
如果只有10万对立用户:
| 数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部内存量 |
| ——– | —————— | —————- | ———————- |
| set | 32位 | 1,000,000 | 32位1,000,000=4MB |
| Bitmap | 1位 | 100,000,000 | 1位100,000,000=12.5MB |
使用经验
- type=string,最大512MB
- 注意setbit时的偏移量,可能有较大耗时
- 位图不是绝对好。
HyperLogLog
是否是新的数据结构
- 基于 HyperLogLog 算法:极小空间完成独立数量统计。
- 本质还是字符串
三个命令
向 hyperloglog 添加元素
1
pfadd key element [element ...]
计算 hyperloglog 的独立总数
1
pfcount key [key ...]
合并多个 hyperloglog
1
pfmerge destkey sourcekey [sourcekey ...]
内存消耗
使用经验
- 存在错误率(0.81%)
- 无法取出单条数据
GEO
GEO是什么
GEO(地理信息定位):存储经纬度,计算两地距离,范围计算等。
5个城市经纬度
城市 | 经度 | 纬度 |
---|---|---|
北京 | 116.28 | 39.55 |
天津 | 117.12 | 39.08 |
石家庄 | 114.29 | 38.02 |
唐山 | 118.01 | 39.38 |
保定 | 115.29 | 38.51 |
相关命令
1 | 1. 添加地理位置信息 |
相关说明
- since 3.2+
- type geoKey = zset
- 没有删除 API:zrem key member
持久化
持久化的作用
(1)什么是持久化
redis 所有数据保存在内存中,对数据的更新将异步的保存到磁盘上。
(2)持久化的实现方式
- 快照
- MySQL Dump
- Redis RDB
- 写日志
- MySQL Binlog
- Hbase Hlog
- Redis AOF
RDB
(1)什么是RDB
redis 通过命令创建 RDB 文件(二进制)保存在硬盘中。当 redis 重启时通过命令将 RDB 文件载入到内存。实质是一个复制媒介。
(2)触发机制-主要三种方式
save(同步)
- 新文件将会替换老文件
- 复杂度位 O(n)
bgsave(异步)
利用 Linux fork() 异步执行
文件策略及复杂度与 save 相同
命令 save bgsave IO类型 同步 异步 阻塞 是 是(阻塞发生在fork) 复杂度 O(n) O(n) 优点 不会消耗额外内存 不阻塞客户端命令 缺点 阻塞客户端命令 需要fork,消耗内存
自动
redis 提供了 save 配置
配置 seconds changes save 900 1 save 300 10 save 60 10000 满足任意一个条件,redis 会自动创建(bgsave)RDB 文件
默认配置:
1
2
3
4
5
6
7
8save 900 1
save 300 10
save 60 10000
dbfiename dump.rdb
dir ./
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbcheksum yes最佳配置:
1
2
3
4
5dbfilename dump-${port}.rdb
dir /bigdiskpath
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbcheksum yes
(3)触发机制-不容忽视方式
1. 全量复制
2. debug reload
3. shutdown
(4)总结
1. RDB是 Redis 内存到硬盘的快照,用于持久化。
2. save 通常会阻塞 Redis
3. bgsave 不会阻塞 Redis,但是会 fork 新进程。
4. save 自动配置满足任意一个条件就会被执行。
AOF
RDB 现存问题
耗时,耗性能
O(n): 耗时
fork(): 消耗内存,copy-on-write 策略
Disk I/O: IO 性能
不可控,丢失数据
什么是 AOF
每执行一条命令,就将该命令写入到 AOF 日志文件中。
AOF 三种策略
(1)always
每条写命令都会出发刷新,将缓冲区的命令日志刷新到 AOF 文件中。
(2)everysec
每秒(默认值)刷新一次
(3)no
由操作系统决定什么时候刷新。
命令 | always | everysec | no |
---|---|---|---|
优点 | 不丢失数据 | 每秒一次 fsync 丢失一秒数据 |
不用管 |
缺点 | IO 开销较大,一般的 sata 盘只有几百 TPS | 丢失1秒数据 | 不可控 |
AOF 重写
原生 AOF | AOF 重写 |
---|---|
set hello world set hello java set hello hehe incr counter incr counter rpush mylist a rpush mylist b rpush mylist c 过期数据 |
set hello hehe set counter 2 rpush mylist a b c |
AOF 重写的作用:
- 减少磁盘占用量
- 加速恢复速度
AOF 重写的两种实现方式
berewriteaof
AOF 重写配置
配置名 含义 autoaof-rewrite-min-size AOF 文件重写需要的尺寸 auto-aof-rewrite-percentage AOF 文件增长率 统计名 含义 aof_current_size AOF 当前尺寸(单位:字节) aof_base_size AOF 上次启动和重写的尺寸(单位:字节) 自动触发时机:
- aof_current_size > auto-aof-rewrite-min-size
- aof_current_size - aof_base_size / aof_base_size > auto-aof-rewrite-percentage
AOF 重写流程
常用配置:
1
2
3
4
5
6
7appendonly yes
appendfilename "appendonly-${port}.aof"
appendfsync everysec
dir /bigdiskpath
no-appendfsync-on-rewrite yes
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
RDB 和 AOF 的抉择
RDB 和 AOF 比较
命令 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
轻重 | 重 | 轻 |
RDB 最佳策略
- “关”
- 集中管理
- 主从,从开
AOF 最佳策略
- ”开“:缓存和存储
- AOF 重写集中管理
- everysec
最佳策略
- 小分片
- 缓存或者存储
- 监控(硬盘、内存、负载、网络)
- 足够的内存
开发运维常见问题
fork 操作
- 同步操作
- 与内存量息息相关:内存越大,耗时越长(与机器类型有关)
- info: latest_fork_usec 查询上一次 fork 的时间
如何改善fork
- 优先使用物理机或者高效支持 fork 操作的虚拟化技术
- 控制 Redis 实例最大可用内存:maxmemory
- 合理配置 Linux 内存分配策略:vm.overcommit_memory = 1
- 降低 fork 频率:例如放宽 AOF 重写自动触发时机,不必要的全量复制
进程外开销
子进程开销和优化
- CPU:
- 开销:RDB 和 AOF 文件生成,属于 CPU 密集型
- 优化:不做 CPU 绑定,不和 CPU 密集型部署
- 内存:
- 开销:fork 内存开销,copy-on-write。
- 优化:echo never > /sys/kernel/mm/transparent_hugepage/enabled
- 硬盘
- 开销:AOF 和 RDB 文件写入,可以结合 iostat,iotop 分析
- 优化:
- 不要和高硬盘负责服务部署在一起:存储服务、消息队列等
- no-appendfsync-on-rewrite = yes
- 根据写入量决定磁盘类型:例如 ssd
- 单机多实例持久化文件目录可以考虑分盘
AOF追加阻塞
AOF 阻塞定位
Reids 日志:
Asynchronous AOF fsync is taking too long (disk is busy?).
Writing the AOF buffer without waiting for fsync to complete,this may slow down Redis.
info Persistence:
…
aof_delayed_fsync: 100
…
单机多实例部署
Redis 复制的原理与优化
什么是主从复制
(1)单机有什么问题?
机器故障
容量瓶颈
QPS 瓶颈
(2)作用
数据副本
扩展读性能
复制的配置
两种实现方式
slaveof 命令
配置
1
2slaveof ip port
slave-read-only yes
全量复制和部分复制
查看 run_id
1 | redis-cli -p 6379 info server | grep run |
查看偏移量
1 | redis-cli -p 6379 info replication |
全量复制开销:
- bgsave 时间
- RDB文件网络传输时间
- 从节点清空数据时间
- 从节点加载 RDB 的时间
- 可能的 AOF 重写时间
故障处理
开发运维常见问题
读写分离
将读流量分摊到从节点
可能遇到的问题:
- 复制数据延迟
- 读到过期数据
- 从节点故障
主从配置不一致
规避全量复制
- 第一次不可避免
- 系欸但运行 ID 不匹配
- 主节点重启
- 故障转移
- 复制积压缓冲区不足
- 网络中断,部分复制无法满足
- 增大复制缓冲区配置 rel_backlog_size,网络“增强”。
规避复制风暴
- 单主节点复制风暴
- 问题:主节点重启,多从节点复制
- 解决:更换复制拓扑
- 单机器复制风暴
- 机器宕机后,大量全量复制
- 主节点分散多机器
- 单主节点复制风暴
Redis Sentinel
主从复制高可用存在的问题
- 手动故障转移
- 写能力和存储能力受限
架构说明
客户端不直接访问 redis,而是通过 sentinel 来访问。由 sentinel 来负责主从节点的管理。
安装配置
sentinel 的默认端口为 26379
- 配置开启主从节点
- 配置开启 sentinel 监控主节点(sentinel 是特殊的 redis)
客户端连接
请求相应流程
jedis
1 | JedisSentinelPool sentinelPool = new JedisSentinelPool(masterName, sentinelSet, poolConfig, timeout); |
redis-py
实现原理
常见开发运维问题
Redis Cluster
呼唤集群
- 超高并发量需求
- 超大数据量需求
数据分布
顺序分布
哈希分布
- 节点取余
- 使用多倍扩容降低迁移率
- 一致性哈希
- 顺时针取余
- 只影响邻近节点
- 扩容后会存在负载不均衡的情况
- 虚拟槽
- 每个槽映射一个数据子集
- 良好的哈希函数:例如 CRC16
- 服务端管理节点、槽、数据:例如 Redis Cluster
分布方式 | 特点 | 典型产品 |
---|---|---|
哈希分布 | 数据分散度高 键值分布业务无关 无法顺序访问 支持批量操作 |
一致性哈希 Memcache Redis Cluster |
顺序分布 | 数据分散度易倾斜 键值业务相关 可顺序访问 不支持批量操作 |
BigTable HBase |
搭建集群
集群伸缩
客户端路由
集群原理
开发运维常见问题
Redis Cluster 架构
- 节点
- meet
- 指派槽
- 复制
Redis Cluster 特性
- 复制
- 高可用
- 分片
两种安装方式
原生命令安装
配置开启节点
1
2
3
4
5
6
7port ${port}
daemonize yes
dir "/opt/redis/data"
dbfilename "dump-${port}.rdb"
logfile "${port}.log"
cluster-enabled yes
cluster-config-file nodes-${port}.confmeet
1
cluster meet ip port
指派槽
1
cluster addslots slot [slot ...]
分配主从关系
1
cluster replicate node-id