【Redis教程0x03】详解Redis的基本数据类型

引言

根据【Redis教程0x02】中介绍的,Redis的数据类型可分为5种基本数据类型(String、Hash、List、Set、Zset)和4种高级数据类型(BitMap、HyperLogLog、GEO、Stream)。在本篇博客中,我们将详解这9种数据类型,分别从介绍、内部实现、常用指令、应用场景 四个维度来说明。此外,还要埋个伏笔,Redis的9种数据类型的底层实现主要依赖了8种数据结构(SDS、LinkedList、Dict、SkipList、Intset、ZipList、QuickList),我们后面也会讨论。废话不多说,让我们从5种基本数据类型开始学习吧。

这里有个在线Redis环境,网页上就能使用:https://try.redis.io/

基本数据类型

字符串String

介绍

字符串String是Redis中最基本也是我们最常用的数据类型,它是基于key-value结构,key是唯一标识,value是具体的值。String是一种二进制安全的数据类型,其value可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的base64编码或者解码或者url)、序列化后的对象。value最多可以容纳的数据长度是512MB

底层实现

Redis的String类型的底层数据结构实现主要是int和SDS(Simple Dynamic String,简单动态字符串)。虽然Redis是用C语言写的,但是SDS跟C语言的原生字符串并不相同,具体表现如下:

  • SDS不仅可以保存文本数据,还可以保存二进制数据 。因为SDS使用len属性的值而不是空字符串来判断是否结束,并且SDS的所有API都会处理二进制的方式来处理SDS存放在buf[]数组里的数据。这也是为什么SDS能存放文本、图片、音频、视频、压缩文件这样的二进制数据的原因。
  • SDS获取字符串长度的时间复杂度为O(1) 。C语言不记录字符串长度,所以获取复杂度为O(n)。而SDS结构里用len属性记录。
  • Redis的SDS API是安全的,拼接字符串不会造成缓冲区溢出。因为SDS在拼接字符串之前会检查SDS空间是否满足要求,空间不够会自动扩容,所以不会产生缓冲区溢出的问题。

字符串对象的内部编码(encoding)有3种:intrawembstr

如果一个字符串保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int

如果字符串对象保存的是一个字符串,且字符串的长度<=32字节(Redis 2.+版本),那么字符串对象将使用一个简单动态字符串SDS来保存这个字符串,并将对象的编码设置为embstrembstr编码是专门用于保存短字符串的一种优化编码方式:

如果字符串对象保存的是一个字符串,且字符串的长度>32字节(Redis 2.+版本),那么字符串对象将使用一个SDS来保存这个字符串,并将对象的编码设置为raw

注意,这里embstr编码和raw编码的边界在redis不同版本中是不一样的:

  • redis 2.+是32字节;
  • redis 3.0-4.0 是39字节;
  • redis 5.0 是44字节;

embstrraw都会用SDS来保存值,但是不同之处在于embstr会通过一次内存分配函数来分配一块连续的内存空间来保存 redisObjectSDS,而raw编码会通过调用两次分配函数来分别分配两块空间保存 redisObjectSDS

这样做的好处如下:

  • embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为1次。
  • 释放embstr编码的字符串对象同样只需调用一次内存释放函数。
  • 数据都放在一块连续的内存空间,可以更好利用CPU缓存提升性能。

但肯定也是有缺点的:

  • 如果字符串长度增加到需要重新分配内存时,整个redisObjectSDS都需要重新分配空间,所以 **embstr**编码的字符串对象实际上是只读的 ,Redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串对象进行任何修改时,程序会先将其转换为raw,再进行修改。

常用命令

基本操作:

java 复制代码
# 设置 key-value 类型的值
> SET name iq50
OK
# 根据 key 获得对应的 value
> GET name
"iq50"
# 判断某个 key 是否存在
> EXISTS name
(integer) 1
# 返回 key 所储存的字符串值的长度
> STRLEN name
(integer) 4
# 删除某个 key 对应的值
> DEL name
(integer) 1

批量设置:

java 复制代码
# 批量设置 key-value 类型的值
> MSET key1 hello key2 world 
OK
# 批量获取多个 key 对应的 value
> MGET key1 key2 
1) "hello"
2) "world"

计数器(字符串的内容为整数时可用):

java 复制代码
# 设置 key-value 类型的值
> SET number 0
OK
# 将 key 中储存的数字值增一
> INCR number
(integer) 1
# 将key中存储的数字值加 10
> INCRBY number 10
(integer) 11
# 将 key 中储存的数字值减一
> DECR number
(integer) 10
# 将key中存储的数字值键 10
> DECRBY number 10
(integer) 0

设置过期时间(默认为永不过期):

java 复制代码
# 设置 key 在 60 秒后过期(该方法是针对已经存在的key设置过期时间)
> EXPIRE name  60 
(integer) 1
# 查看数据还有多久过期
> TTL name 
(integer) 51

#两种方式设置 key-value 类型的值,并设置该key的过期时间为 60 秒
> SET key  value EX 60
OK
> SETEX key  60 value
OK

不存在就插入:

java 复制代码
# 不存在就插入(not exists)
>SETNX key value
(integer) 1

应用场景

  1. 缓存对象

使用String来缓存对象有两种方式:

  • 直接缓存对象的整个JSON,例:SET user:1 '{"name":"iq50", "age":24}'
  • 基于key将对象的属性分离,例:MSET user:1:name iq50 user:1:age 24
  1. 进行计数

因为Redis处理命令是单线程,所以命令的执行过程是原子的。因此String数据类型适合计数的场景,比如计算访问次数、点赞、转发、库存数量等。

例:计算文章的阅读数量

java 复制代码
# 初始化编号为108的文章的阅读量
> SET aritcle:No.108:readcount 0
OK
#阅读量+1
> INCR aritcle:No.108:readcount
(integer) 1
#阅读量+1
> INCR aritcle:No.108:readcount
(integer) 2
#阅读量+1
> INCR aritcle:No.108:readcount
(integer) 3
# 获取对应文章的阅读量
> GET aritcle:No.108:readcount
"3"
  1. 分布式锁

SET命令有个参数NX表示"key不存在才插入",可以借此实现分布式锁。

  • 如果key不存在,则显示插入成功,可以表示加锁成功;
  • 如果key存在,则显示插入失败,表示加锁失败;

一般而言,还会对分布式锁加上过期时间,命令如下:

java 复制代码
SET lock_key unique_value NX PX 10000

# lock_key就是key键
# unique_value是客户端生成的唯一标识
# NX表示只有key不存在,才进行插入
# PX表示过期时间,10*10000ms=10s,这是为了避免客户端异常而无法正常释放锁

而解锁的过程就是将key键lock_key删除,但是要保证执行删除的客户端就是加锁的那个客户端,所以解锁的时候我们要借助unique_value判断是否就是加锁的那个客户端,是的话才允许删除lock_key。

  1. 共享Session信息

作为服务端,通常会使用Session来保存用户的会话状态,但这只适合用于单服务器应用,如果是分布式系统将不适用。比如用户A的Session信息被存储在服务器A,但用户A第二次访问时被负载均衡调度到服务器B,B没有存储用户A的Session,就会出现重复登陆的问题。

因此可以借助Redis对这些Session信息进行统一的存储和管理,保证无论请求发到哪个服务器,服务器都会从同一Redis获取相关信息,解决了分布式系统中Session存储的问题。

列表List

介绍

List列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向List列表添加元素(所以这是个双向链表)。列表的最大长度为2**32-1,也即每个列表支持超过40亿个元素。

内部实现

List类型的底层数据结构是由双向列表或压缩列表实现的:

  • 如果列表的元素个数<512个(默认值,可由list-max-ziplist-entries配置),列表每个元素的值都<64字节(默认值,可由list-max-ziplist-value配置),Redis会使用压缩列表作为List类型的底层数据结构。
  • 如果列表的元素不满足上面的条件,Redis会使用双向链表作为List底层的数据结构。

但是在Redis 3.2版本以后,List数据类型的底层数据结构只由quicklist实现了,替代了双向链表和压缩列表

常用命令

java 复制代码
# 将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面
LPUSH key value [value ...] 
# 将一个或多个值value插入到key列表的表尾(最右边)
RPUSH key value [value ...]
# 移除并返回key列表的头元素
LPOP key     
# 移除并返回key列表的尾元素
RPOP key 

# 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始
LRANGE key start stop

# 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BLPOP key [key ...] timeout
# 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BRPOP key [key ...] timeout

应用场景

  1. 消息队列(有缺陷的)

消息队列在存取消息时,必须满足三个需求,分别是消息保序处理重复的消息保证消息可靠性

Redis的List和Stream两种数据类型,就可以满足消息队列的这三个需求。这里先介绍List,后面讲到Stream时会再详解。
Q1:如何满足消息保序需求?

List本身就是按照FIFO先进先出的顺序进行存取,所以本身就满足消息的保序需求。List可以使用LPUSH+RPOP命令实现消息队列。

不过,在消费者读取数据时,有一个潜在的性能风险点。

在生产者往List中写入数据时,List并不会主动通知消费者有新消息写入,如果消费者想要及时处理消息,就需要在程序中不停进行RPOP命令(比如使用一个while循环)。这必然会导致消费者不必要的性能损失。

为了解决这个问题,Redis提供了BRPOP命令。BRPOP命令也称为阻塞式读取(Blocking Right POP),客户端在没有读取到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据
Q2:如何处理重复的消息?

消费者要实现重复消息的判断,需要2个方面的需求:

  • 每个消息都有一个全局的ID。
  • 消费者要记录已经处理过的消息ID。当收到一条消息后,消费者程序就可以对比这个消息ID和已经处理过的消息ID。如果已经处理过,就不再处理了。

但是List并不会为每个消息生成ID号,所以我们需要自行为每个消息生成一个全局唯一ID。生成之后,再使用LPUSH把消息插入List,并在这个消息中包含全局唯一ID。

例如:

java 复制代码
# 生成一条全局ID:111000102、库存量99的消息插入消息队列
> LPUSH mq "111000102:stock:99"
(integer) 1

Q3:如何保证消息可靠性?

当消费者程序从List中读取一条消息后,List就不会再留存这条消息了。所以如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么消费者程序再次启动后,就没法再次从List中读取消息了。

为了留存消息,List类型提供了BRPOPPUSH命令,这个命令的作用是让消费者程序从一个List中读取消息,同时,Redis会把这个消息再插入到另一个List(备份List)留存 。这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份List中重新读取消息并处理了。

至此,我们解答了List作为消息队列的三大需求的问题。总结一下:

  • 消息保序:使用LPUSH+RPOP;
  • 重复消息处理:生产者自行实现全局唯一ID;
  • 消息可靠性:使用BRPOPPUSH;

画个分割线,这一段的标题说用List作为消息队列是有缺陷的,具体是什么呢?

List不支持多个消费者消费同一条消息,因为一个消费者RPOP消息,List中就删除了此消息,无法被其他消费者再次消费。

要想实现多消费者消费同一条消息,就要将多个消费者构造成一个消费组,但遗憾的是List不支持这么做,不过在Redis 5.0以后,引入的Stream支持,后面我们会再详细讨论。

哈希Hash

介绍

Redis中的Hash是一个键值对field-value的映射表,特别适合用来存储对象。

Redis中String和Hash的区别如下:

可以看到,对于Redis而言,String是key-->value的,而Hash是key-->[{field1,value1}, {field2,value2},...{fieldN,valueN}]。

内部实现

Hash类型的底层数据结构是由压缩列表哈希表实现的:

  • 如果哈希类型元素个数小于512个(默认,可由hash-max-ziplist-entries设置),所有值小于64字节(默认,可由hash-max-ziplist-value设置)的话,Redis会使用压缩列表作为Hash底层数据结构。
  • 否则,Redis使用哈希表作为Hash底层数据结构。

在Redis 7.0中,压缩列表数据结构已经废弃,交由listpack数据结构实现

常用命令

java 复制代码
# 存储一个哈希表key的键值
HSET key field value   
# 获取哈希表key对应的field键值
HGET key field

# 在一个哈希表key中存储多个键值对
HMSET key field value [field value...] 
# 批量获取哈希表key中多个field键值
HMGET key field [field ...]       
# 删除哈希表key中的field键值
HDEL key field [field ...]    

# 返回哈希表key中field的数量
HLEN key       
# 返回哈希表key中所有的键值
HGETALL key 

# 为哈希表key中field键的值加上增量n
HINCRBY key field n   

应用场景

  1. 缓存对象

Hash类型的_(key、field、value)的结构与对象的(id、属性、值)_结构类似,因此很适合拿来存储对象。例:存储用户信息:

java 复制代码
# 存储一个哈希表uid:1的键值
> HMSET uid:1 name Tom age 15
2
# 存储一个哈希表uid:2的键值
> HMSET uid:2 name Jerry age 13
2
# 获取哈希表用户id为1中所有的键值
> HGETALL uid:1
1) "name"
2) "Tom"
3) "age"
4) "15"

那么和String对比,一般的对象用String存JSON就行了,如果是某些属性频繁变化的对象,考虑用Hash来存储。

  1. 购物车

用户id为key,商品id为field,商品数量为value,恰好构成购物车的要素。

  • 添加商品:HSET cart:用户id 商品id 1;
  • 添加数量:HINCRBY cart:用户id 商品id 1;
  • 商品总数:HLEN cart:用户id;
  • 删除商品:HDEL cart:用户id 商品id;
  • 获取购物车所有商品:HGETALL cart:用户id;

当然,这里只是拿到了商品的id,在回显商品具体信息的时候,还是要拿商品id去查数据库的。

集合Set

介绍

Set类型是一种无序集合,集合中的元素没有先后顺序(无序 )但是都唯一 ,类似于Java中的HashSet。一个集合最多可以存储2**32-1个元素。我们可以基于Set轻易实现求交集、并集、差集的操作。

内部实现

Set类型的底层数据结构是哈希表整数集合实现的:

  • 如果集合中的元素都是整数且元素个数小于512,Redis会采用整数集合作为Set的底层数据结构;
  • 否则,Redis会使用哈希表;

常用命令

Set的基本操作:

java 复制代码
# 往集合key中存入元素,元素存在则忽略,若key不存在则新建
SADD key member [member ...]
# 从集合key中删除元素
SREM key member [member ...] 
# 获取集合key中所有元素
SMEMBERS key
# 获取集合key中的元素个数
SCARD key

# 判断member元素是否存在于集合key中
SISMEMBER key member

# 从集合key中随机选出count个元素,元素不从key中删除
SRANDMEMBER key [count]
# 从集合key中随机选出count个元素,元素从key中删除
SPOP key [count]

Set运算操作:

java 复制代码
# 交集运算
SINTER key [key ...]
# 将交集结果存入新集合destination中
SINTERSTORE destination key [key ...]

# 并集运算
SUNION key [key ...]
# 将并集结果存入新集合destination中
SUNIONSTORE destination key [key ...]

# 差集运算
SDIFF key [key ...]
# 将差集结果存入新集合destination中
SDIFFSTORE destination key [key ...]

应用场景

集合Set的主要特性就是:无序、不可重复、支持并交叉等。

但是有个注意点,Set求差集、并集、交集的计算复杂度较高,在数据量大的情况下,不建议做这些操作。在主从集群中,为了避免主库因为Set做这些操作导致阻塞,我们可以选择一个从库做这些运算,然后把结果返回主库。

  1. 点赞

Set可以保证一个用户只能点一个赞。例:文章的id作为key,用户的id作为value。

java 复制代码
# uid:1 用户对文章 article:1 点赞
> SADD article:1 uid:1
(integer) 1
# uid:2 用户对文章 article:1 点赞
> SADD article:1 uid:2
(integer) 1
# uid:3 用户对文章 article:1 点赞
> SADD article:1 uid:3
(integer) 1

# uid:1取消了点赞
> SREM article:1 uid:1
(integer) 1

# 获取哪些人点赞了
> SMEMBERS article:1
1) "uid:3"
2) "uid:2"

# 统计点赞的数量
> SCARD article:1
(integer) 2
  1. 共同关注

Set可以做交集,因此可以用来计算共同关注的好友、公众号等。

用户id作为key,关注的公众号id作为value。

java 复制代码
# uid:1 用户关注公众号 id 为 5、6、7、8、9
> SADD uid:1  5 6 7 8 9
(integer) 5
# uid:2  用户关注公众号 id 为 7、8、9、10、11
> SADD uid:2  7 8 9 10 11
(integer) 5

# 获取共同关注
> SINTER uid:1 uid:2
1) "7"
2) "8"
3) "9"

# 给uid:2推荐uid:1关注的公众号
> SDIFF uid:1 uid:2
1) "5"
2) "6"
  1. 抽奖活动

存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。key为抽奖活动名,value为员工名称,把所有员工名称放入抽奖箱 :

java 复制代码
>SADD lucky Tom Jerry John Sean Marry Lindy Sary Mark
(integer) 5

# 如果允许重复中奖(使用命令 SRANDMEMBER)
# 抽取 1 个一等奖:
> SRANDMEMBER lucky 1
1) "Tom"
# 抽取 2 个二等奖:
> SRANDMEMBER lucky 2
1) "Mark"
2) "Jerry"
# 抽取 3 个三等奖:
> SRANDMEMBER lucky 3
1) "Sary"
2) "Tom"
3) "Jerry"

# 如果不允许重复中奖(抽中后直接剔除,使用命令SPOP)
# 抽取一等奖1个
> SPOP lucky 1
1) "Sary"
# 抽取二等奖2个
> SPOP lucky 2
1) "Jerry"
2) "Mark"
# 抽取三等奖3个
> SPOP lucky 3
1) "John"
2) "Sean"
3) "Lindy"

有序集合Zset(也叫Sorted Set)

介绍

Zset类型相比于Set多了一个排序的属性score。Zset保留了Set不重复的特性(但是score可以重复)。

内部实现

Zset类型的底层数据结构是用压缩列表跳表实现的:

  • 如果Zset的元素个数小于128个,且每个元素值小于64字节,Redis使用压缩列表;
  • 否则,使用跳表;

在Redis 7.0中,废弃了压缩列表,改用listpack数据结构。

常用命令

Zset常用操作:

java 复制代码
# 往有序集合key中加入带分值元素
ZADD key score member [[score member]...]   
# 往有序集合key中删除元素
ZREM key member [member...]                 
# 返回有序集合key中元素member的分值
ZSCORE key member
# 返回有序集合key中元素个数
ZCARD key 

# 为有序集合key中元素member的分值加上increment
ZINCRBY key increment member 

# 正序获取有序集合key从start下标到stop下标的元素
ZRANGE key start stop [WITHSCORES]
# 倒序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]

# 返回有序集合中指定分数区间内的成员,分数由低到高排序。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

# 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。
ZRANGEBYLEX key min max [LIMIT offset count]
# 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同
ZREVRANGEBYLEX key max min [LIMIT offset count]

Zset运算操作(相比Set,不支持差集):

java 复制代码
# 并集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
ZUNIONSTORE destkey numberkeys key [key...] 
# 交集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
ZINTERSTORE destkey numberkeys key [key...]

应用场景

  1. 排行榜

有序集合的典型场景就是排行榜,以博客点赞为例:

java 复制代码
# arcticle:1 文章获得了200个赞
> ZADD user:iq50:ranking 200 arcticle:1
(integer) 1
# arcticle:2 文章获得了40个赞
> ZADD user:iq50:ranking 40 arcticle:2
(integer) 1
# arcticle:3 文章获得了100个赞
> ZADD user:iq50:ranking 100 arcticle:3
(integer) 1
# arcticle:4 文章获得了50个赞
> ZADD user:iq50:ranking 50 arcticle:4
(integer) 1
# arcticle:5 文章获得了150个赞
> ZADD user:iq50:ranking 150 arcticle:5
(integer) 1

文章新增一个赞,使用ZINCRBY命令:

java 复制代码
> ZINCRBY user:iq50:ranking 1 arcticle:4
"51"

查看某篇文章的赞:

java 复制代码
> ZSCORE user:iq50:ranking arcticle:4
"50"

获取文章赞数最多的 3 篇文章,可以使用 ZREVRANGE 命令(倒序获取有序集合 key 从start下标到stop下标的元素):

java 复制代码
# WITHSCORES 表示把 score 也显示出来
> ZREVRANGE user:iq50:ranking 0 2 WITHSCORES
1) "arcticle:1"
2) "200"
3) "arcticle:5"
4) "150"
5) "arcticle:3"
6) "100"

获取 100 赞到 200 赞的文章,可以使用 ZRANGEBYSCORE 命令(返回有序集合中指定分数区间内的成员,分数由低到高排序):

java 复制代码
> ZRANGEBYSCORE user:iq50:ranking 100 200 WITHSCORES
1) "arcticle:3"
2) "100"
3) "arcticle:5"
4) "150"
5) "arcticle:1"
6) "200"
  1. 电话、姓名排序

使用有序集合的 ZRANGEBYLEXZREVRANGEBYLEX 可以帮助我们实现电话号码或姓名的排序,我们以 ZRANGEBYLEX (返回指定成员区间内的成员,按 key 正序排列,分数必须相同)为例。

java 复制代码
# 插入一些号码
> ZADD phone 0 13100111100 0 13110114300 0 13132110901 
(integer) 3
> ZADD phone 0 13200111100 0 13210414300 0 13252110901 
(integer) 3
> ZADD phone 0 13300111100 0 13310414300 0 13352110901 
(integer) 3

# 获取所有号码
> ZRANGEBYLEX phone - +
1) "13100111100"
2) "13110114300"
3) "13132110901"
4) "13200111100"
5) "13210414300"
6) "13252110901"
7) "13300111100"
8) "13310414300"
9) "13352110901"

# 获取[132,133)开头的号码
> ZRANGEBYLEX phone [132 (133
1) "13200111100"
2) "13210414300"
3) "13252110901"

同理,姓名排序:

java 复制代码
> zadd names 0 Toumas 0 Jake 0 Bluetuo 0 Gaodeng 0 Aimini 0 Aidehua 
(integer) 6

> ZRANGEBYLEX names - +
1) "Aidehua"
2) "Aimini"
3) "Bluetuo"
4) "Gaodeng"
5) "Jake"
6) "Toumas"

> ZRANGEBYLEX names [A (B
1) "Aidehua"
2) "Aimini"

> ZRANGEBYLEX names [C [Z
1) "Gaodeng"
2) "Jake"
3) "Toumas"

总结

本篇博客我们详细介绍了Redis中的5种基本数据类型:String、List、Hash、Set、Zset,以及其相关的底层实现、相关命令和使用场景等细节。 下一篇将详细介绍剩下的4种高级数据类型,敬请期待。

相关推荐
桀桀桀桀桀桀8 分钟前
数据库中的用户管理和权限管理
数据库·mysql
superman超哥1 小时前
04 深入 Oracle 并发世界:MVCC、锁、闩锁、事务隔离与并发性能优化的探索
数据库·oracle·性能优化·dba
用户8007165452001 小时前
HTAP数据库国产化改造技术可行性方案分析
数据库
minihuabei1 小时前
linux centos 安装redis
linux·redis·centos
engchina2 小时前
Neo4j 和 Python 初学者指南:如何使用可选关系匹配优化 Cypher 查询
数据库·python·neo4j
engchina2 小时前
使用 Cypher 查询语言在 Neo4j 中查找最短路径
数据库·neo4j
尘浮生2 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
威哥爱编程2 小时前
SQL Server 数据太多如何优化
数据库·sql·sqlserver
小华同学ai2 小时前
AJ-Report:一款开源且非常强大的数据可视化大屏和报表工具
数据库·信息可视化·开源
Acrelhuang3 小时前
安科瑞5G基站直流叠光监控系统-安科瑞黄安南
大数据·数据库·数据仓库·物联网