Redis - String

Redis - String String 字符串字符串类型是 Redis 最基础的数据类型关于字符串需要特别注意Redis 中所有的键的类型都是字符串类型而且其他几种数据结构也都是在字符串类型基础上构建的例如列表和集合的元素类型是字符串类型所以字符串类型能为其他 4 种数据结构的学习奠定基础。字符串类型的值实际可以是字符串包含一般格式的字符串或者类似 JSON、XML 格式的字符串数字可以是整型或者浮点型甚至是二进制流数据例如图片、音频、视频等。不过一个字符串的最大值不能超过512 MB。由于 Redis 内部存储字符串完全是按照二进制流的形式保存的所以 Redis 是不处理字符集编码问题的客户端传入的命令中使用的是什么字符集编码就存储什么字符集编码。不会做任何编码转换存的是啥取出来的就是啥 — 存的不仅仅是文本数据音 / 视频体积可能会比较大Redis 对 String 类型限制了大小最大是512 MBRedis 是单线程模型希望执行的操作都能比较快速。常见命令SET将 string 类型的 value 设置到 key 中。如果 key 之前存在则覆盖无论原来的数据类型是什么。之前关于此 key 的 TTL 也全部失效。语法SET key value [expiration EX seconds|PX milliseconds] [NX|XX]命令有效版本1.0.0 之后时间复杂度O(1) 下面是其相关选项EX seconds— 以秒为单位设置 key 的过期时间。set key value ex 10 // 相当于: set key value expire key 10 // 对于这里一个操作时他是原子的但是分开的两个操作就不是原子的了 // 对于后面的分布式锁就涉及到原子性PX milliseconds— 以毫秒为单位设置 key 的过期时间。NX— 只在 key 不存在时才进行设置如果 key 已存在设置不执行。相当于 MySQL 的if not existsXX— 只在 key 存在时才进行设置新的 value 会覆盖旧值可能会改变原数据类型且该 key 原有的 TTL生存时间会失效如果 key 不存在设置不执行。补充说明Redis 文档给出的语法格式说明[ ]相当于一个独立的单元表示可选性其中|表示 “或者” 的意思多个只能出现一个[ ]和[ ]之间是可以同时存在的注意由于带选项的 SET 命令可以被 SETNX、SETEX、PSETEX 等命令代替所以之后的版本中Redis 可能进行合并。返回值如果设置成功返回OK。如果由于 SET 指定了 NX 或者 XX 但条件不满足SET 不会执行并返回(nil)。示例redis EXISTS mykey (integer) 0 redis SET mykey Hello OK redis GET mykey Hello redis SET mykey World NX (nil) redis DEL mykey (integer) 1 redis EXISTS mykey (integer) 0 redis SET mykey World XX (nil) redis GET mykey (nil) redis SET mykey World NX OK redis GET mykey World redis SET mykey Will expire in 10s EX 10 OK redis GET mykey Will expire in 10s redis GET mykey # 10秒之后 (nil)有一个快速失去年终奖的小技巧清除 Redis 上的所有数据也称之为删库 — 对标 MySQL 的DROP DATABASE可以将 Redis 上的所有键值对全部清空FLUSHALLGET获取 key 对应的 value。如果 key 不存在返回 nil。如果 value 的数据类型不是 string会报错。GET 只是支持字符串类型的 value如果 value 是其他类型使用 GET 就会出错语法GET key命令有效版本1.0.0 之后 时间复杂度O(1) 返回值key 对应的 value或者 nil 当 key 不存在。 示例127.0.0.1:6379 lpush key3 11 22 33 (integer) 3 127.0.0.1:6379 get key3 (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379 type key3 listMGET一次性获取多个 key 的值。如果对应的 key 不存在或者对应的数据类型不是 string返回 nil。语法MGET key [key ...]命令有效版本1.0.0 之后时间复杂度O(N) N 是 key 数量返回值对应 value 的列表127.0.0.1:6379 mset key1 111 key2 222 key3 333 OK 127.0.0.1:6379 mget key1 key2 key3 1) 111 2) 222 3) 333 127.0.0.1:6379 mget key1 key2 key4 1) 111 2) 222 3) (nil) 127.0.0.1:6379MSET一次性设置多个 key 的值。语法MSET key value [key value ...]命令有效版本1.0.1 之后时间复杂度O(N)N 是当前命令中的 key 数量我们可以认为是 O(1) 的返回值永远是 OK127.0.0.1:6379 mset key1 111 key2 222 key3 333 OK 127.0.0.1:6379 mget key1 key2 key3 1) 111 2) 222 3) 333 127.0.0.1:6379 mget key1 key2 key4 1) 111 2) 222 3) (nil) 127.0.0.1:6379一次网络传输就可以获取多个操作使用mget/mset可以有效减少网络时间因此性能相对更高。假设网络耗时1毫秒命令执行时间耗时0.1毫秒。1000次get和1次mget对比操作方式网络耗时ms命令执行耗时ms总耗时ms1000 次 get1000 × 1 10001000 × 0.1 10011001 次 mget1000 个键1 × 1 11000 × 0.1 100101网络耗时每次请求需要 1ms 的网络开销。命令执行耗时每个键的操作需要 0.1ms 的服务器处理时间。mget 优势批量操作只需 1 次网络往返因此总耗时大幅降低101ms vs 1100ms。学会使用批量操作可以有效提高业务处理效率但是要注意每次批量操作所发送的键的数量也不是无节制的否则可能造成单一命令执行时间过长导致 Redis 阻塞。SETNX/SETEX/PSETEXsetnx不存在才能设置否则设置失败setex设置key的同时指定超时时间单位为秒psetex设置key的同时指定超时时间单位为毫秒命令有效版本1.0.0 之后时间复杂度O(1)返回值1 表示设置成功。0 表示没有设置。127.0.0.1:6379 setnx key1 111 (integer) 1 127.0.0.1:6379 get key1 111 127.0.0.1:6379 setnx key1 121 (integer) 0 127.0.0.1:6379 get key1 111 127.0.0.1:6379 setex key2 10 222 OK 127.0.0.1:6379 ttl key2 (integer) 5 127.0.0.1:6379 ttl key2 (integer) -2 127.0.0.1:6379 get key2 (nil) 127.0.0.1:6379 127.0.0.1:6379 psetex key3 5000 333 OK 127.0.0.1:6379 pttl key3 (integer) 1829 127.0.0.1:6379 pttl key3 (integer) -2 127.0.0.1:6379 get key3 (nil) 127.0.0.1:6379计数命令INCR将 key 对应的 string 表示的数字value加一。如果 key 不存在则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型则报错。语法INCR key命令有效版本1.0.0 之后时间复杂度O(1)返回值integer 类型的加完后的数值。127.0.0.1:6379 set key 10 OK 127.0.0.1:6379 incr key (integer) 11 127.0.0.1:6379 get key 11 127.0.0.1:6379 set key2 hello OK 127.0.0.1:6379 incr key2 (error) ERR value is not an integer or out of range 127.0.0.1:6379 set key2 1.5 OK 127.0.0.1:6379 incr key2 (error) ERR value is not an integer or out of range 127.0.0.1:6379 set key2 22222222222222222222222222222222222222222222222222222222222222222222 OK 127.0.0.1:6379 incr key2 (error) ERR value is not an integer or out of range 127.0.0.1:6379 get key3 (nil) 127.0.0.1:6379 incr key3 (integer) 1INCRBY将 key 对应的 string 表示的数字加上对应的值正负数的都是可以的。如果 key 不存在则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型则报错。语法INCRBY key decrement命令有效版本1.0.0 之后时间复杂度O(1)返回值integer 类型的加完后的数值。redis EXISTS mykey (integer) 0 redis INCRBY mykey 3 (integer) 3 redis SET mykey 10 OK redis INCRBY mykey 3 (integer) 13 redis INCRBY mykey not a number (error) ERR value is not an integer or out of range redis SET mykey 234293482390480948029348230948 OK redis INCRBY mykey 3 (error) value is not an integer or out of range redis SET mykey not a number OK redis INCRBY mykey 3 (error) value is not an integer or out of rangeDECR将 key 对应的 string 表示的数字减一。如果 key 不存在则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型则报错。语法DECR key命令有效版本1.0.0 之后时间复杂度O(1)返回值integer 类型的减完后的数值。redis EXISTS mykey (integer) 0 redis DECR mykey (integer) -1 redis SET mykey 10 OK redis DECR mykey (integer) 9 redis SET mykey 234293482390480948029348230948 OK redis DECR mykey (error) value is not an integer or out of range redis SET mykey not a number OK redis DECR mykey (error) value is not an integer or out of rangeDECRBY将 key 对应的 string 表示的数字减去对应的值。如果 key 不存在则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型则报错。语法DECRBY key decrement命令有效版本1.0.0 之后 时间复杂度O(1)返回值integer 类型的减完后的数值。redis EXISTS mykey (integer) 0 redis DECRBY mykey 3 (integer) -3 redis SET mykey 10 OK redis DECRBY mykey 3 (integer) 7 redis DECRBY mykey not a number (error) ERR value is not an integer or out of range redis SET mykey 234293482390480948029348230948 OK redis DECRBY mykey 3 (error) value is not an integer or out of range redis SET mykey not a number OK redis DECRBY mykey 3 (error) value is not an integer or out of rangeINCRBYFLOAT将 key 对应的 string 表示的浮点数加上对应的值。如果对应的值是负数则视为减去对应的值。如果 key 不存在则视为 key 对应的 value 是 0。如果 key 对应的不是 string或者不是一个浮点数则报错。允许采用科学计数法表示浮点数。没有 decrbyfloat语法INCRBYFLOAT key increment命令有效版本2.6.0 之后时间复杂度O(1)返回值加/减完后的数值。redis SET mykey 10.50 OK redis INCRBYFLOAT mykey 0.1 10.6 redis INCRBYFLOAT mykey -5 5.6 redis SET mykey 5.0e3 OK redis INCRBYFLOAT mykey 2.0e2 5200很多存储系统和编程语言内部使用 CAS 机制实现计数功能会有一定的 CPU 开销但在 Redis 中完全不存在这个线程安全问题因为 Redis 是单线程架构任何命令到了 Redis 服务端都要顺序执行。其他命令针对字符串也支持一些常用的操作 --- 拼接获取/修改字符串的部分内容获取字符串长度...补充知识在启动 Redis 客户端的时候在尾部加上 --raw 这个选项就可以使 Redis 客户端能够自动的把二进制数据尝试翻译 --- 中文通过 get 翻译就不是编码了APPEND功能如果 key 已存在且是字符串将 value 追加到原字符串末尾如果 key 不存在等同于 SET 命令语法APPEND key value版本2.0.0时间复杂度O(1)返回值追加后字符串的长度redis EXISTS mykey (integer) 0 redis APPEND mykey Hello (integer) 5 redis GET mykey Hello redis APPEND mykey World (integer) 11 redis GET mykey Hello WorldGETRANGE相当于C的 std::string 中的 substr功能返回 key 对应的 string 的⼦串由 start 和 end 确定左闭右闭C 的风格是左闭右开。可以使⽤负数表⽰倒数。-1 代表倒数第⼀个字符可以看成len -1-2 代表倒数第⼆个其他的与此类似。超过范围的偏移量会根据 string 的⻓度调整成正确的值。--- 下标通常都是从0开始的但是支持负数和 Python 一样语法GETRANGE key start end版本2.4.0时间复杂度O(N)N为[start,end]区间长度通常视为O(1)返回值子串示例redis SET mykey This is a string OK redis GETRANGE mykey 0 3 This redis GETRANGE mykey -3 -1 ing redis GETRANGE mykey 0 -1 This is a string redis GETRANGE mykey 10 100 string注意如果字符串中保存的是汉字此时进行字串切分很可能切出来的就不是完整的汉字了切出来的结构在 utf-8 码表上不知道会查出来是什么对于这种问题在 C 中也是同样存在的但是 Java 就没有关系因为他的字符串基本单位是字符不是字节SETRANGE功能从指定偏移量开始覆盖字符串的一部分语法SETRANGE key offset value版本2.2.0时间复杂度O(N)N 为 value 长度通常视为 O(1)返回值替换后字符串的长度长度的单位是字节不是字符示例redis SET key1 Hello World OK redis SETRANGE key1 6 Redis (integer) 11 redis GET key1 Hello RedisSTRLEN功能获取字符串的长度key 不存在时返回0语法STRLEN key版本2.2.0时间复杂度O(1)返回值字符串长度示例redis SET mykey Hello world OK redis STRLEN mykey (integer) 11 redis STRLEN nonexisting (integer) 0补充MySQL 中的 varchar(N)此处的 N 单位是字符字符串命令小结命令执行效果时间复杂度set key value设置key的值O(1)get key获取key的值O(1)del key [key...]删除keyO(1)mset key value [key value...]批量设置O(k),k为键个数mget key [key...]批量获取O(k),k为键个数incr key值1O(1)decr key值-1O(1)incrby key n值nO(1)decrby key n值-nO(1)incrbyfloat key n值n(浮点)O(1)append key value追加值O(1)strlen key获取长度O(1)setrange key offset value覆盖部分值O(n),通常视为O(1)getrange key start end获取子串O(n),通常视为O(1)内部编码字符串类型有3种内部编码OBJECT encoding key 进行查看int8字节长整型embstr≤39字节的字符串raw39字节的字符串Redis 会根据值的类型和长度动态决定使用哪种编码。小数是使用 embstr 字符串来存储的这就意味着每次进行算术运算都需要将字符串转成小数进行运算结果再转回字符串保存--- 这就有开销了典型使用场景缓存功能Redis 作为缓冲层MySQL 作为存储层绝⼤部分请求的数据都是从 Redis 中获取。由于 Redis 具有⽀撑⾼并发的特性所以缓存通常能起到加速读写和 降低后端压⼒的作⽤。Redis 能支撑高并发本质是架构设计 实现特性共同作用的结果主要有以下几点1.纯内存操作✨数据都存在内存中读写完全避开磁盘 I/O 瓶颈响应时间在微秒级~1ms远快于 MySQL 这类磁盘数据库。单实例可以轻松达到10 万 QPS每秒请求数远超传统关系型数据库。2.单线程 I/O 多路复用⚙️单线程避免了多线程的锁竞争、上下文切换开销保证每个请求都能快速处理没有线程调度损耗。I/O 多路复用epoll/select/poll用一个线程同时监听多个网络连接高效处理大量并发客户端请求不会被单个慢请求阻塞。3.高效的数据结构与命令底层实现了 SDS简单动态字符串、跳表、哈希表等高效数据结构命令执行时间复杂度大多为O (1) 或 O (logN)保证单命令处理极快。大部分操作都是原子性的无需额外加锁进一步提升并发处理能力。4.无阻塞网络模型采用 Reactor 模式的网络事件模型网络 I/O 与命令执行分离不会因为等待网络数据而阻塞整个服务。客户端连接、请求读取、命令执行、结果返回都在事件循环中异步处理最大化 CPU 利用率。5.水平扩展能力支持主从复制、哨兵模式、Redis Cluster 集群可以通过分片将数据分布到多个节点线性扩展并发能力和存储容量。集群模式下多个节点共同提供服务整体 QPS 可突破单机上限支撑百万级甚至更高并发。Redis MySQL 组成的缓存存储架构下面伪代码模拟了上图的业务数据访问过程1. 假设业务是根据用户uid获取用户信息UserInfo getUserInfo(long uid) { }2. BAM Redis 获取用户信息我们假设用户信息保存在user:info:uid对应的键中// 根据 uid 得到 Redis Key String key user:info: uid; // 尝试从 Redis 中获取对应的值 String value Redis.get(key); // 如果缓存命中 (hit) if (value ! null) { // 假设我们的用户信息按照 JSON 格式存储 UserInfo userInfo JSON.parseObject(value, UserInfo.class); return userInfo; }3. 如果没有从 Redis 中得到用户信息及缓存 miss则进一步从 MySQL 中获取对应的信息随后写入缓存并返回// 如果缓存未命中 (miss) if (value null) { // 从数据库中根据 uid 获取用户信息 UserInfo userInfo MySQL.select(select * from user_info where uid uid); // 如果表中没有 uid 对应的用户信息 if (userInfo null) { // 响应 404 return null; } // 将用户信息序列化成 JSON 格式 String value JSON.toJSONString(userInfo); // 写入缓存为了防止数据腐烂 (rot) 设置过期时间为 1 小时 (3600s) Redis.set(key, value, EX, 3600); // 返回用户信息 return userInfo; }通过增加缓存功能在理想情况下每个用户信息一个小时期间只会有一次 MySQL 查询极大地提升了查询效率也降低了 MySQL 的访问数。上述策略存在一个明显的问题随着时间的推移肯定会有越来越多的 key 在 Redis 上访问不到从而从 mysql 读取并写入 redis 中此时 redis 中的数据不是越来越多了吗解决方式在把数据写给 Redis 的同时给这个 key 设置一个过期时间与 MySQL 等关系型数据库不同的是Redis 没有表、字段这种命名空间而且也没有对键名有强制要求除了不能使用一些特殊字符。但设计合理的键名有利于防止键冲突和项目的可维护性比较推荐的方式是使用业务名:对象名:唯一标识:属性作为键名。例如 MySQL 的数据库名为vs用户表名为user_info那么对应的键可以使用vs:user_info:6379,vs:user_info:6379:name来表示如果当前 Redis 只会被一个业务使用可以省略业务名vs:。如果键名过程则可以使用团队内部都认同的缩写替代例如user:6379:friends:messages:5217可以被u:6379:fr:m:5217代替。毕竟键名过长还是会导致 Redis 的性能明显下降的。计数 (Counter) 功能许多应用都会使用 Redis 作为计数的基础工具它可以实现快速计数、查询缓存的功能同时数据可以异步处理或者落地到其他数据源。如下图所示例如视频网站的视频播放次数可以使用 Redis 来完成用户每播放一次视频相应的视频播放数就会自增 1。记录视频播放次数在视频网站中当用户播放视频时系统会通过执行Redis命令如INCR来异步增加视频的播放计数。这种异步处理方式有几个关键好处首先它允许网站快速响应用户操作而无需等待数据库更新从而提升用户体验其次它减轻了数据库的即时写入压力因为播放计数可以先在Redis中累积然后批量同步到数据库这有助于提高整体系统的性能和稳定性最后这种架构提高了系统的可扩展性因为可以通过增加处理后台任务的服务来应对流量增长而不影响前端服务。此外异步处理还便于实现错误处理和重试机制确保数据的最终一致性同时解耦了不同系统组件增强了系统的灵活性和可维护性。// 在 Redis 中统计某视频的播放次数 long incrVideoCounter(long vid) { String key video: vid; long count Redis.incr(key); return count; }实际中要开发一个成熟、稳定的真实计数系统要面临的挑战远不止如此简单防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等。共享会话 (Session)如下图所示一个分布式 Web 服务将用户的 Session 信息例如用户登录信息保存在各自的服务器中但这样会造成一个问题出于负载均衡的考虑分布式服务会将用户的访问请求均衡到不同的服务器上并且通常无法保证用户每次请求都会被均衡到同一台服务器上这样当用户刷新一次访问是可能会发现需登录这个问题是用户无法容忍的。Session 分散存储如果每一个应用服务器都要维护自己的会话数据此时彼此之间不共享用户请求访问到不同的服务器上就可能会出现一些不能正确处理的情况了为了解决这个问题可以使用 Redis 将用户的 Session 信息进行集中管理如下图所示在这种模式下只要保证 Redis 是高可用和可扩展性的无论用户被均衡到哪台 Web 服务器上都集中从 Redis 中查询、更新 Session 信息。Redis 集中管理 Session会话回顾 Session谈到 Session我们会想到和他联系的东西 --- CookieCookie 是属于浏览器存储数据的机制相比之下Session 是服务器存储数据的机制两者一般都是按照键值对的方式来存储数据手机验证码很多应用出于安全考虑会在每次进行登录时让用户输入手机号并且配合给手机发送验证码然后让用户再次输入收到的验证码并进行验证从而确定是否是用户本人。为了短信接口不会频繁访问会限制用户每分钟获取验证码的频率例如一分钟不能超过 5 次如下图所示。此功能可以用以下伪代码说明基本实现思路String 发送验证码(String phoneNumber) { // 1. 构造限流key控制1分钟内最多发送5次 String key shortMsg:Limit: phoneNumber; // 2. 设置1分钟过期仅当key不存在时设置成功首次发送 // set key 1 ex 60 nx boolean setSuccess Redis.set(key, 1, NX, EX, 60); if (!setSuccess) { // key已存在 → 不是首次发送发送次数1 long count Redis.incr(key); // 超过1分钟5次限制拒绝发送 if (count 5) { return null; } } // 3. 生成6位数字验证码 String validationCode 生成随机的6位数的验证码(); // 4. 存储验证码5分钟有效 String validationKey validation: phoneNumber; Redis.set(validationKey, validationCode, EX, 300); // 5. 返回验证码用于短信发送 return validationCode; } // 验证用户输入的验证码是否正确 boolean 验证验证码(String phoneNumber, String validationCode) { String validationKey validation: phoneNumber; // 获取缓存中的验证码 String value Redis.get(validationKey); // 无记录 → 验证失败 if (value null) { return false; } // 比对验证码是否一致 return value.equals(validationCode); }以上介绍了使用 Redis 的字符串数据类型可以使用的几个场景但其适用场景远不止于此开发人员可以结合字符串类型的特点以及提供的命令充分发挥自己的想象力在自己的业务中去找到合适的场景去使用 Redis 的字符串类型。下面是对应的使用场景的C代码#include string // 假设存在UserInfo类和JSON库 #include UserInfo.h #include JSON.h #include RedisClient.h #include MySQLClient.h // 根据用户uid获取用户信息 UserInfo getUserInfo(long uid) { // 根据uid得到Redis key std::string key user:info: std::to_string(uid); // 尝试从Redis中获取对应的值 std::string value RedisClient::get(key); // 如果缓存命中(hit) if (!value.empty()) { // 假设我们的用户信息按照JSON格式存储 UserInfo userInfo JSON::deserialize(value); return userInfo; } // 如果缓存未命中(miss)则进一步从MySQL中获取对应的信息随后写入缓存并返回 UserInfo userInfo MySQLClient::getUserInfo(uid); // 如果表中没有uid对应的用户信息 if (userInfo.isEmpty()) { // 响应404 return nullptr; } // 将用户信息序列化成JSON格式 value JSON::serialize(userInfo); // 写入缓存为了防止数据腐烂(rot)设置过期时间为1小时(3600秒) RedisClient::set(key, value, 3600); // 返回用户信息 return userInfo; } // 在Redis中统计某视频的播放次数 long incrVideoCounter(long vid) { std::string key video: std::to_string(vid); long count RedisClient::incr(key); return count; } // 发送验证码 std::string sendVerificationCode(const std::string phoneNumber) { std::string key shortMsg:Limit: phoneNumber; // 设置过期时间为1分钟(60秒) // 使用NX只在不存在key时才能设置成功 bool r RedisClient::set(key, 1, ex, 60, nx); if (!r) { // 说明之前设置过该手机的验证码了 long c RedisClient::incr(key); if (c 5) { // 说明超过了一分钟5次的限制了 // 限制发送 return ; } } // 说明要么之前没有设置过手机的验证码要么次数没有超过5次 std::string validationCode generateRandomCode(6); // 假设存在生成随机验证码的函数 std::string validationKey validation: phoneNumber; // 验证码5分钟(300秒)内有效 RedisClient::set(validationKey, validationCode, 300); // 返回验证码随后通过手机短信发送给用户 return validationCode; } // 验证用户输入的验证码是否正确 bool verifyCode(const std::string phoneNumber, const std::string validationCode) { std::string validationKey validation: phoneNumber; std::string value RedisClient::get(validationKey); if (value.empty()) { // 说明没有这个手机的验证码记录验证失败 return false; } return value validationCode; }补充我们要先理解业务