文章目录
- 前言
- 一、Redis相关命令详解及原理
- 1.1 string、set、zset、list、hash
- 1.1.1 string
- 1.1.2 list
- 1.1.3 hash
- 1.1.4 set
- 1.1.5 zset
- 1.2 分布式锁的实现
- 1.3 lua脚本解决ACID原子性
- 1.4 Redis事务的ACID性质分析
- 二、Redis协议与异步方式
- 2.1 Redis协议解析
- 2.1.1 redis pipeline
- 2.1.2 Redis协议图
- 2.2 特殊协议操作-订阅发布
- 2.3 异步redis协议
- 2.3.1 hiredis + libevent
- 总结
前言
本文介绍了Redis相关命令以及Redis当中的一些概念(协议)。
一、Redis相关命令详解及原理
内存是稀缺资源,所以:
- 当数据量少时,存储效率高为主
- 当数据量多时,运行速度快为主
1.1 string、set、zset、list、hash
- string 是一个安全的二进制字符串(兼容’\0’作为分隔符,安全指按长度);
- 双端队列 (链表) list :有序(插入有序);
- 散列表 hash:对顺序不关注,field 是唯一的;
- 无序集合 set:对顺序不关注,里面的值都是唯一的;
- 有序集合 zset :对顺序是关注的,里面的值是唯一的;根据 member 来确定唯一;根据 score 来 确定有序;
1.1.1 string
set key_test 1000
get key_test
# 原子减一
decr key_test
decrby key_test decrement(一个数字)
# 原子加一
incr key_test
incrby key_test increment
# set Not exist,当key_test存在时,什么也不做,否则等同于set
setnx key_test value
del key_test
----------
#
setbit key_test offset value
# 第offset位设置为value
getbit key_test offset
# 统计字符串被设置为1的bit数
bitcount key_test
----------
应用
- 对象存储:set,get
- 累加器:incr
- 分布式锁:setnx
- 位运算:setbit,getbit,bitcount
1.1.2 list
双向链表,首尾操作时间复杂度O(1);中间元素操作O(n)
- list.size < 48 不压缩
- 元素压缩前后长度差不超过8,不压缩
为什么压缩?如何压缩的?
# 从队列左侧入队
lpush key value ...
lpop key
# 从队列右侧入队
rpush key value ...
rpop key
# 尾索引
lrange key start end
# 从存于 key 的列表里移除前 count 次出现的值为 value 的元素
lrem key count value
# rpop的阻塞版本
brpop key timeout
应用
- 栈:lpush + lpop
- 队列:lpush + rpop
- 阻塞队列:lpush + brpop
- 异步消息队列
- 操作和队列一样,但是在不同系统间;生产者和消费者;
- 获取固定窗口记录
ltrim key 0 4
保留最近5条记录
1.1.3 hash
散列表;C++ unordered_map
(节点数量 > 512 || 所有字符串长度 > 64) 采用dict
(节点数量 <= 512 || 所有字符串长度 < 64) 采用ziplist
hget key field
hgetall
hset key field value
# 设置多个键值对
hmset key field1 value1 field2 value2 field3 value3 ... fieldn valuen
hmget key field1 field2 ...
hincrby key field increment
# 获取有多少个键值对
hlen key
hdel key field
应用
- 存储对象
- 购物车:商品列表用list,其中属性用hash
1.1.4 set
无序集合
(元素都为整数 && 节点数量 <= 512) 采用整数数组存储
(元素不全为整数 || 节点数量 > 512) 采用字典存储
# 添加一个或多个
sadd key member ...
# 计算集合元素个数
scard key
smembers key
# 返回成员member是否为key的成员
sismember key member
# 随机返回key集合中的一个或多个元素
srandmember key [count]
# 移除一个随机元素
spop key [count]
# 返回差集
sdiff key [key...]
# 返回交集
sinter key [key...]
# 返回并集
sunion key [key...]
应用
- 抽奖:
srandmember
- 共同关注:
sdiff
;sinter
;sunion
1.1.5 zset
有序集合;实现排行榜;有序唯一
zadd key
# 从key中删除member的键值对
zrem key member [member...]
# 返回有序集key中member的score值
zscore key member
# 成员member的score值加上增量
zincrby key increment member
# 返回个数
zcard key
# 返回排名
zrank key member
# 返回指定范围的元素
zrange key start stop
# 返回指定范围的元素(逆序)
zrevrange key start stop
应用
- 百度热榜
- 延时队列
- 分布式定时器
- 时间窗口限流
1.2 分布式锁的实现
释放锁操作:事务操作
锁:谁持有,谁释放
get dislock
-- 释放锁
local uuid = redis.call("get", KEYS[1])
if uuid == KEYS[2] then
redis.call("del", KEYS[1])
end
1.3 lua脚本解决ACID原子性
# 开启事务
multi
# 提交事务
exec
# 取消事务
discard
# 检测key的变动
watch
实际中是使用lua脚本
- redis 中加载了一个 lua 虚拟机;用来执行 redis lua 脚本;
- redis lua 脚本的执行是原子性的;
- 当某个脚本正在执行的时候,不会有其他命令或者脚本被执行;
- lua 脚本当中的命令会直接修改数据状态;
- lua 脚本 mysql 存储区别:MySQL存储过程不具备事务性,所以也不具备原子性;
注意:如果项目中使用了 lua 脚本,不需要使用上面的事务命令
1.4 Redis事务的ACID性质分析
-
A 原子性;事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败;redis 不支持回滚;即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直 到将事务队列中的所有命令都执行完毕为止。
-
C 一致性;事务的前后,所有的数据都保持一个一致的状态,不能违反数据的一致性检测;这里 的一致性是指预期的一致性而不是异常后的一致性;所以 redis 也不满足;这个争议很大:redis 能 确保事务执行前后的数据的完整约束;但是并不满足业务功能上的一致性;比如转账功能,一个扣 钱一个加钱;可能出现扣钱执行错误,加钱执行正确,那么最终还是会加钱成功;系统凭空多了 钱;
set zhang 1000
lpush zhang 1 3 4 #error
get mark
-
I 隔离性;各个事务之间互相影响的程度;redis 是单线程执行,天然具备隔离性;
-
D 持久性;redis 只有在 aof 持久化策略的时候,并且需要在 appendfsync=always 才具备持久性;实际项目中几乎不会使用 redis.conf 中 aof 持久化策略;
-
面试时候回答:lua 脚本满足原子性和隔离性;一致性和持久性不满足;
get zhang ==>100
set zhang 200
如果这两个命令没有作为一个整体,那么可以会有另一条连接set。这将导致数据不一致。
什么时候探讨事务?多条并发连接
什么时候探讨原子操作?多核
二、Redis协议与异步方式
2.1 Redis协议解析
2.1.1 redis pipeline
redis pipeline 是一个客户端提供的机制,而不是服务端提供的;
注意:pipeline 不具备事务性;
目的:节约网络传输时间;
通过一次发送多次请求命令,从而减少网络传输的时间。
2.1.2 Redis协议图
上图描述了如何界定数据包:
- 长度 + 二进制流
- 二进制流 + 特殊分割符
2.2 特殊协议操作-订阅发布
为了支持消息的多播机制,redis引入了发布订阅:发送者发送消息,订阅者接收消息。
# 订阅频道
subscribe `channel`
# 订阅模式频道
psubscribe `channel`
# 取消订阅频道
unsubscribe `channel`
# 发布具体频道或模式频道的内容
publish `channel` `message`
# 客户端接收具体频道内容
message `specificChannel` `message`
# 客户端接收模式频道内容
pmessage
应用:
- 发布订阅可以收到redis主动推送的内容
- 项目中支持发布订阅,需要另开一条连接
缺点:
- 生产者传递来一条消息,redis找到相应的消费者并传递过去,如果没有消费者,消息丢弃;
- 如果有两个消费者,此时其中一个消费者挂掉了,重连上来将不会接收到该消息;
- redis停机重启,发布订阅的消息不会持久化。
2.3 异步redis协议
同步连接方案采用阻塞IO来实现:
优点:代码书写是同步的,业务逻辑不割裂
缺点:阻塞当前线程,直到返回结果,通常需要多个线程来实现线程池来解决效率问题。
异步连接方案采用非阻塞IO来实现:
优点:不阻塞当前线程,redis没有返回,可以继续向redis发送命令
缺点:代码书写是异步的,业务逻辑割裂,可以通过协程解决(skynet,openresty)
2.3.1 hiredis + libevent
我们需要做的事情:
适配:
- 事件对象
- 事件操作函数
hiredis需要做:
- 协议解析
- 读写事件
- 缓冲区操作
- 协议加密等
适配事件对象和函数。
static int redisAttach(reactor_t *r, redisAsyncContext *ac) {
redisContext *c = &(ac->c);
redis_event_t *re;
/* Nothing should be attached when something is already attached */
if (ac->ev.data != NULL)
return REDIS_ERR;
/* Create container for ctx and r/w events */
re = (redis_event_t*)hi_malloc(sizeof(*re));
if (re == NULL)
return REDIS_ERR;
re->ctx = ac;
re->e.fd = c->fd;
re->e.r = r;
// dont use event buffer, using hiredis's buffer
re->e.in = NULL;
re->e.out = NULL;
re->mask = 0;
ac->ev.addRead = redisAddRead;
ac->ev.delRead = redisDelRead;
ac->ev.addWrite = redisAddWrite;
ac->ev.delWrite = redisDelWrite;
ac->ev.cleanup = redisCleanup;
ac->ev.data = re;
return REDIS_OK;
}
总结
本文介绍了Redis的基本命令以及Redis协议中的部分内容。Redis是内存型数据库,围绕着内存的特性,Redis结合了lua脚本,分布式锁(最快的),异步连接等一系列特性。
参考链接:
https://github.com/0voice