Redis

一. Redis基础

1.什么是Redis

Redis是基于C语言开发的开源NoSQL数据库,与传统数据库不同,它的数据保存在内存中(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存。Redis存储KV键值对数据,同时Redis支持多种数据类型,如String、Hash、Sorted Set、Bitmap等。

2.Redis为什么快?

Redis内部做了很多性能优化。

  • Redis基于内存,内存的访问速度很快。
  • Redis内置了多种优化后的数据结构,性能很高。
  • Redis基于Reactor模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和IO多路复用技术。

3.Memcached和Redis的异同

共同点:

  • 二者都是内存数据库,一般都用来做缓存。
  • 都有过期策略。
  • 性能都比较高。

区别:

  • Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
  • Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。
  • Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。但是 Redis 原生支持集群模式。
  • Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 

4.为什么要用Redis?

  • 高性能

用户第一次访问数据库中的某些数据时会比较慢,因为需要从硬盘中读取数据,但如果用户访问的数据是高频数据并且不经常被修改,那么可以将这些数据保存到缓存中。这样做能够保证用户下一次访问时直接从缓存中读取数据,操作缓存就是操作内存,速度非常快。

  • 高并发

MySQL数据库的QPS(服务器每秒可以执行的查询次数)大概在1w左右,但Redis很容易达到10w+,甚至30w,并且这还只是单机情况下,集群模式下会更高。因此,直接操作缓存能承受的数据库请求量远远大于直接访问数据库,所以可以将部分数据保存在缓存中,使得部分请求落入缓存,不经过数据库,提升系统的并发性能。

5.常见的缓存读写策略

主要有3种:旁路缓存模式、读写穿透模式、异步缓存模式。

(1)旁路缓存模式(Cache Aside Pattern)

平时使用较多的缓存模式,适合读请求较多的场景。服务端需要同时维系DB和Cache,并且以DB的结果为准。

读写步骤:

写:

  • 先更新 db。
  • 然后直接删除 cache 。

 :

  • 从 cache 中读取数据,读取到就直接返回。
  • cache 中读取不到的话,就从 db 中读取数据返回。
  • 再把数据放到 cache 中。

问题1:在写过程中,可以先删除cache,后更新db吗?

回答1:不可以。这样可能会导致数据库与缓存中数据不一致的问题。比如:请求A先把cache中的数据(x=1)删除,随后请求B从db中读取数据(x=1),然后把更新到cache,最后请求A又把db中的数据更新(x=2)。

问题2:那么先更新db,再删除cache,就不会导致数据不一致问题吗?

回答2:理论上讲仍然可能出现数据不一致问题,但概论很小。因为缓存的写入速度比数据库的写入速度快很多。比如:请求A先从db中读数据(x=1),请求B随后更新db中的数据(x=2),并且此时没有缓存,所以不需要删除,最后请求A再把数据(x=1)写入cache。

旁路缓存策略的缺陷:

  • 首次请求的数据一定不在cache中。

解决办法:可以将热点数据提前放入cache。

  • 写操作比较频繁的话,导致cache中的数据被频繁删除,影响缓存命中率。

解决办法:

①数据库与缓存数据强一致场景:更新db时同样更新cache,但加一个锁来保证更新cache时不存在线程安全问题。

②短暂允许数据库与缓存数据不一致场景:更新db时同样更新cache,但给cache设置一个比较短的过期时间,这样就保证即使数据不一致影响也比较小。

(2)读写穿透模式(Read/Write Through Pattern)【简单看】

读写穿透模式中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。

这种缓存读写策略在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入 db 的功能。

读写步骤:

写:

  • 先查 cache,cache 中不存在,直接更新 db。
  • cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。

读:

  • 从 cache 中读取数据,读取到就直接返回 。
  • 读取不到的话,先从 db 加载,写入到 cache 后返回响应。

读写穿透模式实际是在旁路缓存模式之上进行了封装。在旁路缓存模式下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而读写穿透模式则是 cache 服务自己来写入缓存,这对客户端是透明的。

(3)异步缓存模式(Write Behind Pattern)【简单看】

异步缓存模式和读写穿透模式很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。

但是,二者又有很大的不同:读写穿透模式是同步更新 cache 和 db,而异步缓存则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。

实际开发中相对少见,但仍有场景使用:消息队列中消息异步写入磁盘等。异步缓存模式下db的写性能非常高,适合数据经常变化并且对数据一致性要求不高的场景,如浏览量、点赞量。

二. Redis应用

1.Redis除了做缓存,还能做什么?

分布式锁、限流、消息队列等。

2.基于Redis实现的分布式锁

(1)为什么需要分布式锁?

在多线程环境下,可能存在多个线程同时访问共享资源,会发生资源竞争问题,影响系统正常运行。为了保证共享资源被安全地访问,需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。

如何实现资源的互斥访问?悲观锁。共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

单机多线程环境下,常用本地锁即可解决:

分布式系统下,不同的客户端运行在独立的JVM进程中,因此使用分布式锁:

(2)分布式锁应具备的条件:

  • 互斥:任意一个时刻,锁只能被一个线程持有。
  • 高可用:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。
  • 可重入:一个节点获取了锁之后,还可以再次获取锁。

除了上面这三个基本条件之外,一个好的分布式锁还需要满足下面这些条件:

  • 高性能:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。
  • 非阻塞:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。

(3)分布式锁的实现:

Redis提供了分布式锁的实现方案,如SETNX。

如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在,保持原值不变。

假设我们有一个存储用户登录状态的Redis键值对,其中键为"user:login:1",值为"true",表示用户1已登录。现在我们想要添加一个第二个用户的登录状态,键为"user:login:2",值为"true"。

如果使用SET命令进行设置:SET user:login:2 true 会覆盖之前的键值对,将user:login:2的值设置为true。因此无法区分不同用户的登录状态。使用SETNX命令进行设置:SETNX user:login:2 true  SETNX命令会先判断键"user:login:2"是否存在。如果不存在,它会在Redis数据库中创建一个新的键值对,键为"user:login:2",值为"true";如果键已经存在,SETNX命令则不进行任何操作。

释放锁,通过DEL命令删除对应的key即可。

为了避免误删锁,可以使用Lua脚本,通过key对应的value值来判断,保证解锁的原子性。也就是将获取value、判断value和删除锁这三步写入Lua,Redis会将整个Lua脚本作为一个整体执行,中间不会被其他命令插入

同时通过给锁设置一个过期时间,来避免锁无法被释放。

在实际开发中,经常使用第三方工具如Redisson,来实现上述逻辑。

同时Redisson中的分布式锁自带自动续期机制。因为可能会出现操作共享资源时间大于过期时间的情况,导致操作还未完成,锁就过期了。Redisson提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

(4)基于Redis实现分布式锁的优缺点:

优点:

  • 性能高兴(核心点)
  • 实现方便(提供了SETNX方法)
  • 避免单点故障(Redis跨集群部署)

缺点:

  • 超时时间不好设置(A还未使用完共享资源,锁到期了,后续B获得了锁,开始使用资源)

解决办法:通过自动续期机制实现锁的自动续期。首先给锁设置一个超时时间,然后启动守护线程去监听锁,如果共享资源还未使用完,就续期。

3.消息队列与搜索引擎

Redis可以做消息队列,但不推荐。Redis可以做搜索引擎,基于RediSearch。

三. Redis数据类型

1.常见数据类型

  • 5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
  • 4 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)、Stream。

(1)String

String是最简单的key-value类型,value可以是字符串、整数、浮点数。

常见应用场景如下:

  • 存储常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存;
  • 常规计数,如用户单位时间的请求数、页面单位时间的访问数;
  • 分布式锁,利用SETNX key value 命令可以实现一个最简易的分布式锁。

(2)List

是一个链表,但为双向链表,支持反向查找和遍历。

应用场景:信息流展示(最新文章、最新动态)、消息队列(不推荐)

(3)Hash

Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,可以直接修改这个对象中的某些字段的值。

应用场景:对象数据存储(商品信息、用户信息等)

(4)Set

Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,可以自动去重。同时提供了判断某个元素是否在一个set集合中的接口。也能实现交集、并集、差集的操作。

应用场景:

  • 存放不重复数据:文章点赞。
  • 需要获取多个数据源的交集、并集和差集:微博的共同关注、共同好友、好友推荐等。
  • 需要随机获取数据源中的元素:抽奖系统、随机点名。

(5)Zset/Sorted Set

增加了一个权重参数score,使得集合中的元素能够按照score排序。

应用场景:直播间间的礼物排行榜、微信步数排行榜等。

(6)其它几种类型的应用场景:

  • BitMap(2.2 版):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数;
  • HyperLogLog(2.8 版):海量数据基数统计的场景,比如百万级网页 UV 计数;
  • GEO(3.2 版):存储地理位置信息的场景,比如滴滴打车;
  • Stream(5.0 版):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。

2.String的底层实现

使用SDS(Simple Dynamic String)简单动态字符串 作为底层实现。

LinkedList(双向链表)、ZipList(压缩列表)、QuickList(快速列表)、SkipList(跳表)

3.String还是Hash存储数据更好?

  • String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
  • String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。

通常情况下,建议使用String来存储。

4.购物车信息用String还是Hash存储?

由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储。

5.跳表(SkipList)

跳表可以理解为在原始链表基础上,建立多级索引,通过多级索引检索定位,将增删改查的时间复杂度变为O(log n)。

假如我们需要查询元素 6,其工作流程如下:

  1. 从 2 级索引开始,先来到节点 4。
  2. 查看 4 的后继节点,是 8 的 2 级索引,这个值大于 6,说明 2 级索引后续的索引都是大于 6 的,没有再往后搜寻的必要,我们索引向下查找。
  3. 来到 4 的 1 级索引,比对其后继节点为 6,查找结束。

理想情况下,每层索引的个数是下层元素个数的一半。如果元素个数为n,那么k层索引的元素个数r为:r = n/2^{k}

为什么Redis的Zset底层要用跳表,而不用平衡树、红黑树、B+树等结构?

  • 跳表结构简单,易于实现。
  • 跳表克服了平衡树的一些缺点,不需要进行旋转等操作。
  • 跳表相比于红黑树要更简单。
  • B+树更适用于数据库和文件系统的索引,Redis本身作为缓存不需要存大量数据,因此无需使用B+树维护。

6.Zset使用哪种数据结构?

Zset底层通过压缩列表跳表实现。

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis会用压缩列表作为 Zset 的底层数据结构。
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构。

四. Redis持久化机制

使用缓存的时候,我们经常需要对内存中的数据进行持久化,也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步。

1.三种持久化方式

  • 快照(RDB)
  • 只追加文件(AOF)
  • RDB和AOF混合持久化

2.RDB持久化

Redis可以通过创建快照来获得内存中的数据在某个时间点上的副本。创建快照后,可以对快照进行备份,也可以将快照复制到其它服务器创建具有相同数据的服务器副本,也可以留在原处,便于下次系统重启后快速恢复。

RDB是Redis的默认持久化方式。

3.AOF持久化

与快照方式比,AOF实时性更好。开启AOF后,每执行一条更改Redis数据的命令,Redis就会把该命令存到AOF缓冲区,然后在写到AOF文件中(此时还在系统内核缓存区未同步到磁盘,最后根据持久化方式的配置来决定什么时间将系统内核缓存区的数据同步到磁盘上。(只有同步到磁盘上,才算是持久化保存)

AOF持久化方式配置:

  • appendfsync always:每次有数据修改时都会立刻同步AOF文件,这样会严重降低Redis性能(write + fsync)
  • appendfsync everysec:每秒钟同步⼀次(write+fsync,fsync间隔为 1 秒)
  • appendfsync no:让操作系统决定何时进⾏同步,Linux 下一般为 30 秒一次(只write但不fsync)

4.如何选择RDB与AOF

RDB比AOF优秀的地方:

  • RDB文件存储的是经过压缩的二进制数据,保存的是某个时间点的数据集,文件很小,适合做数据备份、灾难恢复。AOF文件存储的是每一次的写命令,类似于MySQL的binlog日志,文件会大很多。
  • 使用RDB文件恢复数据,直接解析还原数据即可。而使用AOF文件需要依次执行每条写命令,速度相对会慢很多。因此RDB在恢复大数据集的时候速度会更快。

AOF比RDB优秀的地方:

  • RDB的数据安全性不如AOF,没有办法支持实时或秒级持久化数据。AOF支持秒级数据丢失(取决于持久化方式的配置,最多丢失1秒),仅仅是在AOF文件中追加命令,操作轻量。
  • RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。
  • AOF以一种便于理解和解析的格式包含所有操作的日志,可以轻松导出AOF文件进行解析,也可以直接对AOF文件操作。

因此:

  • 如果Redis保存的数据丢失一些也可以接受,就选择RDB。
  • 不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。
  • 如果保存的数据要求安全性,就使用混合持久化。

5.Redis4.0对持久化机制做的优化

开始支持RDB与AOF混合持久化。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

五. Redis线程模型

对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。

1.Redis单线程模型

Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型 ,这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以一般都说 Redis 是单线程模型。

文件事件处理器包含:多个socket、IO多路复用程序、文件事件分配器和事件处理器。

2.既然是单线程,那怎么监听大量的客户端连接呢?

Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

这样的好处非常明显:I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。

3.Redis为什么选择单线程模型?

  • 使用单线程模型带来更好的可维护性,便于开发和调试。
  • 单线程模型也能够并发处理客户端请求。
  • Redis中绝大多数操作的性能瓶颈都不是CPU,而是内存和网络。

Redis选择单线程模型处理客户端请求主要因为其性能瓶颈不是CPU,因此多线程带来的性能提升并不能抵消其开发和维护成本。Redis的性能瓶颈主要在网络IO操作上,引入多线程也是为了性能考虑,比如大键值对的异步删除操作,通过多线程非阻塞地释放内存空间也能减少对Redis主线程的阻塞时间,提高执行效率。

六. Redis内存管理

1.Redis为什么给缓存数据设置过期时间?

因为内存有限,如果缓存中所有数据一直保存,很快就会爆满,因此设置过期时间可以缓解内存消耗。

除了缓解内存消耗外,还可以应用在某些业务场景,比如需要某个数据只在一段时间内存在:短信验证码1分钟内有效,用户登录令牌只在一天内有效等。

2.Redis如何判断数据是否过期?

Redis 通过过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间。

3.过期数据的删除策略

  • 惰性删除:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
  • 定期删除:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

惰性删除对CPU最友好,定期删除对内存最友好,Redis采用二者相结合的方式。

4.内存淘汰机制

给数据设置过期时间仍然有问题,因为惰性删除和定期删除仍然可能会漏掉一些数据,最终导致内存超载。因此引入内存淘汰机制。

6种内存淘汰机制:

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。很少使用。

七. Redis事务

Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。

不满足原子性和持久性,并且每条命令都要与Redis服务器进行网络交互,浪费资源。实际开发中应用很少,不建议使用。

八. Redis生产问题

1.缓存穿透

缓存穿透就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力。

解决办法:

  • 缓存无效key:如果缓存和数据库都查不到某个 key 的数据,就写一个到 Redis 中去并设置过期时间。
  • 布隆过滤器:解决海量数据的存在性问题。把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
  • 做好参数校验:增加id复杂度,避免被猜测,同时对于不合法的参数请求直接抛出异常返回给客户端。

2.缓存击穿

请求的key对应的是热点数据,该数据存在于数据库中,但不存在于缓存中,通常是因为缓存中的数据过期了。导致瞬时大量的请求落到数据库上,对数据库造成巨大压力。

解决办法:

  • 将热点数据的过期时间设置的长一些。
  • 热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
  • 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求落到数据库上,减少数据库的压力。

3.缓存雪崩

缓存在同一时间大面积失效,导致大量请求同时落到数据库上,对数据库造成巨大压力。

解决办法:

针对Redis服务不可用的情况:

  • 采用Redis集群,避免单机出现问题导致整个服务都不可用。
  • 限流,避免同时处理大量请求。

针对热点缓存失效的情况:

  • 随机设置缓存的失效时间。
  • 缓存预热。

4.缓存穿透和缓存击穿的区别

  • 缓存穿透中请求的key既不存在于缓存,也不存在于数据库。
  • 缓存击穿中请求的key对应的是热点数据,并且存在于数据库中,但不存在于缓存中。

5.缓存击穿和缓存雪崩的区别

  • 缓存击穿是热点数据不存在于缓存中,通常是因为缓存中的数据过期。
  • 缓存雪崩是缓存中的数据大面积失效。

相关推荐

  1. <span style='color:red;'>Redis</span>

    Redis

    2024-03-17 05:48:05      38 阅读
  2. <span style='color:red;'>Redis</span>

    Redis

    2024-03-17 05:48:05      64 阅读
  3. <span style='color:red;'>Redis</span>

    Redis

    2024-03-17 05:48:05      28 阅读
  4. <span style='color:red;'>redis</span>

    redis

    2024-03-17 05:48:05      45 阅读
  5. <span style='color:red;'>Redis</span>

    Redis

    2024-03-17 05:48:05      37 阅读
  6. <span style='color:red;'>redis</span>

    redis

    2024-03-17 05:48:05      43 阅读
  7. <span style='color:red;'>Redis</span>

    Redis

    2024-03-17 05:48:05      41 阅读
  8. <span style='color:red;'>redis</span>

    redis

    2024-03-17 05:48:05      42 阅读
  9. Redis

    2024-03-17 05:48:05       36 阅读
  10. <span style='color:red;'>redis</span>

    redis

    2024-03-17 05:48:05      42 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-17 05:48:05       17 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-17 05:48:05       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-17 05:48:05       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-17 05:48:05       18 阅读

热门阅读

  1. ajax是异步还是同步?

    2024-03-17 05:48:05       17 阅读
  2. redisson分布式锁

    2024-03-17 05:48:05       21 阅读
  3. python opencv的最基础初学

    2024-03-17 05:48:05       19 阅读
  4. C---流

    C---流

    2024-03-17 05:48:05      16 阅读
  5. Linux-centos系统中如何去除配置文件中的注释部分

    2024-03-17 05:48:05       16 阅读
  6. LLMOps:机器学习运营的下一个前沿

    2024-03-17 05:48:05       21 阅读