基本数据结构包含:字符串(strings)、 散列(hashes)、 列表(lists)、 集合(sets)、 有序集合(sorted sets)五种。这五种数据结构在我们工作中经常使用到,面试过程中经常被问到,因此熟练掌握这5种基本数据结构的使用和应用场景是Redis知识最基础也是最重要的部分。
字符串是Redis最简单的储存类型,它存储的值可以是字符串、整数或浮点数,对整个字符串或字符串的其中一部分执行操作;对整数或者浮点数执行自增(increment)或者自减(decrement)操作。
Redis的字符串是一个由字节组成的序列,跟java里面的ArrayList有点类似,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际串长度len。当字符串长度小于1M时,扩容加倍成现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
字符串类型在工作中使用广泛,主要用于缓存数据,提高查询属性。比如存储登录信息,电商中存储商品信息、可以做计数器等等。
插入字符串 java > set username zhangsan "OK" 获取字符串 > get username "zhangsan" 插入多个字符串 >mset age 18 address bj "OK" 获取多个字符串 >mget username age 1) "zhangsan" 2) "18" 自增 >incr num "1" >incr num "2" 自减 >decr num "1" 指定步长自增 >incrby num 2 "3" >incrby num 2 "5" 指定步长自减 >decrby num 3 "2" 删除 >del num "1"
散列相当于Java中的HashMap,内部是无序字典。实现原理跟HashMap一致。一个哈希表有多个节点,每个节点保存一个键值对。
与Java中HashMap不同的是,rehash的方式不一样,因为Java在HashMap在字典很大时,redhash是个耗时的操作,需要一次性全部rehash。Redis为了高性能,不能堵塞服务,所以采用了渐进式rehash策略。
渐进式rehash会有rehash的同时,保留新旧两个hash结构,查询时会同时查询两个 hash结构,然后在后续的定时任务中以及hash操作指令中,循序渐进地将旧hash的内容一占占迁移到新的hash结构中。当搬迁完成了,就会使用新的hash结构取而代之。
当hash移除了最后一个元素之后,该数据结构自动被删除,内存被回收。
Hash也可以同于对象存储,比如存储用户信息,与字符串不一样的是,字符串是需要将对象进行序列化(比如json序列化)之后才能保存,而Hash则可以讲用户对象的每个字段单独存储,这样就能节省序列化和反序列的时间。如下:
此外还可以保存用户的购买记录,比如key为用户id,field为商品id,value为商品数量。同样还可以用于购物车数据的存储,比如key为用户id,field为商品id,value为购买数量等等。
# 设置属性 hset keyname field1 value1 field2 value2 # 获取某个属性值 hget keyname field # 获取所有属性值 hgetall keyname # 删除某个属性 hdel keyname field # 获取属性个数 hlen keyname # 按照步长自增/自减某个属性(该属性必须是数字) hincrby keyname field step
# 插入 hash 数据 >hset userInfo username zhangsan age 18 address bj "3" # 获取 hash 单条 field 数据 >hget userInfo username "zhangsan" >hget userInfo age "18" # 获取 hash 多个 field 数据 >hmget userInfo username age 1) "zhangsan" 2) "18" # 获取 hash 所有 field 数据 >hgetall userInfo 1) "username" 2) "zhangsan" 3) "age" 4) "18" 5) "address" 6) "bj" # 获取 hash 的 field 个数 >hlen userInfo "3" # 自增 hash 的某个 field >hincrby userInfo age 2 "20" >hincrby userInfo age 2 "22" # 自减 hahs 的某个 field(通过自增负步长达到) >hincrby userInfo age -2 "20" # 删除 hash 的某个 field >hdel userInfo age "1" # 删除 hash 所有数据 >del userInfo "1"
Redis中的lists相当于Java中的LinkedList,实现原理是一个双向链表(其底层是一个快速列表),即可以支持反向查找和遍历,更方便操作。插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。
lists的应用场景非常多,可以利用它轻松实习热销榜;可以实现工作队列(利用lists的Push操作,将任务存在list中,然后工作线程再用POP操作将任务取出进行执行);可以实现最新列表,最热评论等。
# 左进 lpush key value1 value2 value3... # 左出 lpop key # 右进 rpush key value1 value2 value3... # 右出 rpop key # 从左往右读取 start和end是下标 lrange key start end 实操 # 从 list 左边依次插入 >lpush student zhangsan lisi wangwu "3" # 从 list 右边插入 >rpush student tianqi "4" # 从 list 左边弹出一个 >lpop liangshan "wangwu" # 从 list 右边弹出一个 >rpop liangshan "tianqi" # 获取 list 下标 0 ~ 1 的数据(左闭右闭) >lrange liangshan 0 1 1) "lisi" 2) "zhangsan"
注意:blpop阻塞版获取
为什么要阻塞版本的pop呢,主要是为了避免轮询。举个简单的例子如果我们用list来实现一个工作队列。执行任务的thread可以调用阻塞版本的pop去获取任务这样就可以避免轮询去检查是否有任务存在。当任务来时候工作线程可以立即返回,也可以避免轮询带来的延迟。