关于java:史上最全Redis数据结构详解

52次阅读

共计 9436 个字符,预计需要花费 24 分钟才能阅读完成。

记得点赞 + 关注呦。
更多更好的文章,请关注公众号【蘑菇睡不着】,外面有知识点干货以及刷题相干的分享。

前言

在 Redis 最重要最根底就属 它丰盛的数据结构了,Redis 之所以能怀才不遇很大起因是他数据结构丰盛,能够反对多种场景。并且 Redis 的数据结构实现以及利用场景在面试中是相当常见的,接下来就和大家聊聊 Redis 的数据结构。

Redis 数据结构有:string、list、hash、set、sorted set 这五个是大家都晓得的,但 Redis 还有更高级得数据结构,比方:HyperLogLog、Geo、BloomFilter 这几个数据结构,接下来聊聊 Redis 得这些数据结构吧。

String

基本概念 String 是 Redis 最简略最罕用的数据结构,也是 Memcached 惟一的数据结构。在平时的开发中,String 能够说是应用最频繁的了。
底层实现

  • 如果一个字符串对象保留的是整数值,并且这个整数值能够用 long 类型来示意,那么字符串对象会将整数值保留在字符串对象构造的 ptr 属性外面(将 void* 转换成 long),并将字符串对象的编码设置为 int。
  • 如果字符串对象保留的是一个字符串值,并且这个字符串值的长度大于 39 字节,那么字符串对象将应用一个简略动静字符串(SDS)来保留这个字符串值,并将对象的编码设置为 raw。
  • 如果字符串对象保留的是一个字符串值,并且这个字符串值的长度小于等于 39 字节,那么字符串对象将应用 embstr 编码的形式来保留这个字符串值。

应用:

> redis_cli            # 启动 redis-cli 客户端
> set hello world      # 将键 hello 的值设置为 world   
OK                     # set 命令胜利后 会返回 OK
> get hello            # 通过 get 命令获取 键为 hello 的值
"world"                # 取得到的值
> del hello            # 删除键为 hello 的值
(integer) 1            # 返回的是删除的数量
> mset a 10 b 20 c 30  # 批量的设置值
OK
> mget a b c           # 批量的返回值
1)"10"
2)"20"
3)"30"
> exists hello         # 是否存在该键
(integer) 1            # 1 示意存在,0 示意不存在
> expire hello 10      # 给 hello 设置过期工夫,单位,秒
(integer) 1            # 返回 1 代表胜利,0 代表 key 不存在或无奈设置过期工夫
> pexpire hello 10     # 给 hello 设置过期工夫,单位,毫秒
(integer) 1            # 返回 1 代表胜利,0 代表 key 不存在或无奈设置过期工夫

接下来会重点讲一下 set key value [EX seconds] [PX milliseconds] [NX|XX] 这个一系列命令,这块还是挺重要的,也很容易混同。
reids 每次对 以前的值笼罩时,会 清空 TLL 值。(TTL 是过期工夫)

  • EX second:设置键的过期工夫为 second 秒。SET key value EX second 成果等同于 SETEX key second value。
  • PX millisecond:设置键的过期工夫为 millisecond 毫秒。SET key value PX millisecond 成果等同于 PSETEX key millisecond value。
  • NX:只在键不存在时,才对键进行设置操作。SET key value NX 成果等同于 SETNX key value。
  • XX:只在键曾经存在时,才对键进行设置操作。
# 应用 EX 选项
> set key1 hello EX 1000   # 设置 过期工夫 1000s
OK
> ttl hello                # 获取 hello 的过期工夫
(integer) 1000
# 应用 PX 选项
> set key1 hello PX 1000   # 设置 过期工夫 1000ms
OK
> ttl hello                # 获取 hello 的过期工夫
(integer) 1000
# 应用 NX 选项
> set hello world NX
OK                         # 键不存在,设置胜利
> get hello
"value"
> set hello world NX
(nil)                      # 键曾经存在,设置失败
> get hello
"world"                    # 维持原值不变
# 应用 XX 选项
> exists hello             # 先确定 hello 不存在
(integer) 0
> set hello world XX
(nil)                      # 因为键不存在,设置失败
> set hello wolrd          # 先给 hello 设置一个值
OK
> set hello newWolrd XX
OK                         # 这回设置胜利了
> get hello
"newWorld"
# NX 或 XX 能够和 EX 或者 PX 组合应用
> set hello world EX 1000 NX
OK
> get hello
"world"
> ttl hello
(integer)1000
> set hello wolrd PX 30000 NX
OK
> pttl hello
(integer)30000          # 实际操作中 这个值必定小于 30000,这次是为了成果才这么写的
# EX 和 PX 能够同时呈现,但前面给出的选项会笼罩后面给出的选项
> set hello wolrd EX 1000 PX 30000
OK
> ttl hello
(integer)30            # 这个是 PX 设置的参数,> pttl hello
(integer)30000
> set number 1
OK
> incr number          # 对 number 做自增操作
(integer) 2

在开发过程中,用 redis 来实现锁是很罕用的操作。联合 NX 以及 EX 来实现。

> set hello world NX EX 10     # 胜利加锁,过期工夫是 10s
OK
> set hello wolrd NX EX 10     # 在 10s 内执行这个命令返回谬误,因为上一次的锁还没有开释
(nil)
> del hello                    # 开释了锁
OK
> set hello world NX EX 10     # 胜利加锁,过期工夫是 10s
OK
> setnx hello world            # 也能够这么写
> setex hello 10 wolrd

锁能够通过设置过期工夫以及手动 del 删除来开释锁。
string 的命令比拟罕用就多介绍了点,上面的命令我就挑重点介绍了。

利用场景

  • 缓存性能:string 最罕用的就是缓存性能,会将一些更新不频繁然而查问频繁的数据缓存起来,以此来加重 DB 的压力。
  • 计数器:能够用来计数,通过 incr 操作,如统计网站的访问量、文章访问量等。

List

基本概念: list 是有序可反复列表,和 Java 的 List 蛮像的,查问速度快,能够通过索引查问;插入删除速度慢。

底层实现

  • 列表对象的编码能够是 ziplist 或者 linkedlist。
  • 列表对象保留的所有字符串元素的长度都小于 64 字节并且保留的元素数量小于 512 个,应用 ziplist 编码;否则应用 linkedlist;

应用

> lpush mylist a     # 从右边插入数据
(ineteger)1
> lpush mylist b
(integer)1
> rpush mylist c     # 从左边插入数据
(integer)1
> lrange mylist 0 -1 # 检索数据,lrange 须要两个索引,左闭右闭;0 就是从第 0 个,-1 是倒数第一个,-2 倒数第二个... 以此类推
1)"b"
2)"a"
3)"c"
> lrange mylist 0 -2 # 0 到 倒数第 2 个 
1)"b"
2)"a"
> lpush mylist a b c # 批量插入
(integer)3
> lpop mylist        # 从左侧弹出元素
"b"
> rpop mylist        # 从右侧弹出元素
"c"
> rpop mylist        # 当列表中没有元素时返回 null
(nil)
> brpoop mylist 5    # 从右侧弹出元素,如果列表没有元素,会阻塞住,如果 5 s 后还是没有元素则返回
1)"mylist"   # 列表名        
2)"b"        # 弹出元素
> del mylist         # 删除列表 
(integer)1

应用场景:

  • 音讯队列:Redis 的 list 是有序的列表构造,能够实现阻塞队列,应用左进右出的形式。Lpush 用来生产 从左侧插入数据,Brpop 用来生产,用来从右侧 阻塞 的生产数据。
  • 数据的分页展现:lrange 命令须要两个索引来获取数据,这个就能够用来实现分页,能够在代码中计算两个索引值,而后来 redis 中取数据。
  • 能够用来实现粉丝列表以及最新消息排行等性能。

Hash

简介 :Redis 散列能够存储多个键值对之间的映射。和字符串一样,散列存储的值既能够是字符串又能够是数值,并且用户同样能够对散列存储的数字值执行自增或自减操作。这个和 Java 的 HashMap 很像,每个 HashMap 有本人的名字,同时能够存储多个 k/v 对。
底层实现:

  • 哈希对象的编码能够是 ziplist 或者 hashtable。
  • 哈希对象保留的所有键值对的键和值的字符串长度都小于 64 字节并且保留的键值对数量小于 512 个,应用 ziplist 编码;否则应用 hashtable;

应用

> hset student name 张三    # 能够了解为忘名叫 student 的 map 中增加 kv 键值对
(integer)1            # 返回 1 代表 不存在这个 key,并且增加胜利
> hset student sex 男
(integer)1
> hset student name 张三
(integer)0            # 返回 0 因为这个 key 曾经存在
> hgetall student
1)"name"
2)"张三"
3)"sex"
4)"男"
> hdel student name       #删除这 key
(integer)1           # 返回 1 同样代表整个 key 存在 并且删除胜利
> hdel student name
(integer)0           # 返回 0 是因为 该 key 曾经不存在

利用场景

  • Hash 更适宜存储结构化的数据,比方 Java 中的对象;其实 Java 中的对象也能够用 string 进行存储,只须要将 对象 序列化成 json 串就能够,然而如果这个对象的某个属性更新比拟频繁的话,那么每次就须要从新将整个对象序列化存储,这样耗费开销比拟大。可如果用 hash 来存储 对象的每个属性,那么每次只须要更新要更新的属性就能够。
  • 购物车场景:能够以用户的 id 为 key,商品的 id 为存储的 field,商品数量为键值对的 value,这样就形成了购物车的三个因素。

Set

基本概念 :Redis 的 set 和 list 都能够存储多个字符串,他们之间的不同之处在于,list 是有序可反复,而 set 是无序不可反复。
底层实现

  • 汇合对象的编码能够是 intset 或者 hashtable。
  • 汇合对象保留的所有元素都是整数值并且保留的元素数量不超过 512 个,应用 intset 编码;否则应用 hashtable;

应用

> sadd family mother          # 尝试将 mother 增加进 family 汇合中
(integer)1       # 返回 1 示意增加胜利,0 示意元素曾经存在汇合中
> sadd family father
(integer)1
> sadd family father
(intger)0
> smembers family             # 获取汇合中所有的元素
1)"mother"
2)"father"
> sismember family father     # 判断 father 是否在 family 汇合中 
(integer)1      # 1 存在;0 不存在
> sismber family son
(integer)0
> srem family son             # 移除 family 汇合中元素 son
(integer)1     # 1 示意存在并且移除胜利;0 示意存在该元素
> srem family som
(integer)0
> sadd family1 mother
(integer)1
> smembers family 
1)"mother"
2)"father"
> smember family1
1)"mother"
> sinter family family1     # 获取 family 和 family1 的交加
1)"mother"
> sadd family1 son
(integer)1
> sunion family family1     # 获取 family 和 family1 的并集
1)"mother"
2)"father"
> sdiff family family1      # 获取 family 和 family1 的差集(就是 family 有然而 family1 没有的元素)1)"father"

利用场景

  • 标签:能够将博客网站每个人的标签用 set 汇合存储,而后还按每个标签 将用户进行归并。
  • 存储好友 / 粉丝:set 具备去重性能;还能够利用 set 并集性能失去独特好友之类的性能。

Sorted Set

基本概念 :有序汇合和散列一样,都用于存储键值对:其中有序汇合的每个键称为成员(member),都是举世无双的,而有序汇合的每个值称为分值(score),都必须是浮点数。能够依据分数进行排序,有序汇合是 Redis 外面惟一既能够依据成员拜访元素(这一点和散列一样),又能够依据分值以及分值的排列程序来拜访元素的构造。和 Redis 的其余构造一样,用户能够对有序汇合执行增加、移除和获取等操作。
底层实现

  • 有序汇合的编码能够是 ziplist 或者 skiplist
  • 有序汇合保留的元素数量小于 128 个并且保留的所有元素成员的长度都小于 64 字节。应用 ziplist 编码;否则应用 skiplist;

应用

> zadd class 100 member1 # 将 member1 元素及其 score 值 100 退出到 有序汇合 class 中
(integer)1
> zadd class 90 member2 80 member3 # 批量增加
(integer)2
> zrange class 0 -1 withscores  # 获取有序汇合中的值与 score,并按 score 排序
1)"member3" 
2)"80"
3)"member2"
4)"90"
5)"member1"
6)"100"
> zrem class member1   # 删除 class 中 的 member1
(integer)1

利用场景

  • 排行榜:有序汇合最罕用的场景。如新闻网站对热点新闻排序,比方依据点击量、点赞量等。
  • 带权重的音讯队列:重要的音讯 score 大一些,一般音讯 score 小一些,能够实现优先级高的工作先执行。

    HyperLogLog

    基本概念
    Redis 在 2.8.9 版本增加了 HyperLogLog 构造。

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的长处是,在输出元素的数量或者体积十分十分大时,计算基数所需的空间总是固定 的、并且是很小的。

在 Redis 外面,每个 HyperLogLog 键只须要破费 12 KB 内存,就能够计算靠近 2^64 个不同元素的基 数。这和计算基数时,元素越多消耗内存就越多的汇合造成鲜明对比。

然而,因为 HyperLogLog 只会依据输出元素来计算基数,而不会贮存输出元素自身,所以 HyperLogLog 不能像汇合那样,返回输出的各个元素。
应用
这里就拿一个统计网站 2021 年 5 月 23 日,有多少用户登录举例

> pfadd user_login_20210523 tom #  user_login_20210523 是 key;tom 是登录的用户
(integer)1
> pfadd user_login_20210523 tom jack lilei 的用户
(integer)1
> pfcount user_login_20210523  # 获取 key 对应值的数量,同一个用户屡次登录只统计一次
(integer) 3  
> pfadd user_login_20210522 sira 
(integer)1
> pfcount user_login_20210523 user_login_20210522 # 统计 22 号和 23 号一共有多少登陆的用户
(integer)4
>pfmerge user_login_20210522_23 user_login_20210522 user_login_20210523 # 将连个键内容合并
"OK"
> pfcount user_login_20210522_23
(integer)4

利用场景

  • 能够用来统计网站的登陆人数以及其余指标

GEO

基本概念
在 Redis 3.2 版本中新增了一种叫 geo 的数据结构,它次要用来存储地理位置信息,并对存储的信息进行操作。
应用
geoadd 用于存储指定的天文空间地位,能够将一个或多个经度(longitude)、纬度(latitude)、地位名称(member) 增加到指定的 key 中。

> GEOADD beijing 116.405285 39.912835 "蘑菇睡不着"
(integer)2

geopos 用于从给定的 key 里返回所有指定名称 (member) 的地位(经度和纬度),不存在的返回 nil。

> GEOPOS beijing "蘑菇睡不着" "故宫"
1) 1)116.405285
   2)39.912835
2)(nil)   

geodist 用于返回两个给定地位之间的间隔。
单位参数:
m:米,默认单位。
km:千米。
mi:英里。
ft:英尺。

> GEOADD beijing 116.403681 39.921156 "故宫"
(integer)1
> GEODIST beijing "蘑菇睡不着" "故宫" km
"0.936"

利用场景
用于存储地理信息以及对地理信息作操作的场景。
** 科普一个天文小常识:
经度范畴:-180 – 180。从 0°经线算起,向东、向西各分作 180°,以东的 180°属于东经,习惯上用“E”作代号,以西的 180°属于西经,习惯上用“W”作代号。0°地位是:英国格林威治(Greenwich)天文台子午仪核心的经线为本初子午线。
纬度范畴:-90 – 90。位于赤道以北的点的纬度叫北纬,记为 N;位于赤道以南的点的纬度称南纬,记为 S。为了钻研问题不便,人们把纬度分为低、中、高纬度。0°~30°为低纬度,30°~ 60°为中纬度,60~90°为高纬度。**

BloomFilter

基本概念
一种数据结构,是由一串很长的二进制向量组成,能够将其看成一个二进制数
组。既然是二进制,那么外面寄存的不是 0,就是 1,然而初始默认值都是 0。他的次要作用是:判断一个元素是否在某个汇合中。比如说,我想判断 20 亿的号码中是否存在某个号码,如果间接插 DB,那么数据量太大工夫会很慢;如果将 20 亿数据放到 缓存 中,缓存也装不下。这个时候用 布隆过滤器 最合适了,布隆过滤器的原理是:

  1. 增加元素
    当要向布隆过滤器中增加一个元素 key 时,咱们通过多个 hash 函数,算出一个值,而后将这个值所在的方格置为 1。
  2. 判断元素是否存在:
    判断元素是否存在,是先将元素通过多个 hash 函数计算,计算到多个下标值,而后判断这些下标对应的元素值是否都为 1,如果存在不是 1 的,那么元素必定不在汇合中;如果都是 1,那么元素大概率在汇合中,并不能百分之百必定元素存在汇合中,因为多个不同的数据通过 hash 函数算进去的后果是会有反复的,所以会存在某个地位是别的数据通过 hash 函数置为的 1。
    总的来说:布隆过滤器能够判断某个数据肯定不存在,然而无奈判断肯定存在。
  3. 布隆过滤器的优缺点:
  4. 长处:长处很显著,二进制组成的数组,占用内存极少,并且插入和查问速度都足够快。
  5. 毛病:随着数据的减少,误判率会减少;还有无奈判断数据肯定存在;另外还有一个重要毛病,无奈删除数据。

应用
redis 4.0 后能够应用 布隆过滤器的插件RedisBloom,命令如下:

bf.add 增加元素到布隆过滤器
bf.exists 判断元素是否在布隆过滤器
bf.madd 增加多个元素到布隆过滤器,bf.add 只能增加一个
bf.mexists 判断多个元素是否在布隆过滤器

> bf.add boomFilter tc01
(integer) 1            # 1:存在;0:不存在
> bf.add boomFilter tc02
(integer) 1
> bf.add boomFilter tc03
(integer) 1
> bf.exists boomFilter tc01
(integer) 1
> bf.exists boomFilter tc02
(integer) 1
> bf.exists boomFilter tc03
(integer) 1
> bf.exists boomFilter tc04
(integer) 0
> bf.madd boomFilter tc05 tc06 tc07
1) (integer) 1
2) (integer) 1
3) (integer) 1
> bf.mexists boomFilter tc05 tc06 tc07 tc08
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0

  1. Redisson 应用布隆过滤器 :
public static void main(String[] args) {Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.15.105:6379");
        config.useSingleServer().setPassword("password123");
        
        // 结构 Redisson
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("userPhones");
        
        // 初始化布隆过滤器:预计元素为 500000000L, 误差率为 3%
        bloomFilter.tryInit(500000000L,0.03);
        
        // 将号码 10086 插入到布隆过滤器中
        bloomFilter.add("18846014678");

         // 判断上面号码是否在布隆过滤器中
        System.out.println(bloomFilter.contains("18846014678")); //true
        System.out.println(bloomFilter.contains("1111111222")); //false
 }

  1. Guava 应用布隆过滤器:
    Guava 是谷歌提供的 Java 工具包,性能十分弱小
public static void main(String[] args) {BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 500000, 0.01);

        bloomFilter.put("18846047789");

        System.out.println(bloomFilter.mightContain("18846047789")); // true
        System.out.println(bloomFilter.mightContain("1122222"));     //false
    }
}

利用场景

  • 解决缓存穿透问题:个别得查问场景都是先去查问缓存,如果缓存没有,那么就去 DB 查问,如果查到了,先存在 缓存 中,而后返回给调用方。如果查不到就返回空。这种状况如果有人频繁的申请缓存中没有得数据,比方 id = -1 得数据,那么会对 DB 造成极大得压力,这种状况就能够应用 redis 得布隆过滤器了,能够先将可能得 id 都存在布隆过滤器中,当查问来的时候,先去布隆过滤器查,如果查不到间接返回,不申请缓存以及 DB,如果存在 布隆过滤器 中,那么才去缓存中取数据。
  • 黑名单校验:能够将黑名单中得 ip 放入到布隆过滤器中,这样不必每次来都去 db 中查问了。

总结

Redis 丰盛的数据结构是撑持 Redis 重要基石之一。他使 Redis 能够适应很多简单的场景。这块的内容在面试中能够说是必考的内容,所以要在这方面要多下些功夫。
更多的常识干货以及 leetcode 题解析,能够来公众号 蘑菇睡不着 看看,可能会对你有帮忙。
看到这里记得 点赞 + 关注 + 转发,不要白嫖呦

正文完
 0