4

1.认识Redis

  • 工作模型:单线程架构和IO多路复用来实现高性能的内存数据库服务
  • 原因:a)单线程简化数据结构和算法的实现;b)避免线程切换和线程竞争的开销
  • 应用场景:缓存/排行系统/统计器应用/社交网络/消息队列/热数据

2.数据类型

2-1.字符串类型

  • 命令相关:使用mget可以减少网络次数,提高开发效率(字符串不能超过512MB)
  • 内部编码:根据当前值的类型和长度决定使用哪种编码

    • int:8bytes长整型
    • embstr:小于等于39bytes的字符串
    • raw:大于39bytes的字符串
  • 底层数据结构:数组
  • 应用场景:缓存功能/计数/共享Seesion/限速

2-2.哈希类型

  • 命令相关:键值本身又是一个键值对结构; set key field
  • 内部编码:

    • ziplist压缩列表;满足哈希类型元素个数小于hash-max-ziplist-entries和所有值小于hash-max-ziplist-value
    • hashtable哈希表:当无法满足ziplist的条件时,会使用hashtable作为哈希的内部实现
  • 底层数据结构:数组(hashkey) + 链表(field)
  • 应用场景:关系数据表记录

2-3.列表类型

  • 命令相关:可以根据索引查询修改,阻塞删除,头尾添加;根据不同的命令可以实现队列/栈等操作
  • 内部编码:

    • ziplist压缩列表;满足哈希类型元素个数小于hash-max-ziplist-entries和所有值小于hash-max-ziplist-value
    • linkedlist链表:当无法满足ziplist的条件时,会使用linkedlist作为列表的内部实现
  • 底层数据结构:双向链表
  • 应用场景:消息队列/文章列表

2-4.集合

  • 命令相关:一个集合中可以存放多个元素
  • 内部编码:

    • intset整数集合:当集合中的元素个数小于set-max-intset-entries配置
    • hashtable哈希表;当集合类型无法满足intset条件
  • 底层数据结构:hash
  • 应用场景:标签功能

2-5.有序集合

  • 命令相关:一个集合中可以存放多个拥有分数的元素,用于排序
  • 内部编码:

    • ziplist压缩列表:当集合中的元素个数小于set-max-ziplist-entries配置
    • skiplist跳跃表;当集合类型无法满足ziplist条件时
  • 底层数据结构:hash + 跳跃表
  • 应用场景:排行系统

3.小功能

3-1.慢查询

  • 配置方式:a)配置文件;b)动态配置(rewrite持久化到本地配置文件中)
  • 获取慢查询日志:slowlog show get [n];采用队列存储,先进先出形式
  • 日志属性:日志标识,发生时间,命令耗时,执行命令和参数
  • 最佳实践:max-len > 1000 & slower-than 1ms

3-2.Pipeline

  • 场景:将一组命令封装,通过一次RTT传输;能够减少网络延迟和性能瓶颈
  • Redis的批量命令与Pipeline区别:

    • 原子性:批量命令是原子性的
    • 命令格式:批量命令一次对应多个Key
    • 实现机制:Pipeline需要客户端支持
  • 最佳实践:可以将大Pipeline拆分成多个小Pipeline来完成

3-3.事务和Lua

  • 事务:原子性,保证数据一致性
  • 相关命令:multi开始事务,exec事务提交,discard停止事务;watch确保事务中的key没有被其他客户端修改
  • 事务错误: a)语法错误:Redis事务忽略; b)运行错误:事务提交后执行报错
  • 不支持回滚:保持Redis的简单快捷;关于语法错误而失败应该在开发过程中被发现
  • Lua脚本:Redis脚本语言

    • Lua脚本在Redis中是原子执行的,执行过程中不插入其他命令
    • Lua脚本定制命令并可以常驻内存中,实现复用的效果
    • Lua脚本可以将多条命令一次性打包,减少网络开销

3-4.Bitmaps和Hyperloglog

  • Bitmaps:一个以位为单位的数组,数组中的每个单元只能存储0和1,数组的下标称之为偏移量
  • 设置:setbit key offset value
  • 运算:bitop [and(交集) | or(并集) | not(非) | xor(异或)] destkey [keys...]
  • 应用场景:大用户量时统计活跃数,能有效减少内存
  • Hyperloglog:一种基数算法,实际数据类型为字符串类型,利用极小的内存空间完成独立总数的统计(不需要单条记录)

3-5.发布和订阅

发布者客户端向指定的频道(channel)发布消 息,订阅该频道的每个客户端都可以收到该消息

  • 相关命令:publish发布,subscrible订阅
  • 注意点:a)客户端在执行订阅命令之后进入订阅状态;b)新开启的订阅客户端,无法接受到该频道之前的消息,因为Redis不会对发布的消息进行持久化
  • 使用场景:聊天室/公告牌/消息解耦/视频管理系统

3-6.GEO

地理信息定位功能:支持存储地理位置信息来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能;底层实现是:zset

4.客户端

客户端通信协议:基于TCP协议定制RESP实现交互的
RESP协议优点:a)实现容易;b)解析快;c)人类可读

4-1.客户端管理

4-2.客户端常见异常

1.无法从连接池中获取到连接

对象个数默认是8个:blockWhenExhausted=false代表连接池没有资源可能的原因:

  1. 连接池设置过小
  2. 没有释放连接
  3. 存在慢查询操作
  4. 服务端命令执行过程被堵塞

2.客户端读写超时

1.读写超时时间设置过短
2.命令本身比较慢
3.客户端与服务端网络不正常
4.Redis自身发生堵塞

3.客户端连接超时

1.连接超时设置过短
2.Redis发生堵塞,造成tcp-backlog已满
3.客户端缓冲区异常
4.输出缓冲区满
5.长时间闲置连接被服务端主动断开
6.不正常并发读写

4.Lua脚本正在执行,并超过lua-time-limit
5.Redis正在加载持久化文件
6.Redis使用的内存超过maxmemory配置
7.客户端连接数过大

5.持久化

5-1.RDB方式

RDB持久化:把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化分为手动触发和自动触发

  • 手动触发:通过bgsave命令Redis进程执行fork操作创建子进程,由子进程负责完成
  • 自动触发:

    • save相关配置: sava m n (表示m秒内数据集存在n次修改即自动触发bgsave)
    • 从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
    • 执行debug reload命令重新加载Redis时,也会自动触发save操作
    • 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave
  • 执行流程:

image.png

  • RDB文件处理:保存和压缩并校验
  • RDB优点:

    • 代表某个时间点的数据快照,适用备份和全量复制等场景
    • 压缩的二进制文件,恢复“大数据集”效率较高
  • RDB缺点:

    • 数据的实时持久化较差并且fork()操作会带来堵塞
    • 特定的二进制文件会带来兼容性问题

5-2.AOF方式

AOF持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的,解决数据持久化的实时性

  • AOF工作流程:
1.命令写入以追加方式到AOF_buf
2.AOF缓冲区根据策略同步到AOF文件
3.随着AOF文件变大,需要定期对AOF文件进行重写,达到压缩目的
4.当Redis重启时,可以加载AOF文件进行数据恢复
  • 命令写入追加到缓冲区的目的:

    • 写入的内容是文件协议格式(1.兼容性;2.可读性;3.避免二次处理开销)
    • 在性能和安全性方面做出平衡
  • 同步策略:Redis提供多种AOF缓冲区同步文件策略,由appendfsync控制

    • always:调用write操作后并调用fsync保证AOF文件写入到磁盘中
    • no:调用write操作后,AOF文件同步到磁盘交由操作系统去实现
    • everysec:调用write操作后,由专门线程去每秒调用一次fsync
  1. write操作:会触发延迟写机制,因为Linux在内核提供页缓冲区来提供磁盘IO性能;write操作在写入系统缓冲区后直接返回,同步硬盘依赖于系统调度机制
  2. fsync操作:针对单个文件操作做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了数据持久化

重写机制:把Redis进程内的数据转化为命令同步到新AOF文件的过程,这个过程会让AOF文件体积变小,从而提高恢复时的效率

1.进程已经超时的数据不再写入新文件
2.通过进程内数据直接生成,避免旧文件中的无效命令
3.多条命令可以合并为一个

AOF重写触发方式:
手动触发:执行bgrewriteaof命令
自动触发:同时满足以下2个条件

1.auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB
2.auto-aof-rewrite-percentage:表示当前AOF文件空间和上一次重写后AOF文件空间的比值
  • AOF重写工作流程

image.png

  • Redis持久化加载流程

    1. AOF持久化开启时且存在AOF文件时,优先加载AOF文件
    2. AOF关闭或者AOF文件不存在时,加载RDB文件
    3. 加载AOF/RDB文件成功后,Redis启动成功
    4. AOF/RDB文件存在错误时,Redis启动失败
  • 关于AOF文件异常

    • 损坏的文件Redis服务会拒绝启动(可以通过redis-check-aof-fix命令修复后,进行对比并进行手工修改补全)
    • 结尾不挖完整的文件Redis服务忽略并继续启动,同时打印报警信息

AOF追加阻塞:当开启AOF持久化时,常用的同步磁盘策略是everysec,对于这种方式,Redis会使用同步条线程每秒执行fsync同步硬盘,当系统硬盘资源繁忙时,会造成Redis主线程阻塞
everysec刷盘策略过程

1.同步线程负责每秒调用fsync操作进行同步磁盘
2.主进程会去对比上次fsync同步时间,如果在2s内则通过,否则会堵塞(磁盘资源紧张)
3.everysec策略最多可能丢失2s数据;如果系统fsync缓慢,会导致主进程堵塞
  • AOF优点:

    • 实时备份:能够到秒级
    • 以appen-only的模式写入:没有磁盘寻址开销,写入性能较高
    • 可读性强:具有更灵活的处理方式
  • AOF缺点:

    • 相同的数据集:AOF文件会比RDB文件大
    • Redis高负载情况下:RDB会拥有更好的性能保证
    • 数据恢复:比较慢并不适合全量备份

6.复制

6-1.配置

  • 建立复制的3种方式:

    • 配置文件:slaveof [masterHost] [masterPort]
    • 启动命令:redis-server --slaveof [masterHost] [masterPort]
    • 直接命令:slaveof [masterHost] [masterPort]
  • 断开复制:命令1操作后从节点晋升为主节点;命令2操作后可以完成切主操作

    • 命令1:slaveof no none
    • 命令2:slaveof [newmasterHost] [newmasterPort]
  • 安全性:主节点通过requirepass参数进行密码验证来保证数据安全性

    • 客户端校验:通过auth命令
    • 从节点校验:通过masterauth参数
  • 只读:默认情况下,从节点使用slave-read-only=yes配置为只读模式;因为从节点的数据无法同步给主节点
  • 传输延迟:主从节点一般部署在不同机器上,复制时的网络延迟成为需要考虑的问题

    • repl-disable-tcp-nodelay=yes时:代表关闭,主节点产生的命令无论大小都会及时发送给从节点,这样做延迟会变小,但是增加了网络带宽消耗
    • repl-disable-tcp-nodelay=no时:代表开启,主节点会合并比较小的TCP数据包从而节省网络带宽消耗,但是这样增加了主从之间的延迟

6-2.复制原理

复制过程

1.保存主节点信息:IP+Port
2.建立socket连接
3.发送ping命令:a)检测socket是否可用;b)判断主节点是否能处理命令
4.权限验证
5.同步数据集:首次建立复制,主节点会把数据集全部发往从节点
6.命令持续复制:主节点把持续写命令复制给从节点,保持数据一致性

全量复制过程
image.png

部门复制过程
image.png

  • 心跳判断:主从节点建立连接后保持长连接

    • 主节点默认每隔10s对从节点发送ping命令,判断从节点的存活性和连接状态(repl-ping-slave-period控制发送频率)
    • 从节点在主线程每隔1s发送replconf ack {offset}命令,给主节点上报自身当前的复制偏移量
    • 如果超过repl-timeout配置的值(默认60秒),则判定从节点下线并断开复制客户端连接
  • 补充知识点:

    • Redis为了保证高性能复制过程是异步的,写命令处理完后直接返回给客户端,不等待从节点复制完成。因此从节点数据集会有延迟情况
    • 当使用从节点用于读写分离时会存在数据延迟、过期数据、从节点可用性等问题,需要根据自身业务提前作出规避
    • 在运维过程中,主节点存在多个从节点或者一台机器上部署大量主节点的情况下,会有复制风暴的风险

7.阻塞

Redis是单线程架构:所有读写操作都是串行的而会导致阻塞问题

内在原因:不合理使用API或数据结构、CPU饱和、持久化阻塞
外在原因:CPU竞争,内存交换,网络问题等

7-1.发现堵塞

  • 登录Redis:执行info,查看blocked_clients
  • 执行redis-cli --latency -h -p 查看延时情况

7-2.内在原因

  • 不合理使用API或数据结构:比如执行hgetall获取的数据量会非常大

    • 获取慢查询:a) 修改为低复杂度命令;b) 调整大对象
    • 获取Bigkey:redis-cli <ip+port> bigkeys(大于10K)
  • CPU饱和:把单核CPU占用率达到100%
  • 持久化阻塞:a) fork阻塞;b)AOF刷盘阻塞;c) HugePage写操作阻塞

7-3.外在原因

  • CPU竞争:其他进程过度消耗CPU时,会影响Redis性能
  • 内存交换:使用的部分内存换出到硬盘
  • 网络问题:a)网络闪断;b)连接拒绝;c)连接溢出

8.内存

8-1.内存消耗

重点指标:mem_fragmentation_ratio

mem_fragmentation_ratio = used_memory_rss / used_memory
used_memory_rss: 系统认为Redis使用的物理内存
used_memory: 内部存储的所有数据占用量
mem_fragmentation_ratio > 1表示存在内存碎片
mem_fragmentation_ratio < 1表示存在交换内存
  • Redis内存消耗划分

    • 自身内存
    • 对象内存:用户所有数据
    • 缓冲内存:客户端缓冲/复制积压缓冲/AOF缓冲
    • 内存碎片:频繁更新操作或大量过期键删除导致释放的空间无法利用
  • 子进程内存消耗:指AOF/RDB重写时Redis创建的子进程复制内存的消耗
  • THP机制:虽然可以降低fork子进程的速度,但复制内存页的单位会从4KB变为2MB,如果父进程有大量写命令,会加重内存拷贝量

8-2.内存管理

Redis使用maxmemroy参数限制最大可用内存,限制内存的主要目的:

1.缓存场景:当超过内存上限时根据淘汰策略删除键释放内存
2.防止所用内存超过物理内存(限制的是used_memory;所以考虑内存溢出)

内存回收策略

  • 删除过期键带有

    • 惰性删除:如果存在过期键,当客户单请求到的时候进行删除并返回空
    • 定时任务删除:默认每秒运行10次(通过配置hz控制);删除过期键逻辑采用自适应算法,根据键的过期比例,使用快慢两种速率模式回收键
  • 内存溢出控制策略:当达到maxmemory自动触发

    • noeviction:默认策略,不删除任何数据,拒绝所有写入操作并返回客户端错误信息
    • volatile-lru:根据LRU算法删除设置超时属性的键
    • volatile-random:随机删除过期键
    • allkeys-lru:根据LRU算法随机删除键
    • allkeys-random:随机删除所有键
    • volatile-ttl:根据键对象的ttl属性删除最近将要过期数据
1.关于volatile-lru和volatile-ttl控制策略:如果没有,会回退到noeviction控制策略
2.在Redis的LRU算法中:可以通过设置样本的数量来调优算法精度(参数:maxmemory-samples 5->10)

8-3.内存优化

内存优化总结

1.精简键值对大小,键值字面量精简,使用高效二进制序列化工具。
2.使用对象共享池优化小整数对象。
3.数据优先使用整数,比字符串类型更节省空间。
4.优化字符串使用,避免预分配造成的内存浪费。
5.使用ziplist压缩编码优化hash、list等结构,注重效率和空间的平衡。
6.使用intset编码优化整数集合。
7.使用ziplist编码的hash结构降低小对象链规

9.高可用架构

主从架构问题

  1. 当主节点出现故障,需要手动晋升新的主节点
  2. 主节点的写能力受单机限制
  3. 主节点的存储能力受单机限制

Sentinel架构问题

  1. 系统架构变复杂后,较难支持在线扩容
  2. 主节点的存储能力受单机限制

Cluster架构问题

  1. 部分功能受限(key批量操作,key事务操作等等)

9-1.Sentinel

Sentinel节点集合会定期对所有节点进行监控,从而实现主从的故障自动转移

  • 监控任务

    • 每隔10s:每个Sentinel节点会向master节点执行info命令获取最新的拓扑信息
    • 每隔2s:每个Sentinel节点会向Redis数据节点的__sentinel__频道发送消息而发现新Sentinel节点及交换主节点状态
    • 每隔1s:每个Sentinel节点会向其他数据节点和Sentinel节点发送ping命令来进行心跳检测
  • 主观下线和客观下线

    • 主观下线:任意Sentinel节点进行心跳检测时在超时时间内没有收到响应消息即判断该数据节点下线
    • 客观下线:所有Sentinel节点的判断票超过quorum个数后,即认为该数据节点下线
  • 领导Sentinel节点选举:故障转移工作的执行者

    • 采用Raft算法进行选举,一般来说哪个Sentinel节点发现客观下线情况就会成为执行者
  • 故障转移过程:

    1. 执行者在从列表中选举出一个节点作为新节点:优先级 > 复制偏移量 > Runid小的
    2. 执行者对新节点执行slave no one命令让其成为主节点
    3. 执行者向其他节点发送命令,让其成为新主节点的从节点
    4. Sentinel集合将原来的主节点更新为从节点(恢复后不抢占)
  • 读写分离:将Slave节点集合作为“读”资源连接池,依赖Sentinel节点的消息通知,获取Redis节点的状态变化

9-2.Cluster

  • 数据分布:Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据
  • Gossip协议:节点间不断通信交换信息,一段时间后所有节点都会知道整个集群的元数据信息
  • Gossip消息分类:ping/pong/meet/fail

    • meet消息:用于新节点加入
    • ping消息:用于检测节点是否在线和交换彼此状态信息
    • pong消息:当接收到ping和meet消息时,作为响应消息回复
    • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息
  • 通信节点选择规则:

image.png

  • 集群伸缩:槽和数据在节点之间的移动;当有新节点加入的时候,每个节点负责的槽和数据会迁移一部分给新节点;反之亦然
  • 请求路由:使用客户端去操作集群

    • 请求重定向:Redis首先计算键对应的槽,再根据槽找出对应的节点,如果是自身节点,则执行命令,否则恢复MOVED重定向发送键命令到目标节点(返回key所对应的槽并不负责转发)
  • Smart客户端:内部维护slot-node映射关系,本地实现键到节点的查找,而MOVE重定向只负责维护客户端的映射关系,减少每次都需要redis节点的命令执行重定向带来的IO开销

故障转移

  • 故障发现:通过ping/pong消息实现节点通信

    • 主观下线:某个节点认为另一个节点不可用(标记为下线状态)
    • 客观下线:多个节点认为另一个节点不可用(标记为不可用)
  • 恢复流程:

    1. 资格检查:检查最后与主节点断线时间
    2. 准备选举时间:延迟触发机制,通过offset判断从节点优先级
    3. 发起选举:更配置纪元标识本次从节点选举的版本
    4. 选举投票:从节点拥有N/2+1个主节点票时,可以执行替换操作
    5. 替换主节点:负责故障主节点的槽并向集群广播次消息

开发和运维常见问题::超大规模集群带宽消耗, pub/sub广播问题,集群节点倾斜问题,手动故障转移,在线迁移数据等

10.缓存设计

缓存收益:a)加速速度;b)减少后端负载
缓存成本:a)数据不一致性;b)代码维护;c)运维
缓存场景:a)开销大的复杂计算;b)加速请求响应

10-1.缓存更新策略

LRU/LFU/FIFO算法剔除:缓存使用量超过设定的最大值(maxmemory-policy配置剔除策略)
超时剔除:缓存数据设置过期时间
主动更新:数据一致性要求高,需要真实数据更新后立马更新缓存数据
  • 最佳实践

    • 低一致性业务建议设置最大内存和淘汰策略的方式使用
    • 高一致性业务建议结合超时剔除和主动更新

10-2.缓存穿透

缓存穿透:查询一个根本不存在的数据,导致不存在的数据每次请求都要到存储层去查询,会使后端存储负载加大
基本原因

1.自身业务代码或者数据出现问题
2.恶意攻击、爬虫等造成大量空命中

解决办法

1.缓存空对象
2.布隆过滤器

10-3.缓存雪崩

缓存雪崩:缓存层宕掉后,流量会突然全部打到后端存储
预防和解决缓存雪崩问题

1.保证缓存层服务高可用性
2.依赖隔离组件为后端限流并降级

10-4.无底洞问题

10-5.热点key重建

问题原因:当前key是一个热点key,并发量非常大而重建缓存又不能在短时间内完成
解决办法:互斥锁、“永远不过期”能够在一定程度上解决热点key问题

11.其他

11-1.Linux配置优化

内存分配控制优化

1.Redis设置合理的maxmemory,保证机器有20%~30%的限制内存
2.设置vm.overcommit_memory=1,防止极端情况下造成fork失败

Swap交换内存优化:当物理内存不足时,系统会使用swap导致磁盘IO会成为性能瓶颈

权值越大,使用swap概率越高:0-100 默认60:
echo "vm.swappiness={bestvalue}" >> /etc/sysctl.conf

OMM killer: 当内存不足时选择性杀死进程

降低redis优先级:
echo {value} > /proc/{pid}/oom_adj

Transparent Huge Pages:虽然可以加快fork操作,但是写时内存copy消耗从4KB-2MB

关闭大页:
echo never > /sys/kernel/mm/transparent_hugepage/enabled

打开文件描述符

因为openfile优先级大于redis maxclients
/etc/rc.local配置文件中:ulimit -Sn {max-open-files}

Tcp backlog: Tcp连接队列长度

调高全连接队列值:默认是511
echo 10000 > /proc/sys/net/core/somaxconn

11-2.误操作恢复

  • AOF机制恢复:文件中有追加记录;恢复时,将AOF文件中的fulshall相关操作去掉,然后使用redis-check-aof工具校验和修复一下AOF文件;如果发生AOF重写,意味着之前的数据就丢掉了
  • RDB机制恢复:文件中依然有追加记录;要注意:防止bgsave命令执行,这样旧的RDB文件会被覆盖;如果开启RDB自动策略,flush涉及键值数量较多,RDB文件会被清除,这样恢复就无望

快速恢复数据:

1.防止AOF重写
2.去掉AOF文件中的flush相关内容
3.重启Redis服务器,恢复数据

11-3.Bigkey问题

危害

1.网络拥塞
2.超时堵塞
3.内存空间不均匀

定位:找到键的serializedlength信息,然后判断指定键值的大小

1.debug object key
2.strlen key
3.主动检测:scan+debug object;然后检测每个键值的长度
补充:使用redis-cli -h[ip] -p[port] bigkeys命令(内部进行scan操作,把历史扫描过的最大对象统计出来)

优雅删除:直接删除所有bigkey可能会导致堵塞

可以结合Python的RedisAPI编写脚本去实现:
1.hash key:通过hscan命令,每次获取500个字段,再用hdel命令
2.set key:使用sscan命令,每次扫描集合中500个元素,再用srem命令每次删除一个元素;
3.list key:删除大的List键,通过ltrim命令每次删除少量元素。
4.sorted set key:删除大的有序集合键,和List类似,使用sortedset自带的zremrangebyrank命令,每次删除top 100个元素。
后台删除:lazyfree机制

11-4.分布式锁

11-5.安全建议

  1. 根据具体网络环境决定是否设置Redis密码
  2. rename-command可以伪装命令,但是要注意成本
  3. 合理的防火墙是防止攻击的利器
  4. bind可以将Redis的访问绑定到指定网卡上
  5. 定期备份数据应该作为习惯性操作
  6. 可以适当错开Redis默认端口启动
  7. 使用非root用户启动Redis

参考

  • 《Redis开发与运维》

英格拉姆浩
40 声望12 粉丝

面对焦虑,认识自我,提升技术


« 上一篇
MySQL面试复习1
下一篇 »
MySQL面试复习2