集合数据操作效率
一个集合类型的值:
- 通过全局哈希表找到对应的哈希桶位置
- 在集合中再增删改查
影响因素
底层数据结构
使用哈希表实现的集合,要比使用链表实现的集合访问效率更高。
操作效率和这些操作本身的执行特点有关,比如读写一个元素的操作要比读写所有元素的效率高。
整数数组和双向链表都是顺序读写,时间复杂度基本是O(N),效率较低。
压缩列表
类似数组,数组中每个元素对应一个数据。
而压缩列表在表头有三个字段zlbytes、zltail和zllen,分别表示列表长度、列表尾的偏移量和列表中的entry个数;压缩列表在表尾还有一个zlend,表示列表结束。
- 第一个元素、最后一个元素,可通过表头三字段的长度直接定位,复杂度O(1)
- 查找其他元素时,就只能逐个查找,复杂度O(N)
跳表
有序链表只能逐一查找元素,非常慢,于是出现跳表。
跳表基于链表,增加多级索引,通过索引位置跳转,快速定位数据:
查找过程就是在多级索引上跳来跳去,最后定位到元素。故名“跳”表。
数据量很大时,跳表查找复杂度O(logN)。
不同操作的复杂度
不同操作的复杂度
集合类型的操作类型:
- 读写单个集合元素的
如HGET、HSET - 操作多个元素的
如SADD - 遍历整个集合操作
如SMEMBERS
各种骚操作复杂度不尽相同,事关选型。
单元素操作
每种集合类型对单个数据实现的crud操作。例如,Hash类型的HGET、HSET和HDEL,Set类型的SADD、SREM、SRANDMEMBER等。这些操作的复杂度由集合采用的数据结构决定,如:
- HGET、HSET和HDEL操作哈希表,所以复杂度都是O(1)
- Set类型用哈希表作为底层数据结构时,SADD、SREM、SRANDMEMBER复杂度也是O(1)
集合类型支持同时对多个元素进行增删改查,如:
- Hash类型的HMGET和HMSET
- Set类型的SADD也支持同时增加多个元素
这些操作的复杂度,就是由单个元素操作复杂度和元素个数决定的。如HMSET增加M个元素时,复杂度就从O(1)变成O(M)。
范围操作
集合类型中的遍历操作,可返回集合所有数据,如Hash类型的HGETALL和Set类型的SMEMBERS,或者返回一个范围内的部分数据,如:
- List类型的LRANGE
- ZSet类型的ZRANGE
复杂度一般是O(N),应尽量避免。
Redis从2.8版本开始提供SCAN系操作(HSCAN,SSCAN和ZSCAN),实现渐进式遍历,每次只返回有限数量的数据。这相比HGETALL、SMEMBERS,避免了一次性返回所有元素而导致Redis过久阻塞。
统计操作
集合类型对集合中所有元素个数的记录,例如LLEN和SCARD。
复杂度只有O(1),因为当集合类型采用压缩列表、双向链表、整数数组这些数据结构时,这些结构中专门记录了元素的个数统计,可高效完成。
例外情况
某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于List类型的LPOP、RPOP、LPUSH、RPUSH这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有O(1),可以实现快速操作。
总结
Redis之所以能快速操作键值对,因为:
- O(1)复杂度的哈希表被广泛使用,包括String、Hash和Set,它们的操作复杂度基本由哈希表决定
- Sorted Set也采用了O(logN)复杂度的跳表
集合类型的范围操作,因为要遍历底层数据结构,复杂度通常是O(N)。
建议用其他命令来替代,如SCAN来代替,避免在Redis内部产生费时的全集合遍历操作。
List底层实现结构:双向链表和压缩列表的操作复杂度都是O(N)。
因地制宜使用List类型。既然它的POP/PUSH效率很高,那就将它主要用于FIFO队列场景,而不是作为一个可随机读写的集合。