为什么Redis能这么快
- 完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高
- 数据结构简单,对数据操作也简单
- 采用单线程,单线程也能处理高并发请求
- 由于采用单线程,在高并发情况下避免了使用锁
- 使用多路I/O复用模型,非阻塞IO
Redis供用户使用的数据类型
String
最基本的数据类型,二进制安全(二进制安全意味着可以保存任何数据,比如图片,最大512M)
set name "aaa" // ok
get name // "aaa"
127.0.0.1:6379> set count 1
OK
127.0.0.1:6379> incr count
(integer) 2
127.0.0.1:6379> get count
"2"
127.0.0.1:6379> del count
(integer) 1
Hash
String元素组成的字典,适合用于存储对象
127.0.0.1:6379> hmset ant name "ant" age 23
OK
127.0.0.1:6379> hget ant age
"23"
127.0.0.1:6379> hset ant name ant666
(integer) 0
127.0.0.1:6379> hget ant
(error) ERR wrong number of arguments for 'hget' command
127.0.0.1:6379> hgetall ant
1) "name"
2) "ant666"
3) "age"
4) "23"
List
列表,按照String元素插入顺序排序
127.0.0.1:6379> lpush mylist aaa
(integer) 1
127.0.0.1:6379> lpush mylist bbb
(integer) 2
127.0.0.1:6379> lpush mylist ccc
(integer) 3
127.0.0.1:6379> lrange mylist 0 10
1) "ccc"
2) "bbb"
3) "aaa"
127.0.0.1:6379> rpush mylist ddd
(integer) 4
127.0.0.1:6379> lrange mylist 0 10
1) "ccc"
2) "bbb"
3) "aaa"
4) "ddd"
你可以添加一个元素到列表的头部(左边)或者尾部(右边)
利用lpush
可以实现后进先出的栈结构
Set
String元素组成的无序集合,通过哈希表实现,不允许重复。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
127.0.0.1:6379> sadd myset 111
(integer) 1
127.0.0.1:6379> sadd myset 222
(integer) 1
127.0.0.1:6379> sadd myset 333
(integer) 1
127.0.0.1:6379> sadd myset 333
(integer) 0
127.0.0.1:6379> smembers myset
1) "111"
2) "222"
3) "333"
Sorted Set
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
127.0.0.1:6379> zadd myzset 3 abc
(integer) 1
127.0.0.1:6379> zadd myzset 2 aaa
(integer) 1
127.0.0.1:6379> zadd myzset 1 abd
(integer) 1
127.0.0.1:6379> zadd myzset 2 abd
(integer) 0
127.0.0.1:6379> zrange myzset 0 10
1) "aaa"
2) "abd"
3) "abc"
127.0.0.1:6379> zrange myzset 0 10 withscores
1) "aaa"
2) "2"
3) "abd"
4) "2"
5) "abc"
6) "3"
从海量Key里查询出某一固定前缀的Key
SCAN cursor [MATCH pattern] [COUNT count]
以0作为游标开始一次新的迭代,知道命令返回游标0完成一次遍历
127.0.0.1:6379> scan 0 match my* count 10
1) "6"
2) 1) "myzset"
2) "mystring"
如何使用Redis实现分布式锁
分布式锁需要解决的问题
- 互斥性
- 安全性
- 死锁
- 容错
SETNX
如果key不存在,则创建并赋值
- 时间复杂度:O(1)
- 返回值:设置成功,返回1;设置失败,返回0
127.0.0.1:6379> setnx locknx test
(integer) 1
127.0.0.1:6379> setnx locknx aaa
(integer) 0
127.0.0.1:6379> get locknx
"test"
EXPIRE 解决SETNX长期有效的问题
- 设置key的生存时间,当key过期时,会被自动删除
127.0.0.1:6379> expire locknx 2
(integer) 1
127.0.0.1:6379> get locknx
(nil)
127.0.0.1:6379> setnx locknx aaa
(integer) 1
但是这样操作会有缺点,原子性得不到满足。当setnx之后还没来得及执行EXPIRE时程序挂掉了,就会导致锁一直不会被释放。于是便有了SET key value [EX seconds] [PX milliseconds] [NX|XX]
SET的同时设置EXPIRE
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX second:设置键的过期时间为xx秒
- PX millisecond: 设置键的过期时间为xx毫秒
- NX:只在键不存在时,才对键进行设置操作
- XX: 只在键已经存在时,才对键进行设置操作
- SET操作成功完成时,返回OK,否则返回nil
127.0.0.1:6379> set locktest test EX 10 NX
OK
127.0.0.1:6379> set locktest test1 EX 10 NX
(nil)
127.0.0.1:6379> set locktest test1 EX 10 NX
OK
如何使用Redis实现异步队列
使用List作为队列,LPUSH/RPUSH生产消息,RPOP/LPOP消费消息
127.0.0.1:6379> lpush mylist aaa
(integer) 1
127.0.0.1:6379> lpush mylist bbb
(integer) 2
127.0.0.1:6379> lpush mylist ccc
(integer) 3
127.0.0.1:6379> rpop mylist
"aaa"
127.0.0.1:6379> rpop mylist
"bbb"
127.0.0.1:6379> rpop mylist
"ccc"
127.0.0.1:6379> rpop mylist
(nil)
缺点:没有等待队列里有值,就直接消费
弥补:通过应用层引入Sleep机制循环调用POP重试
这里引入一种不需要Sleep的方法,BLPOP/BRPOP key [key ...] timeout
BLPOP/BRPOP
BLPOP/BRPOP key [key ...] timeout
阻塞直到队列有消息或者超时
这种方式实现队列依旧有缺点,只能供一个消费者消费。解决方法是引入pub/sub主题订阅者模式
pub/sub 主题订阅者模式
- 发送者(pub)发送消息,订阅者(sub)接收消息
- 订阅者可以订阅任意数量的频道
127.0.0.1:6379> SUBSCRIBE topic
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "topic"
3) (integer) 1
重新开启个 redis 客户端,然后在同一个频道 redisChat 发布两次消息,订阅者就能接收到消息。
127.0.0.1:6379> PUBLISH topic "hello"
(integer) 1
127.0.0.1:6379> PUBLISH topic "i love you"
(integer) 1
订阅者的客户端会显示如下消息
1) "message"
2) "topic"
3) "hello"
1) "message"
2) "topic"
3) "i love you"
缺点:消息的发布是无状态的,无法保证可达。解决这类问题就需要用专业的消息队列了,Rabbitmq等
Redis如何做持久化
RDB(快照)持久化
可通过配置文件配置redis自动备份,也可通过命令手动备份
- SAVE 阻塞Redis的服务器进程,知道RDB文件被创建完毕
- BGSAVE Fork出一个子进程来创建RDB文件,不阻塞服务器进程
以上命令将在 redis 安装目录中创建dump.rdb文件,如果需要恢复数据,只需将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可。
缺点:
- 内存数据的全量同步,数据量大会由于I/O而严重影响性能
- 可能会因为Redis挂掉而丢失从当前至最近一次快照期间的备份
AOF(Append-Only-File)持久化:保存写状态
- 记录下除了查询以外的所有变更数据库状态的指令
- 以append的形式追加保存到AOF文件中(增量)
日志重写可以解决AOF文件大小不断增大的问题。
RDB和AOF的优缺点
- RDB优点:全量数据快照,文件小,恢复快
- RDB缺点:无法保存最近一次快照之后的数据
- AOF优点:可读性高,适合保存增量数据,数据不易丢失
- AOF缺点:文件体积大,恢复时间长
RDB-AOF混合模式
- BGSAVE做镜像全量持久化,AOF做增量持久化
Redis管道技术
Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤:
- 客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。
- 服务端处理命令,并将结果返回给客户端。
Redis 管道技术可以在服务端未响应时,客户端可以继续向服务端发送请求,并最终一次性读取所有服务端的响应。
Pipeline批量执行指令,节省多次IO往返时间,提高了Redis服务性能。
Redis 全同步过程
- slave发送sync命令到master
- master启动一个后台进程,将Redis中的数据快照保存到文件中
- master将保存数据快照期间接收到的写命令缓存起来
- master完成写文件操作后,将该文件发送给slave
- 使用新的AOF文件替换掉旧的AOF文件
- master将这期间收集的增量写命令发送给slave端
Redis 增量同步过程
- master接收到用户的操作指令,判断是否需要传播到slave
- 将操作记录追加到AOF文件
- 将操作传播到其他slave
- 将缓存中的数据发送给slave
Redis Sentinel
Redis主从结构如果master挂了那么整个集群会处于不可用状态,Redis哨兵就是为了解决这个问题
- 监控:检查主从服务器是否运行正常
- 提醒:通过API向管理员或其他应用程序发送故障通知
- 自动故障迁移:主从切换
主流Redis应用
主从复制 + Redis哨兵 + 一致性Hash算法