• redis知识集锦


    Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。

    它是一种 NoSQL(not-only sql,泛指非关系型数据库)的数据库

    Redis 作为一个内存数据库:

    • 性能优秀,数据在内存中,读写速度非常快,支持并发 10W QPS。
    • 单进程单线程,是线程安全的,采用 IO 多路复用机制。
    • 丰富的数据类型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
    • 支持数据持久化。
      可以将内存中数据保存在磁盘中,重启时加载。
    • 主从复制,哨兵,高可用。
    • 可以用作分布式锁。
    • 可以作为消息中间件使用,支持发布订阅。
    • memcached所有的值均是简单的字符串,
    • redis作为其替代者,支持更为丰富的数据类型,
    • redis的速度比memcached快很多,
    • redis可以持久化其数据。

    首先 Redis 内部使用一个 redisObject 对象来表示所有的 key 和 value。

    redisObject 最主要的信息如上图所示:type 表示一个 value 对象具体是何种数据类型,encoding 是不同数据类型在 Redis 内部的存储方式。

    比如:type=string 表示 value 存储的是一个普通字符串,那么 encoding 可以是 raw 或者 int。

    下面我简单说下 5 种数据类型:

    ①String 是 Redis 最基本的类型,可以理解成与 Memcached一模一样的类型,一个 Key 对应一个 Value。Value 不仅是 String,也可以是数字。

    String 类型是二进制安全的,意思是 Redis 的 String 类型可以包含任何数据,比如 jpg 图片或者序列化的对象。String 类型的值最大能存储 512M。

    ②Hash是一个键值(key-value)的集合。Redis 的 Hash 是一个 String 的 Key 和 Value 的映射表,Hash 特别适合存储对象。常用命令:hget,hset,hgetall 等。

    ③List 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边) 常用命令:lpush、rpush、lpop、rpop、lrange(获取列表片段)等。

    应用场景:List 应用场景非常多,也是 Redis 最重要的数据结构之一,比如 Twitter 的关注列表,粉丝列表都可以用 List 结构来实现。

    数据结构:List 就是链表,可以用来当消息队列用。Redis 提供了 List 的 Push 和 Pop 操作,还提供了操作某一段的 API,可以直接查询或者删除某一段的元素。

    实现方式:Redis List 的是实现是一个双向链表,既可以支持反向查找和遍历,更方便操作,不过带来了额外的内存开销。

    ④Set 是 String 类型的无序集合。集合是通过 hashtable 实现的。Set 中的元素是没有顺序的,而且是没有重复的。常用命令:sdd、spop、smembers、sunion 等。

    应用场景:Redis Set 对外提供的功能和 List 一样是一个列表,特殊之处在于 Set 是自动去重的,而且 Set 提供了判断某个成员是否在一个 Set 集合中。

    ⑤Zset 和 Set 一样是 String 类型元素的集合,且不允许重复的元素。常用命令:zadd、zrange、zrem、zcard 等。

    使用场景:Sorted Set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。

    当你需要一个有序的并且不重复的集合列表,那么可以选择 Sorted Set 结构。

    和 Set 相比,Sorted Set关联了一个 Double 类型权重的参数 Score,使得集合中的元素能够按照 Score 进行有序排列,Redis 正是通过分数来为集合中的成员进行从小到大的排序。

    实现方式:Redis Sorted Set 的内部使用 HashMap 和跳跃表(skipList)来保证数据的存储和有序,HashMap 里放的是成员到 Score 的映射。

    而跳跃表里存放的是所有的成员,排序依据是 HashMap 里存的 Score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

    数据类型应用场景总结:

    noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。大多数写命令都会导致占用更多的内存(有极少数会例外。
    allkeys-lru:所有key通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。
    volatile-lru:只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。
    allkeys-random:所有key通用; 随机删除一部分 key。
    volatile-random: 只限于设置了expire的部分; 随机删除一部分 key。
    volatile-ttl: 只限于设置了expire的部分; 优先删除剩余时间(time to live,TTL) 短的key。
    • Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。

    • 单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。

    • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
    • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
    • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
    • 使用多路I/O复用模型,非阻塞IO;这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程
    • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
    • used_memory:Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即swap);Redis分配器后面会介绍。used_memory_human只是显示更友好。
    • used_memory_rss:Redis进程占据操作系统的内存(单位是字节),与top及ps命令看到的值是一致的;除了分配器分配的内存之外,used_memory_rss还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存。
    • mem_fragmentation_ratio:内存碎片比率,该值是used_memory_rss / used_memory的比值。
    • mem_allocator:Redis使用的内存分配器,在编译时指定;可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc;截图中使用的便是默认的jemalloc。
    • 数据 作为数据库,数据是最主要的部分;这部分占用的内存会统计在used_memory中。 进程本身运行需要的内存 Redis主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。
    • 缓冲内存 缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等;其中,客户端缓冲存储客户端连接的输入输出缓冲;复制积压缓冲用于部分复制功能;AOF缓冲区用于在进行AOF重写时,保存最近的写入命令。在了解相应功能之前,不需要知道这些缓冲的细节;这部分内存由jemalloc分配,因此会统计在used_memory中。
    • 内存碎片 内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致redis释放的空间在物理内存中并没有释放,但redis又无法有效利用,这就形成了内存碎片。内存碎片不会统计在used_memory中。
    Redis 的持久化策略有两种:
    • RDB:快照形式是直接把内存中的数据保存到一个 dump 的文件中,定时保存,保存策略。
    • AOF:把所有的对 Redis 的服务器进行修改的命令都存到一个文件里,命令的集合。Redis 默认是快照 RDB 的持久化方式。

    当 Redis 重启的时候,它会优先使用 AOF 文件来还原数据集,因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。你甚至可以关闭持久化功能,让数据只在服务器运行时存。

    RDB 是怎么工作的?

    默认 Redis 是会以快照"RDB"的形式将数据持久化到磁盘的一个二进制文件 dump.rdb。

    工作原理简单说一下:当 Redis 需要做持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时 RDB 文件中。

    当子进程完成写临时文件后,将原来的 RDB 替换掉,这样的好处是可以 copy-on-write。

    RDB 的优点是:这种文件非常适合用于备份:比如,你可以在最近的 24 小时内,每小时备份一次,并且在每个月的每一天也备份一个 RDB 文件。

    这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。RDB 非常适合灾难恢复。

    RDB 的缺点是:如果你需要尽量避免在服务器故障时丢失数据,那么RDB不合适你。

    那你要不再说下 AOF?

    使用 AOF 做持久化,每一个写命令都通过 write 函数追加到 appendonly.aof 中,配置方式如下:

    appendfsync yes   
    appendfsync always     #每次有数据修改发生时都会写入AOF文件appendfsync everysec   #每秒钟同步一次该策略为AOF的缺省策略

    AOF 可以做到全程持久化,只需要在配置中开启 appendonly yes。这样 Redis 每执行一个修改数据的命令,都会把它添加到 AOF 文件中,当 Redis 重启时,将会读取 AOF 文件进行重放,恢复到 Redis 关闭前的最后时刻。

    使用 AOF 的优点是会让 Redis 变得非常耐久。可以设置不同的 Fsync 策略,AOF的默认策略是每秒钟 Fsync 一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。

    缺点是对于相同的数据集来说,AOF 的文件体积通常要大于 RDB 文件的体积。根据所使用的 Fsync 策略,AOF 的速度可能会慢于 RDB。

    说了这么多,那我该用哪一个呢?

    如果你非常关心你的数据,但仍然可以承受数分钟内的数据丢失,那么可以额只使用 RDB 持久。

    AOF 将 Redis 执行的每一条命令追加到磁盘中,处理巨大的写入会降低Redis的性能,不知道你是否可以接受。

    数据库备份和灾难恢复:定时生成 RDB 快照非常便于进行数据库备份,并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度快。

    当然了,Redis 支持同时开启 RDB 和 AOF,系统重启后,Redis 会优先使用 AOF 来恢复数据,这样丢失的数据会最少。

    下面是AOF常用的配置项,以及默认值

    appendonly no:可以配置为yes
    appendfilename "appendonly.aof":AOF文件名
    dir ./:RDB文件和AOF文件所在目录
    appendfsync everysec:fsync持久化策略
    no-appendfsync-on-rewrite no:AOF重写期间是否禁止fsync;如果开启该选项,可以减轻文件重写时CPU和硬盘的负载(尤其是硬盘),但是可能会丢失AOF重写期间的数据;需要在负载和安全性之间进行平衡
    auto-aof-rewrite-percentage 100:文件重写触发条件之一
    auto-aof-rewrite-min-size 64mb:文件重写触发提交之一
    aof-load-truncated yes:如果AOF文件结尾损坏,Redis启动时是否仍载入AOF文件

    优点: RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快很多。当然,与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。

    缺点: RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件

    AOF持久化与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。

    • 如果Redis中的数据完全丢弃也没有关系(如Redis完全用作DB层数据的cache),那么无论是单机,还是主从架构,都可以不进行任何持久化。

    • 在单机环境下(对于个人开发者,这种情况可能比较常见),如果可以接受十几分钟或更多的数据丢失,选择RDB对Redis的性能更加有利;如果只能接受秒级别的数据丢失,应该选择AOF

    • 但在多数情况下,我们都会配置主从环境,slave的存在既可以实现数据的热备,也可以进行读写分离分担Redis读请求,以及在master宕掉后继续提供服务。

    dbsize 返回当前数据库 key 的数量。
    info 返回当前 redis 服务器状态和一些统计信息。
    monitor 实时监听并返回redis服务器接收到的所有请求信息。 
    shutdown 把数据同步保存到磁盘上,并关闭redis服务。
    config get parameter 获取一个 redis 配置参数信息。(个别参数可能无法获取)
    config set parameter value 设置一个 redis 配置参数信息。(个别参数可能无法获取)
    config resetstat 重置 info 命令的统计信息。(重置包括:keyspace 命中数、
    keyspace 错误数、 处理命令数,接收连接数、过期 key 数)
    debug object key 获取一个 key 的调试信息。
    debug segfault 制造一次服务器当机。
    flushdb 删除当前数据库中所有 key,此方法不会失败。小心慎用
    flushall 删除全部数据库中所有 key,此方法不会失败。小心慎用
    #redis-server:Redis 服务器的 daemon 启动程序
    #redis-cli:Redis 命令行操作工具。当然,你也可以用 telnet 根据其纯文本协议来操作
    #redis-benchmark:Redis 性能测试工具,测试 Redis 在你的系统及你的配置下的读写性能
    #redis-benchmark -n 100000 –c 50
    #模拟同时由 50 个客户端发送 100000 个 SETs/GETs 查询
    #redis-check-aof:更新日志检查
    #redis-check-dump:本地数据库检查
    

    Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内 尽量避免在压力很大的主库上增加从库

    RESP 是redis客户端和服务端之前使用的一种通讯协议;

    RESP 的特点:实现简单、快速解析、可读性好

    先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!

    利用SCAN系列命令(SCAN、SSCAN、HSCAN、ZSCAN)完成数据迭代。

    • SCAN的参数没有key,因为其迭代对象是DB内数据;
    • 返回值都是数组,第一个值都是下一次迭代游标;
    • 时间复杂度:每次请求都是O(1),完成所有迭代需要O(N),N是元素数量;
    • 可用版本:version >= 2.8.0;

    定时删除:在设置键的过期时间的同时,创建一个定时任务,当键达到过期时间时,立即执行对键的删除操作

    • 优点: 对内存友好,定时删除策略可以保证过期键会尽可能快地被删除,并释放国期间所占用的内存

    • 缺点: 对cpu时间不友好,在过期键比较多时,删除任务会占用很大一部分cpu时间,在内存不紧张但cpu时间紧张的情况下,将cpu时间用在删除和当前任务无关的过期键上,影响服务器的响应时间和吞吐量

    定期删除:每隔一点时间,程序就对数据库进行一次检查,删除里面的过期键,至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

    • 优点: 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响定时删除策略有效地减少了因为过期键带来的内存浪费。

    惰性删除:放任键过期不管,但在每次从键空间获取键时,都检查取得的键是否过期,如果过期的话,就删除该键,如果没有过期,

    • 优点: 对cpu时间友好,在每次从键空间获取键时进行过期键检查并是否删除,删除目标也仅限当前处理的键,这个策略不会在其他无关的删除任务上花费任何cpu时间

    • 缺点: 对内存不友好,过期键过期也可能不会被删除,导致所占的内存也不会释放。甚至可能会出现内存泄露的现象,当存在很多过期键,而这些过期键又没有被访问到,这会可能导致它们会一直保存在内存中,造成内存泄露。

    21.redis主从复制

    Redis 单节点存在单点故障问题,为了解决单点问题,一般都需要对 Redis 配置从节点,然后使用哨兵来监听主节点的存活状态,如果主节点挂掉,从节点能继续提供缓存功能

    Redis 主从复制的过程和原理吗?

    主从配置结合哨兵模式能解决单点故障问题,提高 Redis 可用性。

    从节点仅提供读操作,主节点提供写操作。对于读多写少的状况,可给主节点配置多个从节点,从而提高响应效率。

    我顿了一下,接着说:关于复制过程,是这样的:

    • 从节点执行 slaveof[masterIP][masterPort],保存主节点信息。
    • 从节点中的定时任务发现主节点信息,建立和主节点的 Socket 连接。
    • 从节点发送 Ping 信号,主节点返回 Pong,两边能互相通信。
    • 连接建立后,主节点将所有数据发送给从节点(数据同步)。
    • 主节点把当前的数据同步给从节点后,便完成了复制的建立过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。

    数据同步的过程吗?

    Redis 2.8 之前使用 sync[runId][offset] 同步命令,Redis 2.8 之后使用 psync[runId][offset] 命令。

    两者不同在于,Sync 命令仅支持全量复制过程,Psync 支持全量和部分复制。

    介绍同步之前,先介绍几个概念:

    • runId:每个 Redis 节点启动都会生成唯一的 uuid,每次 Redis 重启后,runId 都会发生变化。
    • offset:主节点和从节点都各自维护自己的主从复制偏移量 offset,当主节点有写入命令时,offset=offset+命令的字节长度。
      从节点在收到主节点发送的命令后,也会增加自己的 offset,并把自己的 offset 发送给主节点。
      这样,主节点同时保存自己的 offset 和从节点的 offset,通过对比 offset 来判断主从节点数据是否一致。
    • repl_backlog_size:保存在主节点上的一个固定长度的先进先出队列,默认大小是 1MB。

    主节点发送数据给从节点过程中,主节点还会进行一些写操作,这时候的数据存储在复制缓冲区中。

    从节点同步主节点数据完成后,主节点将缓冲区的数据继续发送给从节点,用于部分复制。

    主节点响应写命令时,不但会把命名发送给从节点,还会写入复制积压缓冲区,用于复制命令丢失的数据补救。

    上面是 Psync 的执行流程,从节点发送 psync[runId][offset] 命令,主节点有三种响应:

    • FULLRESYNC:第一次连接,进行全量复制
    • CONTINUE:进行部分复制
    • ERR:不支持 psync 命令,进行全量复制

    全量复制和部分复制的过程吗?

    上面是全量复制的流程。主要有以下几步:

    • 从节点发送 psync ? -1 命令(因为第一次发送,不知道主节点的 runId,所以为?,因为是第一次复制,所以 offset=-1)。
    • 主节点发现从节点是第一次复制,返回 FULLRESYNC {runId} {offset},runId 是主节点的 runId,offset 是主节点目前的 offset。
    • 从节点接收主节点信息后,保存到 info 中。
    • 主节点在发送 FULLRESYNC 后,启动 bgsave 命令,生成 RDB 文件(数据持久化)。
    • 主节点发送 RDB 文件给从节点。到从节点加载数据完成这段期间主节点的写命令放入缓冲区。
    • 从节点清理自己的数据库数据。
    • 从节点加载 RDB 文件,将数据保存到自己的数据库中。如果从节点开启了 AOF,从节点会异步重写 AOF 文件。

    关于部分复制有以下几点说明

    ①部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync[runId][offset] 命令实现。

    当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,主节点的复制积压缓冲区将这部分数据直接发送给从节点。

    这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据。

    ②主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内的复制积压缓冲区依然可以保存最近一段时间的写命令数据。

    ③当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行 ID。因此会把它们当做 psync 参数发送给主节点,要求进行部分复制。

    ④主节点接收到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点。

    之后根据参数 offset 在复制积压缓冲区中查找,如果 offset 之后的数据存在,则对从节点发送+COUTINUE 命令,表示可以进行部分复制。因为缓冲区大小固定,若发生缓冲溢出,则进行全量复制。

    ⑤主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

    哨兵

    主从复制会存在哪些问题呢?

    • 一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。
    • 主节点的写能力受到单机的限制。
    • 主节点的存储能力受到单机的限制。
    • 原生复制的弊端在早期的版本中也会比较突出,比如:Redis 复制中断后,从节点会发起 psync。
      此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时,可能会造成毫秒或秒级的卡顿。

    比较主流的解决方案是什么呢?哨兵啊。哨兵有哪些功能?

    如图,是 Redis Sentinel(哨兵)的架构图。Redis Sentinel(哨兵)主要功能包括主节点存活检测、主从运行情况检测、自动故障转移、主从切换。

    Redis Sentinel 最小配置是一主一从。Redis 的 Sentinel 系统可以用来管理多个 Redis 服务器。

    该系统可以执行以下四个任务:

    • 监控:不断检查主服务器和从服务器是否正常运行。
    • 通知:当被监控的某个 Redis 服务器出现问题,Sentinel 通过 API 脚本向管理员或者其他应用程序发出通知。
    • 自动故障转移:当主节点不能正常工作时,Sentinel 会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点,这样人工干预就可以免了。
    • 配置提供者:在 Redis Sentinel 模式下,客户端应用在初始化时连接的是 Sentinel 节点集合,从中获取主节点的信息。

    哨兵的工作原理吗?

    ①每个 Sentinel 节点都需要定期执行以下任务:每个 Sentinel 以每秒一次的频率,向它所知的主服务器、从服务器以及其他的 Sentinel 实例发送一个 PING 命令。(如上图)

    ②如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 所指定的值,那么这个实例会被 Sentinel 标记为主观下线。(如上图)

    ③如果一个主服务器被标记为主观下线,那么正在监视这个服务器的所有 Sentinel 节点,要以每秒一次的频率确认主服务器的确进入了主观下线状态。

    ④如果一个主服务器被标记为主观下线,并且有足够数量的 Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,那么这个主服务器被标记为客观下线。

    ⑤一般情况下,每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令。

    当一个主服务器被标记为客观下线时,Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率,会从 10 秒一次改为每秒一次。

    ⑥Sentinel 和其他 Sentinel 协商客观下线的主节点的状态,如果处于 SDOWN 状态,则投票自动选出新的主节点,将剩余从节点指向新的主节点进行数据复制。

    ⑦当没有足够数量的 Sentinel 同意主服务器下线时,主服务器的客观下线状态就会被移除。

    当主服务器重新向 Sentinel 的 PING 命令返回有效回复时,主服务器的主观下线状态就会被移除。

  • 相关阅读:
    Flink中的广播流之BroadcastStream
    啊。。这是为什么。。 花甜的工作笔记
    我那庞大身躯,脆弱心灵的3250 花甜的工作笔记
    好吧,我承认,我不是一个专一的人。。。 花甜的工作笔记
    沾沾自喜 花甜的工作笔记
    我高调的来啦。。。。 花甜的工作笔记
    今天偶听一词云终端 花甜的工作笔记
    我开始出轨了。 花甜的工作笔记
    软件限制策略。。。。。辛苦中。。 花甜的工作笔记
    推荐系统建构精选文章
  • 原文地址:https://www.cnblogs.com/albertrui/p/14579889.html
Copyright © 2020-2023  润新知