第四章:分析 Redis 性能高原因和核心字符串类型命令
一.引出单线程模型假设现在开启了三个 redis-cli 客户端同时执行命令。客户端 1 设置一个字符串键值对127.0.0.1:6379 set hello world客户端 2 对 counter 做自增操作127.0.0.1:6379 incr counter客户端 3 对 counter 做自增操作127.0.0.1:6379 incr counter我们已经知道从客户端发送的命令经历了发送命令、执行命令、返回结果三个阶段其中我们重点关注第 2 步。我们所谓的 Redis 是采用单线程模型执行命令的是指虽然三个客户端看起来是同时要求 Redis 去执行命令的但微观角度这些命令还是采用线性方式去执行的只是原则上命令的执行顺序是不确定的但一定不会有两条命令同步执行如上 所示可以想象 Redis 内部只有一个服务窗口多个客户端按照它们达到的先后顺序被排队在窗口前(任务放在队列中)依次接受 Redis 的服务所以两条 incr 命令无论执行顺序结果一定是 2不会发生并发问题这个就是 Redis 的单线程执行模型。简而言之请求可能同时到达但是处理请求肯定不会是同时进行redis是单线程啊宏观上3 个客户端是同时请求 Redis 服务的微观上客户端是有前后次序的虽然顺序不确定二.*为什么redis这么“快”效率这么“高”redis的“快”要看谁对比我们一般对比的是mysql。1.存储介质差异Redis 访问内存而传统数据库如 mysql、oracle、sql server访问硬盘。内存的读写速度远快于硬盘这是 Redis 速度快的重要基础举个栗子~Redis 场景数据存储在内存中读取时直接通过内存地址访问耗时约 0.1 毫秒内存读写速度通常为 GB/s 级别。MySQL 场景数据存储在硬盘文件中读取时需要先通过机械硬盘的磁头寻址约 10 毫秒或固态硬盘的闪存寻址约 0.1 毫秒再加载数据到内存总耗时可能达到 10-100 毫秒硬盘读写速度通常为 MB/s 级别。结论内存的物理特性无机械延迟、电子信号传输让 Redis 在数据访问速度上远超依赖硬盘的传统数据库。2.功能复杂度不同Redis 核心功能相对简单而数据库核心功能更复杂。数据库对于数据的插入、删除、查询等操作有更复杂的功能支持这些复杂功能需要花费更多的计算开销例如数据库中的各种约束会使数据库做额外的工作。Redis 提供的功能相比于 mysql 等数据库少了不少干的活少做的事情也简单大部分的查询操作所以速度快。举个栗子~以 “插入一条用户数据” 为例Redis 操作执行SET user:1 name:张三,age:20仅需在内存中分配空间并写入键值对无额外校验耗时约 1 微秒。MySQL 操作执行INSERT INTO user (name, age) VALUES (张三, 20)需完成检查字段类型是否匹配age 必须为数字、验证主键是否重复、更新索引树如 B 树、记录事务日志redo/undo log等耗时约 10-100 微秒。结论Redis 省略了传统数据库的约束校验、事务管理等复杂功能操作更轻量化。3.避免线程竞争开销Redis 采用单线程模型避免了不必要的线程竞争开销。Redis 的每个基本操作都很快速只是简单操作内存不是特别消耗 CPU 的操作就算搞多个线程性能提升也不大。举个栗子~假设有 1000 个并发请求同时读取一个热门商品的库存Redis 场景单线程按顺序处理请求每个请求直接读取内存中的库存值无需加锁总耗时约 10 毫秒单线程无切换开销。多线程数据库场景为了并发处理可能启动 10 个线程每个线程处理 100 个请求。但线程间需要通过锁如行锁保证数据一致性线程切换和锁竞争会额外消耗 50 毫秒总耗时可能达到 60 毫秒。结论Redis 的单线程模型避免了线程切换、锁竞争等开销适合高频简单操作I/O 多路复用机制Redis 在处理网络 I/O 时使用了 epoll 这样的 I/O 多路复用机制。一个线程就可以管理多个 socket。对于 TCP 服务来说服务器端为每个客户端安排一个 socket当有很多客户端时会有很多 socket但很多情况下客户端和服务器之间的通信并不频繁大部分 socket 处于静默状态。而 I/O 多路复用机制可以让一个线程处理多个 socket避免了为每个客户端分配一个线程导致的系统开销增大问题。在 Linux 系统上epoll 是效率最高的 I/O 多路复用 API2006 年左右出现C 可以直接使用 Linux 原生的 epoll apiJava 可以使用 NIO标准库提供的一组类底层封装了 epoll。举个栗子~假设你要帮自己、同学 A 和同学 B 分别买蛋炒饭、肉夹馍和饺子这就相当于 Redis 要处理多个客户端的请求。传统方式的问题方案 1单线程串行你一个人去买先买蛋炒饭等做好了再去买肉夹馍再等做好了去买饺子。这种方式效率很低就像 Redis 单线程串行处理每个客户端请求一个请求处理完再处理下一个中间等待的时间都被浪费了。方案 2多线程并行你们三个人一起去买各自买自己的。这种方式效率高但需要多个人多线程系统开销大就像传统的多线程处理方式需要为每个客户端请求分配一个线程资源消耗大。epoll 的作用还有一个方案 就是 epoll 的工作方式。你一个人去买先去买蛋炒饭在等蛋炒饭做好的过程中你可以去买肉夹馍在等肉夹馍做好的过程中你又可以去买饺子。当其中任何一个做好了老板会喊你事件通知你就去取。这样你一个人单线程就可以同时处理多个任务多个客户端请求大大提高了效率。对应到 RedisRedis 使用 epoll 这种 I/O 多路复用机制就像方案 3 中的你。Redis 单线程可以同时监听多个客户端的连接和数据读写请求就像你同时处理买三种小吃的任务。当某个客户端有数据要读取或写入时就像某份小吃做好了epoll 会通知 Redis 线程Redis 线程就会去处理这个客户端的请求处理完后又可以继续监听其他客户端。这样 Redis 用单线程就高效地处理了大量的客户端请求避免了多线程的开销同时也避免了单线程串行处理的低效。三.Redis 字符串类型核心命令3.1SET和PX EX,NX XX的组合EX seconds⸺使用秒作为单位设置 key 的过期时间。PX milliseconds⸺使用毫秒作为单位设置 key 的过期时间。NX ⸺只在 key 不存在时才进行设置即如果 key 之前已经存在设置不执行。XX ⸺只在 key 存在时才进行设置即如果 key 之前不存在设置不执行。SET 命令⽀持多种选项来影响它的⾏为3.2Redis 的 GET ,MSET,MGET命令对于 GET 来说只支持字符串类型的 value。如果 value 是其他类型使用 GET 获取就会出错MSET 命令一次操作多组键值对。注意一次设置 10w 个键值对可能会把 Redis 给阻塞住。MGET 命令一次获取多个键的值。时间复杂度O(N)N 是 key 数量可以认为是 O(1)。此处的 N 不是整个 Redis 服务器中所有 key 的数量而只是当前命令中给出的 key 的数量。使⽤ mget / mset 由于可以有效地减少了⽹络时间所以性能相较更⾼。假设⽹络耗时 1 毫秒命令执⾏时间耗时 0.1 毫秒学会使⽤批量操作可以有效提⾼业务处理效率但是要注意每次批量操作所发送的键的数量也不是⽆节制的否则可能造成单⼀命令执⾏时间过⻓导致 Redis 阻塞。3.3SETNX,SETEX(秒),PSETEX毫秒在 Redis 中SETNX 是一个条件性设置键值对的命令它的全称是 SET if Not eXists即仅当指定的键不存在时才会成功设置键值对并返回 1如果键已存在则不会进行任何操作并返回 0。SETEX 是一个带过期时间的键值设置命令它允许在设置键值对的同时指定一个以秒为单位的生存时间TTL。当过期时间到达后Redis 会自动删除该键值对。PSETEX 的功能与 SETEX 类似但它的时间精度更高支持以毫秒为单位设置键值对的生存时间。3.4Redis 的自增自减命令3.4.1INCR 命令INCR 命令针对 value 1。3.4.2INCRBYINCRBY 可以针对 key 对应的 value 进行 n 操作。注意此时 key 对应的 value 必须是整数。此操作的返回值是 1 之后的值。incr 操作的 key 如果不存在就会把这个 key 的 value 当做 0 来使用。除了非整数超过64位的数字不能自增3.4.3DECRDECR把 key 对应的 value 进行 -1 操作。3.4.4DECRBYDECRBY 把 key 对应的 value 进行 -n 的操作。注意key 对应的 value 必须是整数在 64 位的范围内如果这个 key 对应的 value 不存在则当做 0 来处理。decr 的运算结果也是计算之后的值。3.4.5INCRBYFLOATINCRBYFLOAT 把 key 对应的 value 进行 - 运算运算的操作数可以是浮点数。只能用加上负数的形式来实现减法。时间复杂度上述操作的时间复杂度都是 O (1)。由于 Redis 处理命令的时候是单线程模型多个客户端同时针对同一个 key 进行 incr 操作不会引起 “线程安全” 问题。3.5Redis 的字符串操作命令3.5.1APPENDAPPEND 命令拼接字符串。APPEND 返回值长度的单位是字节。Redis 的字符串不会对字符编码做任何处理Redis 不认识字符只认识字节。当前 xshell 终端默认的字符编码是 utf8在终端中输入汉字之后也就是按照 utf8 编码的一个汉字在 utf8 字符集中通常是 3 个字节。3.5.2GETRANGEGETRANGE 命令获取字符串的子串由 start 和 end 确定左闭右闭Redis 指定的区间是闭区间。C 和 Java 中谈到一个区间大多都是前闭后开左闭右开。正常下标都是从 0 开始的整数Redis 的下标是可以支持负数的-1 倒数第一个元素下标为 len - 1 的元素这和 Python 的设定是一致的。如果字符串中保存的是汉字此时进行子串切分很可能切出来的就不是完整的汉字了上述代码是强行切出了中间的四个字节随便这么一切切出的结果在 utf8 码表上不知道能查出啥了。上述问题在 C 中同样存在Java 中就没有因为 Java 中字符串的基本单位是字符Java 的字符占 2 个字节Java 中相当于 String 帮我们把汉字的编码转换都处理好了而 C 中字符串的基本单位是字节对于汉字的处理需要程序猿手动处理。3.5.3SETRANGESETRANGE 命令从指定的偏移量开始替换字符串的部分内容。凭空生成了一个字节这个字节里的内容就是 0x00aaa 就被追加到 0x00 的后面了。setrange 针对不存在的 key 也是可以操作的不过会把 offset 之前的内容填充成 0x00。如果当前 value 是一个中文字符串进行 setrange 的时候可能会出问题。3.5.4STRLENSTRLEN 命令功能获取字符串的长度单位是字节。C 中字符串的长度本身就是用字节为单位。Java 中字符串的长度则是以字符为单位的Java 中的一个 char 等于 2 字节Java 中的 char 基于 unicode 这样的编码方式就能够表示中文等符号。MySQL 的 varchar(N) 中N 的单位是字符MySQL 中的字符也可以是完整的汉字这样的一个字符也可能是多个字节。一个汉字通常是 3 个字节编码方式是 utf8但 Java 中一个 char 是 2 字节使用 unicodeJava 中的 String 则是用的 utf8一个汉字就是 3 个字节Java 的标准库内部在进行上述操作过程中程序一般是感知不到编码方式的变换的。总结1. 多个客户端可同时向 Redis 发送命令请求如设置字符串键值对、对同一个计数器做自增操作。2. Redis 采用单线程模型执行命令微观上命令按线性顺序执行执行顺序虽不确定但不会有两条命令同步执行能保证像两个自增命令执行后结果一定正确不会出现并发问题。宏观上客户端是同时请求的微观上有先后次序。3. Redis 基于内存存储数据传统数据库如 MySQL 等基于硬盘存储。内存读写速度远快于硬盘这是 Redis 速度快的重要基础。4. Redis 核心功能相对简单而传统数据库功能复杂在数据操作上有更多复杂功能支持如各种约束等这些会增加计算开销。Redis 功能少且操作简单主要是查询操作所以速度快。5. Redis 采用单线程模型避免了线程竞争带来的额外开销。Redis 基本操作都是快速的内存操作不怎么消耗 CPU多线程对性能提升不大。6. Redis 使用 epoll 等 I/O 多路复用机制处理网络 I/O一个线程可管理多个 socket。避免了为每个客户端分配线程带来的系统开销增大问题提高了 I/O 处理效率。7. EX seconds以秒为单位设置 key 的过期时间。8. PX milliseconds以毫秒为单位设置 key 的过期时间。9. NX仅当 key 不存在时才设置。10. XX仅当 key 存在时才设置。11. GET仅支持获取字符串类型的 value若 value 为其他类型则获取出错。12. MSET可一次操作多组键值对但一次设置过多如 10w 个可能阻塞 Redis。13. MGET可一次获取多个键的值时间复杂度为 O(N)N 为当前命令中 key 的数量可视为 O(1)。14. SETNX仅当 key 不存在时设置键值对。15. SETEX设置键值对的同时指定以秒为单位的生存时间。16. PSETEX设置键值对的同时指定以毫秒为单位的生存时间。17. INCR将 key 对应的 value 加 1。18. INCRBY将 key 对应的 value 加指定整数 n。19. DECR将 key 对应的 value 减 1。20. DECRBY将 key 对应的 value 减指定整数 n。21. INCRBYFLOAT将 key 对应的 value 进行加减浮点数运算。22. 这些操作时间复杂度均为 O(1)且因 Redis 单线程模型多客户端同时操作同一 key 不会有线程安全问题。23. APPEND用于拼接字符串返回值为拼接后字符串的字节长度Redis 字符串按二进制存储不做编码转换。24. GETRANGE获取字符串子串区间为左闭右闭若字符串含汉字切分可能得到不完整汉字该问题在 C 中也存在Java 因字符串基本单位是字符所以无此问题。25. SETRANGE从指定偏移量开始替换字符串部分内容对不存在的 key 操作会在偏移量前填充 0x00若 value 是中文字符串可能出问题。26. STRLEN获取字符串的长度单位是字节。