# 1 Redis总览
# 1.1 特性
# 1.2 应用场景
# 2 数据结构简单介绍
使用 redis 3.0 版本
# 2.1 安装与启动
# 2.1.1 安装目录文件
# 2.1.2 server 三种启动方式
序号 | 方式 | 说明 |
---|---|---|
1 | 执行命令:redis-server | 最简启动 |
2 | 执行命令:redis-server --port 6380 | 动态参数启动 |
3 | 执行命令:redis-server configPath | 配置参数启动 |
# 2.1.3 客户端连接
redis-cli -h [ip] -p [port] 连上之后就可以执行各种操作了
# 2.1.4 常用配置
- port:端口(默认6379)
- logfile:日志文件名称,只是一个文件名
- dir:Redis的工作目录,logfile、aof等存放的路径
- RDB config、AOF config、slow Log config、maxMemory等等
# 2.2 数据结构
# 2.2.1 通用命令
- keys [pattern]:返回redis中所有的key
- keys he*:返回he开头的key
- dbsize:返回redis中key-value的数量
- exists key:key是否存在
- del key:删除key,可以删除多个del key1 key2...
- 过期时间:
- expire key seconds:设置key的过期时间
- ttl key:查看key剩余时间
- persist key:去掉过期时间
- type key:查看key的数据类型
命令 | 时间复杂度 | 说明 |
---|---|---|
keys | O(n) | 扫描全表,不建议生产环境使用 |
dbsize | O(1) | redis内部维护一个计数器 |
del | O(1) | |
exists | O(1) | |
expire | O(1) | |
type | O(1) |
# 2.2.2 数据结构和内部编码
row:[rɔː]
# 2.2.3 单线程
- 一次只运行一条命令
- 拒绝长(慢)命令:keys,flush all,flushdb,会堵住后面的命令
- 有些功能不是单线程:fysnc file descriptor
为什么这么快?
- 纯内存
- 非阻塞IO,select/poll/epoll
- 单线程优势:避免线程切换和静态消耗
# 2.2.4 基本数据结构一:字符串
mgset和mset的优点:
1次mget = 1次网络时间 + n次命令时间
应用:
- 某个人主页被访问次数,key = userid:pageview;incr userid:pageview(单线程、无并发问题)
- 分布式Id
- incr id
# 2.2.5 基本数据结构二:哈希(命令以h开头)
命令 | 含义 | 命令 | 含义 |
---|---|---|---|
hget key field | hmset key field1 value1 field2 value2... | ||
hget set fileld value | hvalues key | 返回这个key对应的所有value | |
hdel key field | hkeys key | 返回这个key对应的所有field | |
hexists key field | hgetall key | 返回这个key对应的所有field和value | |
hlen key | key的field总数 | hsetnx key field value | |
hmget key field1 field2... | ... |
命令 | 优点 | 缺点 |
---|---|---|
String key json | 编程简单 | 序列化开销;设置属性要操作整个数据 |
hash | 直观;可以部分更新 | 编程稍微复杂;ttl不好控制 |
# 2.2.6 基本数据结构三:列表list(命令以l开头)
特点:有序、可以重复、支持左右操作
命令 | 含义 | 命令 | 含义 |
---|---|---|---|
rpush key value1 value2 ... | ltrim key startIndex endIndex | 让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除 | |
lpush key value1 value2 ... | lrange key startIndex endIndex | 获取列表指定索引范围所有项目 1. lrange key 0 2:[0,2],包含end 2. lrange key 1 -1:[1,∞) | |
linsert key before|after value newValue | 在list指定的值前|后插入newValue | lindex key index | 按照索引取值 |
lpop key | 从左边弹出一个 | llen key | 获取列表长度 |
rpop key | lset key index newValue | ||
lrem key count value | 根据count值,从列表中删除所有value相等的项: 1. count>0,从左到右,删除最多count个value相等的项 2. count<0,从右到左,删除最多abs(count)个value相等的项 3. count = 0,删除所有value相等的项 | blpop/brpop,lpop/rpop的阻塞版本 | 取不到数据就阻塞等待,用于生产者和消费者模型 |
应用:微博的TimeLine,查看所有关注的人的动态,按照时间倒序 当关注的人发了动态,就使用lpush插入到你的timeline中
# 2.2.7 基本数据结构四:set(命令以s开头,集合)
特性:value不能有重复元素、无序、支持集合之间的操作
命令 | 含义 | 命令 | 含义 |
---|---|---|---|
sadd key element | 向集合key添加element,如果存在,添加失败 | spop key | 从集合中弹出一个元素 |
srem key element | 删除集合key中的element元素 | sdiff key1 key2 | 差集 |
scard key | 计算集合大小 | sinter key1 key2 | 交集 |
sismember key element | 判断元素element是否在集合中 | sunion key1 key2 | 并集 |
srandmember key count | 随机从集合中挑count个元素 | ||
smembers key | 取出集合中所有元素 |
应用:
- 微博的关注,每关注一个,就放一个到value中;
- 共同关注:可以计算出两个user的共同关注sinter方法
- 微博抽奖:如果是可以重复抽奖可以使用srandmember,不可重复抽可以使用spop
- 一条微博有哪些人like、赞、踩,可以使用集合记录
# 2.2.8 基本数据结构五:zset(命令以z开头,有序集合)
集合(set) vs 有序集合(zset)
- 均无重复元素(zset, score能重复,element不能重复)
- set无序,zset有序
- set只有element,zset有element + score
命令 | 含义 | 命令 | 含义 |
---|---|---|---|
zadd key score element | 向集合key添加score,element | zrangebyscore key score1 score2 | 返回指定分数范围的element |
zrem key element | 删除集合key中的element元素 | zcount key score1 score2 | 返回指定分数范围的element个数 |
zscore key element | 获取element的score | zremrangebyrank key index1 index2 | 按照排名范围删除element |
zincrby key increScore element | 给element增加分数 | zremrangebyscore key score1 score2 | 按照分数范围删除element |
zcard key | 返回element的个数 | zinter、zunion、zdiff | |
zrange key index1 index2 | 返回指定索引范围的element |
应用:微博热搜榜,每看过一次分数加1
# 2.3 扩展功能
# 2.3.1 慢查询日志
redis可以记录执行时间超过某个时长的命令,使用先进先出的队列保存,这个队列固定长度,且是保存在内存中 ;最好是一段时间持久化一次慢查询,因为超过慢查询队列长度,先进的慢查询就会被删除
配置:
- slowlog-max-len:慢查询队列长度,默认是128
- slowlog-log-slower-than:慢查询日志阈值(微妙,1毫秒 = 1000微妙),默认是10 000 = 10毫秒
- 设置0,记录所有命令
- 设置小于0,不记录任何命令
- 建议设置成1毫秒(redis通常的qps最少是10000/1秒,每条命令平均0.1毫秒)
方法:1. 修改配置文件重启;2. 动态配置:config set ...
相关命令:
- slowlog get n :获取n条慢查询
- slowlog len:获取慢查询队列有多少个慢查询
- slowlog reset:清空慢查询
# 2.3.2 pipeline(批量操作)
redis每条命令的执行时间平均0.1毫秒,这样的话网络就成为了瓶颈
- N个命令操作:n次网络 + n次命令
- 1次pipeline:1次网络 + n次命令
jedis写法:
Jedis jedis = new Jedis("127.0.0.1", 6379);
for (int i = 0; i<100; i ++) {
Pipeline pipeline = jedis.pipelined();
for (int j = i * 100; i < (i + 1) * 100; j ++)) {
pipeline.hset("hashkey" + j, "field" + j, "value" + j);
}
pipeline.syncAndReturnAll();
}
与mget、mset的区别:
pipeline命令不是原子的
# 2.3.3 发布订阅
注意和消息队列模型的区别:发布订阅是每个订阅者都能收到,消息队列是每个消息只能被一个消费者消费
命令:
- publish channel message:publish sohu:tv "hello world"
- subscribe channel:subscribe sohu:tv
- unsubscribe channel
# 2.3.4 bitMap(位图)
bitmap底层是字符串类型,所以value最大为512M
字符串big对应的二进制:
命令:
- setbit key offset 0/1:把key对于的value的第offset为修改为0或1
- 如果value只有10位,执行setbit key 50 1,那么10-49都会自动补0
- getbit key offset
- bitcout key [start end]:获取指定范围值为1的个数
应用:独立用户统计,1亿用户,每天大概有5千万人访问,使用set和bitmap的区别:
数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部内存量 |
---|---|---|---|
set | 32位(假设userid用的是整型) | 50,000,000 | 32*50,000,000 = 200MB |
bitmap | 1位 | 100,000,000 | 1*100,000,000 = 12.5M |
假如还是1亿用户,但是每天大概只有100万人访问
数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部内存量 |
---|---|---|---|
set | 32位(假设userid用的是整型) | 1,000,000 | 32*1,000,000 = 4MB |
bitmap | 1位 | 100,000,000 | 1*100,000,000 = 12.5M |
可以使用用户表的id来决定每个用户所在的位,因为是独立用户访问(去重),所以不能用累加的方式
# 2.3.5 HyperLogLog(极小空间完成独立数量统计)
底层是字符串,原理是伯努利概率论相关的东西
命令:
- pfadd key element1 element2...:向hyperloglog添加元素
- pfcount key:计算这个key的独立总数
- pfmerge destkey sourcekey1 sourcekey1...:合并多个hyperloglog key到另外一个destkey
比bitmap消耗内存量更小,100万个用户,大约只能消耗15KB 缺点:
- 有一定的错误率:0.81%
- 无法知道具体的用户id
# 2.3.6 geo(地理位置)
应用:摇一摇、附近的酒店
# 3 持久化
持久化的方式:
- 快照:MySQL Dump、Redis RDB
- 写日志:MySQL binlog、Redis AOF
# 3.1 快照 RDB(Redis DataBase)
stop-writes-on-bgsave-error:如果等于 yes
假设 :创建快照(硬盘上,产生一个新的rdb文件)需要 20s时间,redis主进程,在这20s内,会继续接受客户端命令,但是,就在这20s内,创建快照出错了,比如磁盘满了,那么redis会拒绝新的写入,也就是说,它认为,你当下,持久化数据出现了问题,你就不要再set了
命令 | save | bgsave |
---|---|---|
IO类型 | 同步 | 异步 |
阻塞 | 是 | 是(阻塞发生在fork) |
优点 | 不会消耗额外内存 | 不太阻塞redis |
# 3.2 日志 AOF(Append Only File)
# 3.2.1 AOF重写
aof重写:例如重写之前
- set hello world
- set hello java
- set hello heee
重写之后:set hello hehe,节约磁盘,加速数据恢复
AOF重写流程:
- 执行 aof 重写请求
- 如果当前进程正在执行 aof 重写,请求不执行并返回如下响应:ERR Background append only file rewriting already in process
- 如果当前正在执行 bgsave ,重写命令等待 bgsave 完成后执行 ,返回如下响应:Background append only file rewriting shceduled
- 父进程执行 fork 创建子进程,等同于bgsave过程
- 父进程 fork 操作完毕之后,依然响应其他命令,所有修改命令依然写入aof_buffer,并写入aof_rewrite_buffer中
- 子进程根据内存快照,按照命令合并规则写入到新的 aof 文件
- 新 aof 文件写入完成之后,子进程通知父进程:
- 父进程把 aof 重写缓冲区的数据写入 新的 aof 文件
- 使用 新 aof 文件替换 旧的 aof 文件
AOF参数:
- appendonly:开启AOF
- appendfilename:appendonly-${port}.aof
- appendsync:建议everysec
- dir:/bigdiskpath
- no-appendfsync-on-rewrite:在进行aof重写的时候,是否需要执行正常的aof刷盘;设置成yes是不进行,这样可能会丢失些数据(同时在执行bgrewriteaof操作和主进程写aof文件的操作,两者都会操作磁盘,而bgrewriteaof往往会涉及大量磁盘操作,这样就会造成主进程在写aof文件的时候出现阻塞的情形,现在no-appendfsync-on-rewrite参数出场了。如果该参数设置为no,是最安全的方式,不会丢失数据,但是要忍受阻塞的问题)
- auto-aof-rewrite-percentage
- auto-aof-rewrite-min-save
RDB默认是打开的,按照:900s/1change,300s/10change,60s/10000chang
AOF默认是关闭的
# 3.3 RDB 和 AOF 比较
RDB | AOF | |
---|---|---|
恢复/启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全 | 丢数据 | 根据策略决定 |
轻重 | 重 | 轻 |
建议:关RDB,开AOF
如果 Redis 同时使用 RDB 和 AOF 持久化,Redis 会优先使用 AOF 进行恢复数据
# 3.4 常见问题与优化
# 3.4.1 fork问题
- 同步操作,会阻塞主线程
- 与内存量息息相关:内存越大,耗时越长
- 可以通过info stats命令查看最近一次fork的时长latest_fork_usec,可以对这个参数进行监控和告警
- 解决方案:
- 控制redis实例最大可用内存:maxmemory,小分片
- 监控latest_fork_usec
- 合理配置Linux内存分配策略:vm.overcommit_memory=1,当内存不太够时进行fork会报错
- 降低fork频率:减少bgsave、bgrewriteaof的执行
注:vm.overcommit_memory用来设置内存分配策略,有3个可选值0,1,2:
- 0:表示内核将检查是否有足够的可用内存,如果有足够的可用内存,内存申请通过,否则申请失败,并把错误返回给应用进程
- 1:表示内核允许超量使用内存直到用完为止(redis建议配置成这个,能保证fork能最大程度成功)
- 2:表示内核绝不过量使用内存,内存申请不能超过系统的最大内存
# 3.4.2 子进程开销和优化
fork操作生成的子进程其实会和主进程共享内存,只有当主进程有写入或修改操作时,子线程会生成一个副本 这就是为什么fork比较快
# 3.4.3 硬盘优化
- 不要和高硬盘负载服务部署一起:存储服务、消息队列
- no-appendfsync-on-rewirte设置成yes
- 根据写入量决定磁盘类型要不要使用SSD
- 单机多实例要控制好,共享资源实例之间的资源分配