Redis 入门

2023/2/22

本文主要内容来自:《一站式学习Redis从入门到高可用分布式实践》 (opens new window)

# 1 Redis总览

# 1.1 特性

1.png

# 1.2 应用场景

DD1B0D87-B526-4F31-B947-43F0F664C2C1.png

# 2 数据结构简单介绍

使用 redis 3.0 版本

# 2.1 安装与启动

# 2.1.1 安装目录文件

3.png

# 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 通用命令

  1. keys [pattern]:返回redis中所有的key
    1. keys he*:返回he开头的key
  2. dbsize:返回redis中key-value的数量
  3. exists key:key是否存在
  4. del key:删除key,可以删除多个del key1 key2...
  5. 过期时间:
    1. expire key seconds:设置key的过期时间
    2. ttl key:查看key剩余时间
    3. persist key:去掉过期时间
  6. 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ɔː]

4.png

# 2.2.3 单线程

  1. 一次只运行一条命令
  2. 拒绝长(慢)命令:keys,flush all,flushdb,会堵住后面的命令
  3. 有些功能不是单线程:fysnc file descriptor

为什么这么快?

  1. 纯内存
  2. 非阻塞IO,select/poll/epoll
  3. 单线程优势:避免线程切换和静态消耗

# 2.2.4 基本数据结构一:字符串

image.png

mgset和mset的优点: 5.png 1次mget = 1次网络时间 + n次命令时间

应用:

  1. 某个人主页被访问次数,key = userid:pageview;incr userid:pageview(单线程、无并发问题)
  2. 分布式Id
  3. incr id

# 2.2.5 基本数据结构二:哈希(命令以h开头)

6.png

命令 含义 命令 含义
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开头)

7.png

特点:有序、可以重复、支持左右操作

命令 含义 命令 含义
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不能有重复元素、无序、支持集合之间的操作 8.png

命令 含义 命令 含义
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开头,有序集合)

9.png

集合(set) vs 有序集合(zset)

  1. 均无重复元素(zset, score能重复,element不能重复)
  2. set无序,zset有序
  3. 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 扩展功能

10.png

# 2.3.1 慢查询日志

redis可以记录执行时间超过某个时长的命令,使用先进先出的队列保存,这个队列固定长度,且是保存在内存中 ;最好是一段时间持久化一次慢查询,因为超过慢查询队列长度,先进的慢查询就会被删除

配置:

  1. slowlog-max-len:慢查询队列长度,默认是128
  2. slowlog-log-slower-than:慢查询日志阈值(微妙,1毫秒 = 1000微妙),默认是10 000 = 10毫秒
    1. 设置0,记录所有命令
    2. 设置小于0,不记录任何命令
    3. 建议设置成1毫秒(redis通常的qps最少是10000/1秒,每条命令平均0.1毫秒)

方法:1. 修改配置文件重启;2. 动态配置:config set ...

相关命令:

  1. slowlog get n :获取n条慢查询
  2. slowlog len:获取慢查询队列有多少个慢查询
  3. slowlog reset:清空慢查询

11.png

# 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的区别:

12.png

pipeline命令不是原子的

13.png

# 2.3.3 发布订阅

14.png

注意和消息队列模型的区别:发布订阅是每个订阅者都能收到,消息队列是每个消息只能被一个消费者消费

命令:

  1. publish channel message:publish sohu:tv "hello world"
  2. subscribe channel:subscribe sohu:tv
  3. unsubscribe channel

# 2.3.4 bitMap(位图)

bitmap底层是字符串类型,所以value最大为512M

字符串big对应的二进制:

15.png

命令:

  1. setbit key offset 0/1:把key对于的value的第offset为修改为0或1
    1. 如果value只有10位,执行setbit key 50 1,那么10-49都会自动补0
  2. getbit key offset
  3. 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(极小空间完成独立数量统计)

底层是字符串,原理是伯努利概率论相关的东西

命令:

  1. pfadd key element1 element2...:向hyperloglog添加元素
  2. pfcount key:计算这个key的独立总数
  3. pfmerge destkey sourcekey1 sourcekey1...:合并多个hyperloglog key到另外一个destkey

比bitmap消耗内存量更小,100万个用户,大约只能消耗15KB 缺点:

  1. 有一定的错误率:0.81%
  2. 无法知道具体的用户id

# 2.3.6 geo(地理位置)

应用:摇一摇、附近的酒店

# 3 持久化

持久化的方式:

  1. 快照:MySQL Dump、Redis RDB
  2. 写日志:MySQL binlog、Redis AOF

# 3.1 快照 RDB(Redis DataBase)

16.png

stop-writes-on-bgsave-error:如果等于 yes

假设 :创建快照(硬盘上,产生一个新的rdb文件)需要 20s时间,redis主进程,在这20s内,会继续接受客户端命令,但是,就在这20s内,创建快照出错了,比如磁盘满了,那么redis会拒绝新的写入,也就是说,它认为,你当下,持久化数据出现了问题,你就不要再set了

命令 save bgsave
IO类型 同步 异步
阻塞 是(阻塞发生在fork)
优点 不会消耗额外内存 不太阻塞redis

# 3.2 日志 AOF(Append Only File)

17.png

# 3.2.1 AOF重写

aof重写:例如重写之前

  • set hello world
  • set hello java
  • set hello heee

重写之后:set hello hehe,节约磁盘,加速数据恢复

AOF重写流程:

18.png

  • 执行 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参数:

  1. appendonly:开启AOF
  2. appendfilename:appendonly-${port}.aof
  3. appendsync:建议everysec
  4. dir:/bigdiskpath
  5. no-appendfsync-on-rewrite:在进行aof重写的时候,是否需要执行正常的aof刷盘;设置成yes是不进行,这样可能会丢失些数据(同时在执行bgrewriteaof操作和主进程写aof文件的操作,两者都会操作磁盘,而bgrewriteaof往往会涉及大量磁盘操作,这样就会造成主进程在写aof文件的时候出现阻塞的情形,现在no-appendfsync-on-rewrite参数出场了。如果该参数设置为no,是最安全的方式,不会丢失数据,但是要忍受阻塞的问题
  6. auto-aof-rewrite-percentage
  7. 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问题

  1. 同步操作,会阻塞主线程
  2. 与内存量息息相关:内存越大,耗时越长
  3. 可以通过info stats命令查看最近一次fork的时长latest_fork_usec,可以对这个参数进行监控和告警
  4. 解决方案:
    1. 控制redis实例最大可用内存:maxmemory,小分片
    2. 监控latest_fork_usec
    3. 合理配置Linux内存分配策略:vm.overcommit_memory=1,当内存不太够时进行fork会报错
    4. 降低fork频率:减少bgsave、bgrewriteaof的执行

注:vm.overcommit_memory用来设置内存分配策略,有3个可选值0,1,2:

  • 0:表示内核将检查是否有足够的可用内存,如果有足够的可用内存,内存申请通过,否则申请失败,并把错误返回给应用进程
  • 1:表示内核允许超量使用内存直到用完为止(redis建议配置成这个,能保证fork能最大程度成功)
  • 2:表示内核绝不过量使用内存,内存申请不能超过系统的最大内存

# 3.4.2 子进程开销和优化

fork操作生成的子进程其实会和主进程共享内存,只有当主进程有写入或修改操作时,子线程会生成一个副本 这就是为什么fork比较快

# 3.4.3 硬盘优化

  1. 不要和高硬盘负载服务部署一起:存储服务、消息队列
  2. no-appendfsync-on-rewirte设置成yes
  3. 根据写入量决定磁盘类型要不要使用SSD
  4. 单机多实例要控制好,共享资源实例之间的资源分配