基础数据结构
redis的所有操作都是原子的,这得益于redis是单线程的结构。redis有5种基本的数据结构,分别是:
string
:字符串k-v
:键值对list
:链表hash
:哈希set
:集合zset
:有序集合
string
是动态字符串,通过预分配空间减少内存的频繁分配操作,string
在长度小于1M时,是成倍的增加现有空间;超过1M时,每次增加1M的空间,最大长度512MB。
键值对相当于一个字典,支持增删改查,从键值对的角度来看,redis本身就可以看成一个字典。k-v
结构中,k
是字符串,v
可以是字符串或者是整数。整数的情况下,可以使用incrby
命令实现自增操作。每个k-v
可以设置超时时间,使用expire
命令可以设置,比如expire myKey 5
,则myKey
在5秒后自动销毁。自增的大小,必须在signed long
之间,超过范围会报错。
list
是链式结构,双端插入删除的复杂度是
O
(
1
)
O(1)
O(1),索引定位的复杂度是
O
(
n
)
O(n)
O(n)。基本操作除了插入,索引下标之外,还有ltrim
操作,例如ltrim myList 1 3
是只保留myList
范围1-3的数据。但是,这个复杂度也是接近
O
(
n
)
O(n)
O(n)的,比如ltrim myList 1 -1
就是
O
(
n
)
O(n)
O(n)的复杂度。-1
表示最后一个元素,其余同理。链表在元素个数较少的情况下,会是ziplist
的结构,即元素存储在一个内存块中,这样提高空间利用率,减少内存碎片。
hash
是字典的结构,它的内部本身存储了很多键值对。
hash
的键值通过桶哈希的方式存储。当哈希冲突时,redis采取渐进式rehash操作。这个方式同时保留新旧两个hash表,在后续的定时任务或者操作中,旧表的内容会渐进式地移动到新的表,移动完成后,旧表删除,保留新表。
set
就相当于C++中的std::unordered_set<T>
结构,内部键值是无序唯一的,可以一次添加多个,或者删除多个,可以统计个数,判定是否是对应的成员等。
zset
结构是带有权重的k-v
操作,每个k
存储的v
是一个score
,该结构使用了跳表实现,元素根据score
实现有序,我们可以根据k
来索引元素,也可以根据score
来划分区间等操作。基本结构如下:
zset
可以用来存放粉丝列表,比如key
表示id
,score
表示时间等。
所有的容器,如果不存在,则操作时立刻新建一个;如果操作后没有元素了,则立刻删除。
分布式锁
分布式锁,不是我们传统意义上说的锁,比如在内存中使用锁的结构来保证同时只能有一个线程访问临界区;而是使用一个信号量的机制,设置一个标记,来表示当前是否有分布式的进程操作这个数据。
最简单的操作:
setnx mylock true
这相当于建立了一个锁,使用完成后,需要取消,命令是:
del mylock
不过,这么做有缺陷,比如设置完成锁后,且进程获取到了锁,但是进程此时由于某些原因挂了,那么就形成了死锁。一般的解决方案是设置超时时间,代码示例:
set mylock true ex 5 nx
这条命令把设置锁和设置超时结合起来,方式操作被终端,引发死锁。这是这是5秒的超时时间。上面的命令如果分开写,则是:
setnx mylock
expire mylock 5
注意,如果一个锁超过了设置的超时时间,但是使用锁的任务获取锁后,在超时时间过了之后还在操作临界区,而此时又有新的分布式进程设置了锁,那么就会出现问题。旧的进程会删除掉新进程的锁,这样会造成不安全的因素。
可重入锁可以采用引用计数的方式实现,比较麻烦,而且需要精确考虑超时时间,一般不推荐使用。这里给出可重入锁的基本使用方式,没有时间过期的方式:
如果加锁失败,可以选择直接抛出异常,或者sleep
定时时间不断重试,或者放入异步队列中等待重试。
延时队列
异步消息队列
该队列用于生产者和消费者模式,比如有多个分布式生产者和消费者,消费者获取生产者的数据,但是没有顺序要求。这更适合只有一组消费者的情况。
在这种情况下,可以使用list
作为异步队列,使用lpush
和rpop
读取数据。
如果队列空了,而且消费者比较多,那么可能出现消费者不断轮询redis服务器的情况,此时造成消费者CPU消耗高,同时造成redis的QPS过高。
这种情况,最简单的方式是消费者定时检测,每次轮询sleep(1)
。不过容易造成延迟。另一个方式,使用list
的blpop
或者brpop
进行操作,这是阻塞读,直到有数据放入队列中。注意,如果阻塞时间过长,redis可能会切断客户端的连接。这需要我们检测到异常时进行重试。一般redis或者有关的库可以配置空闲时间。
延时队列
使用zset
可以设置延时队列,每次取出超时最近的消息。
def delay():
msg = str(uuid.uuid4())
value = json.dump()
retry_ts = time.time() + 5
redis.zadd("delay_queue", retry_ts, value)
# loop函数会有多个线程执行,为了保证可用性
def loop():
while True:
values = redis.zrangebyscore("delay_queue", 0, time.time(), start=0, num=1)
if not values:
time.sleep(1)
continue
value = values(0)
success = redis.zrem("delay_queue", value) # 只有一个线程能成功执行删除操作
if success:
msg = json.load(value) # 执行删除成功的才可以真正获取数据
handle_msg(msg)
redis队列不保证数据的可靠性
消息不保证可靠,应该是消息被发送出去,消费者是否接收到消息redis不做保证,不像一般的mq,会有ack机制,要求消费者收到消息进行ack确认,超时未确认mq会再次投递消息,而redis没有这个机制。
高级结构
位图
相当于位操作。位图本身是自动扩展的,如果偏移量超过当前的内容返回,会自动填充。注意位图的填充模式,数组顺序和位图顺序是相反的。比如:
h w
01101000 01100101 # 从右向左
0110100001100101 # 从左向右
bitcount # 统计指定范围1的个数
bitops # 统计指定范围第一个1出现的位置
bitfield + get | set | incrby # 对区间进行操作
bitfield + overflow # 可以指定溢出的操作方式。默认是截断的
HyperLogLog
适用于不精确的统计情景。比如网页统计用户的访问量UV,需要对用户的ID进行去重操作。这个结构占用12KB的空间,一般来说,可能有0.x%的误差。只能添加,不能删除。
pfadd hll user1 # 添加数据 O(1)
pfadd count hll # 统计数据 O(1)
pfmerge hll0 hll1 hll2 # 1 2合并到0上,O(N)
布隆过滤器
布隆过滤器,是一个不太精确的set
结构,用于去重,但是节约90%的空间。基本原理是把数据哈希映射到比特位,之后根据比特位进行判断是否存在,如下图:
可以参考:https://www.cnblogs.com/zhanggguoqi/p/10571225.html
布隆过滤器,如果判断某个值存在,则此时该值可能不存在;但是如果判定一个值不存在,则一定不存在。
我们可以设置过滤器的错误率和容量。如果超过容量,那么误判率会快速上升。