• 【Redis】深入探索 Redis 的数据类型 —— 字符串 string



    前言

    string 字符串类型是 Redis 中最基础的数据类型,关于字符串类型需要注意以下几点:

    1. 在 Redis 中,所有的 key 的类型都是 string 类型的,并且其他几种数据类型也都是在字符串类型的基础上构建的,例如列表和集合的元素的类型都是字符串类型的。
    2. 在 Redis 中,字符串都是直接按照二进制的形式储存的,因此在使用 Redis 的时候,不需要像 MySQL 那样考虑编码问题(编码不匹配则会出现乱码)。所以 Redis 不会处理字符集的编码问题,客户端传入的命令中使用的是什么字符集编码,就存储什么字符集编码。
    3. Redis 中的 string 类型的值可以是字符串,JSON、XML格式的字符串,数字、整型、浮点数,甚至是二进制流数据,如图片、音频、视频等。不过一个 string 的最大值不能超过 512MB 。

    一、String 类型的操作命令

    设置和获取相关命令

    1. SET 和 GET

    a)SET:

    SET 命令的作用是将 string 类型的 value 设置到 Redis 中。如果 key 在设置之前已经存在了,无论原来的数据类型是什么,都会覆盖原来的 value,并且 key 设置的 TTL 也会失效。

    SET 的语法:

    SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
    
    • 1

    选项说明:

    • EX seconds:以秒为单位设置 key 的过期时间,相当于 EXPIRE;
    • PX milliseconds:以毫秒为单位设置 key 的过期时间,相当于 PEXPIRE;
    • NX:只有在 key 不存在的时候才进行设置,如果 key 之前已经存在了,则设置不会执行;
    • XX:只有在 key 存在的时候才进行设置,通过 key 之间不存在,则设置不会执行。

    注意事项:

    1. []之内的选项只能选择一个,[]之间的选项则可以同时存在。即 EXPX 选项不能同时存在,而 NXXX 不能同时存在,因为它们之间是互斥关系。
    2. 带选项的 SET 命令可以被 SETEX、PSETEX、SETNX等命令替代。

    返回值:

    • 如果设置成功,则返回 OK。
    • 如果由于 SET 指定了 NX 或者 XX 选项但条件不满足,则执行失败,返回 nil

    使用示例:

    EX 和 PX:

    NX 和 XX

    SETSET NXSET XX 执行流程:

    b)GET

    GET 命令的作用是获取 key 对应 的 value。如果 key 不存在,就返回 nil,如果 value 的数据类型不是 string,就会报错。

    使用示例:

    2. MSET 和 MGET

    MSET 的作用是一次设置多个键值对;而 MGET 的作用是一次性获取多个 key 的 value,如果对应的 key 不存在或者对应的数据类型不是 string,返回 nil

    使用案例:

    a)MSET:

    b)MGET:

    注意事项:

    在前面的文章提到过,Redis 是处理任务是单线程的,并且 Redis 的客户端和服务端之间是通过网络进行通信的,因此要设置或者获取多个key 的时候,建议同时进行操作,以减少网络请求的次数。

    3. SETNX、SETEX、SETPX

    a)SETNX 命令的作用是在如果 key 不存在则设置,否则就不设置,例如:


    b)SETEX 命令的作用是在设置 key 的时候指定秒级的过期时间

    语法:

    SETEX key seconds value
    
    • 1

    例如:

    c)PSETEX 命令的作用是在设置 key 的时候指定毫秒级的过期时间

    语法:

    SETEX key milliseconds value
    
    • 1

    例如:

    计数相关命令

    1. INCR 和 INCRBY

    1. INCR

    INCR 命令的作用是将 key 对应的 string 表示的数字加一。

    • 如果 key 不存在,则视为 key 对应的 value 的值为 0,然后再加一;
    • 如果 key 对应的 value 不是一个整数或者其范围超出了 64 位有符号整型,则会报错;
    • 如果 INCR 执行成功,则返回加一后的值,否则返回相应错误信息。

    使用示例:

    1. INCRBY

    INCRBY 命令的作用是为 key 对应的 string 表示的整数加上一个指定的整数。

    • 如果 key 不存在,则视为 key 对应的 value 的值为 0,然后再加指定的数;
    • 如果 key 对应的 value 不是一个整数或者其范围超出了 64 位有符号整型,则会报错;
    • 如果 INCRBY 执行成功,则返回相加后的值,否则返回相应错误信息。

    使用示例:

    2. DECR 和 DECRBY

    1. DECR

    DECR 命令的作用是将 key 对应的 string 表示的数字减一。

    • 如果 key 不存在,则视为 key 对应的 value 的值为 0,然后再减一;
    • 如果 key 对应的 value 不是一个整数或者其范围超出了 64 位有符号整型,则会报错;
    • 如果 DECR 执行成功,则返回减一后的值,否则返回相应错误信息。

    使用示例:

    1. DECRBY

    DECRBY 命令的作用是为 key 对应的 string 表示的整数减去一个指定的整数。

    • 如果 key 不存在,则视为 key 对应的 value 的值为 0,然后再减指定的数;
    • 如果 key 对应的 value 不是一个整数或者其范围超出了 64 位有符号整型,则会报错;
    • 如果 DECRBY 执行成功,则返回相减后的值,否则返回相应错误信息;
    • 如果 DECRBY 指定要减去的是一个负数,则表示加上这个数。

    使用示例:

    3. INCRBYFLOAT

    INCRBYFLOAT命令的作用是将 key 对应的 string 表示的浮点数加上指定的数。

    语法:

    INCRBYFLOAT key increment 
    
    • 1
    • 如果 key 不存在,则默认为0,然后在进行相加操作;
    • 如果 key 对应的 string 不是一个数,则会报错
    • 如果指定的数是负数,则表示减去这个数;
    • 如果 INCRBYFLOAT 执行成功,则返回运算结果,否则返回错误信息;
    • 允许采用科学计数法表示浮点数。

    使用示例:

    字符串操作相关命令

    1. APPEND

    APPEND命令的作用是中 key 对应的 string 后面追加字符串。

    语法:

    APPEND key value 
    
    • 1
    • 如果 key 不存在,其效果等同于 SET 命令;
    • 如果 APPEND 执行成功,则返回最终字符串的长度。

    2. GETRANGE

    GETRANGE命令的作用是截取key 对应的 string 中的子串。

    语法:

    GETRANGE key start end
    
    • 1
    • 截取的内容由指定的偏移量 start 和 end 确定,并且区间是左右闭合的;
    • 指定的偏移量可以是负数,当指定为负数时,表示的是倒数第几个字符,如 -1 表示倒数第一个字符;
    • 超出范围的偏移量会根据 string 的长度调整成正确的值。

    使用示例:

    3. SETRANGE

    SETRANGE 命令的作用是覆盖从指定位置开始的 key 对应 string 中的一部分。

    语法:

    SETRANGE key offset value 
    
    • 1
    • 替换的长度为替换字符串的长度,如果原字符串后面的长度不足,则将后面的全部替换;
    • 如果 key 不存在,并且指定替换的位置大于 0,则前面的位置由十六进制的 0 替换;
    • 替换成功则返回最终字符串的长度。

    4. STRLEN

    STRLEN 命令的作用是获取 key 对应的 string 的长度。

    语法:

    STRLEN key
    
    • 1
    • 其返回值为字符串的长度;
    • 如果 key 不存在,则返回 0;
    • 如果 key 对应的 value 的类型不是 string 则会报错;
    • 字符串的长度有当前编码规则所决定。

    使用示例:

    string 相关命令总结

    以下是 Redis 中与 string 类型相关的命令的总结,包括命令、作用和时间复杂度:

    命令作用时间复杂度
    SET设置key的值为指定字符串O(1)
    GET获取key对应的字符串值O(1)
    MSET批量设置多个键值对O(N)(N为键值对数量)
    MGET批量获取多个key的值O(N)(N为键的数量)
    SETNX仅当key不存在时设置值O(1)
    SETEX设置key的值和过期时间(秒)O(1)
    PSETEX设置key的值和过期时间(毫秒)O(1)
    INCR将key对应的数字值加一O(1)
    INCRBY将key对应的数字值加上指定整数O(1)
    DECR将key对应的数字值减一O(1)
    DECRBY将key对应的数字值减去指定整数O(1)
    INCRBYFLOAT将key对应的浮点数值加上指定浮点数O(1)
    APPEND在key对应的字符串值后追加字符串O(1)
    GETRANGE获取key对应的字符串的子串O(N)(N为子串长度)
    SETRANGE覆盖key对应字符串的部分内容O(N)(N为替换字符串长度)
    STRLEN获取key对应的字符串的长度O(1)

    二、String 类型的编码方式

    在Redis中,字符串(string)类型的值可以使用多种不同的编码方式存储,具体的编码方式是根据数据的内容和大小来动态选择的,以最大程度地节省内存和提高性能。以下是Redis中字符串类型的常见编码方式:

    1. RAW(简单动态字符串):这是最常见的字符串编码方式。它用于存储较短的字符串,长度不超过字符串编码结构的限制。这种编码方式不会对字符串进行压缩,因此在存储较小的字符串时效率高。

    2. INT(整数编码):当一个字符串可以被解释为整数时,Redis会将其编码为整数,以节省内存。整数编码分为以下几种子编码方式:

      • int16_t:16位整数编码,存储16位以内的整数。
      • int32_t:32位整数编码,存储32位以内的整数。
      • int64_t:64位整数编码,存储64位以内的整数。
    3. EMBSTR(嵌套字符串编码):用于存储较短的字符串,但与RAW不同的是,EMBSTR的编码方式将字符串长度也一并存储在编码结构中,以节省内存。

    4. RAW和EMBSTR共享编码:在某些情况下,Redis会使用一种特殊的编码方式,该方式可以共享RAW和EMBSTR编码方式的优点。这意味着它既可以存储较短的字符串,又可以高效地存储较大的字符串。

    5. SDS(简单动态字符串):SDS是一种用于表示字符串的数据结构,它具有动态大小,可以在不需要重新分配内存的情况下进行扩展。这种编码方式用于存储较大的字符串,以节省内存和提高性能。

    需要注意的是,Redis会根据字符串的内容和大小动态选择适当的编码方式,因此开发者无需手动指定编码方式。这种动态编码方式使得Redis能够在不同情况下充分利用内存,提高效率。

    可以使用Redis的OBJECT ENCODING命令来查看特定键的编码方式,例如:

    OBJECT ENCODING mykey
    
    • 1

    这将返回键mykey的编码方式。

    例如:

    三、String 类型的使用场景

    1. 缓存(Cache)

    由于 Redis 速度快的特点,因此常用于缓存功能。比较典型的缓存使用场景就是,Redis 作为缓冲层,MySQL 作为存储层,绝大部分请
    求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

    Redis + MySQL 组成的缓存存储架构:

    下面通过伪代码模拟了上图的业务数据的访问过程:

    1)假设业务是根据用户的 uid 获取用户信息

    UserInfo getUserInfo(long uid) {
    	...
    }
    
    • 1
    • 2
    • 3

    2)首先从 Redis 获取用户信息,我们假设用户信息保存在 “user:info:” 对应的键中

    // 根据 uid 得到 Redis 的键
    String key = "user:info:" + uid;
    // 尝试从 Redis 中获取对应的值
    String value = Redis 执⾏命令:get key;
    // 如果缓存命中(hit)
    if (value != null) {
    	// 假设用户信息按照 JSON 格式存储
    	UserInfo userInfo = JSON 反序列化(value);
    	return userInfo;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3)如果没有从 Redis 中得到用户信息或者缓存未命中(miss),则进一步从 MySQL 中获取对应的信息,随后写入缓存并返回

    // 如果缓存未命中(miss)
    if (value == null) {
    	// 从数据库中,根据 uid 获取⽤⼾信息
    	UserInfo userInfo = MySQL 执⾏ SQL:select * from user_info where uid = <uid>
    	// 如果表中没有 uid 对应的⽤⼾信息
    	if (userInfo == null) {
    		// 响应 404
    		return null;
    	}
    
    	// 将用户信息序列化成 JSON 格式
    	String value = JSON 序列化(userInfo);
    	
    	// 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒)
    	Redis 执⾏命令:set key value ex 3600
    
    	// 返回用户信息
    	return userInfo;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    通过增加缓存功能,在理想情况下,每个用户信息,一个小时期间只会有一次 MySQL 查询存在,极大地提升了查询效率,同时也降低了MySQL 的访问数。

    PS: Redis 键名的设计原则和示例

    1. 业务名前缀:将键名以业务名开头,以便将不同业务或项目的键隔离开来。这有助于确保不同的应用程序或团队之间的键不会发生冲突。
      示例:
      user_info:6379
      order_info:1234
    2. 对象名:指定键名中的对象名,以描述存储的内容是什么。这有助于清晰地了解键存储的数据类型或业务对象。
      示例:
      user_profile:6379
      product_catalog:5678
    3. 唯一标识:如果需要,可以在键名中包含唯一标识符,以便更具体地标识存储的数据。这有助于将数据细分为不同的实体。
      示例:
      user:12345:profile
      product:7890:details
    4. 属性:如果数据具有多个属性,可以在键名中包含属性名称,以进一步细化键的用途。
      示例:
      user:12345:profile:name
      product:7890:details:price
    5. 键名缩写:如果键名变得过长,可以使用缩写来减少键名的长度,但要确保缩写对团队内部是清晰可理解的。
      示例:
      u:12345:pr:n(缩写版)

    通过使用这种键名命名空间约定,可以使Redis键更有组织,易于维护和管理。它还可以帮助避免键名冲突,特别是在多个应用程序或团队使用同一个Redis实例的情况下。但请注意,键名过长可能会导致性能下降,所以需要权衡键名的可读性和性能需求。

    2. 计数(Counter)

    计数(Counter)功能是Redis中常见且有用的功能之一,它可以用来快速记录和查询某个对象的计数值。这种功能在许多应用中都非常有用,例如网站的访问计数、点赞数、评论数、播放次数等。

    例如记录视频播放次数:

    伪代码:

    // 在 Redis 中统计某视频的播放次数
    long incrVideoCounter(long vid) {
    	key = "video:" + vid;
    	long count = Redis 执⾏命令:incr key
    	return counter;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然而,在实际开发一个成熟、稳定的计数系统时,会面临许多挑战和复杂性。以下是一些可能需要应对的挑战和考虑因素:

    1. 防作弊:确保计数系统不容易被恶意操纵是至关重要的。常见的防作弊措施包括限制每个用户或IP地址的计数速率,使用验证码或令牌来验证用户行为等。
    2. 按不同维度计数:有时需要按照不同的维度进行计数,例如按时间、地理位置、用户类型等。为了实现这种灵活性,需要设计适应性强的计数系统架构。
    3. 避免单点问题:单点故障可能会导致计数系统的不可用性。为了确保高可用性,可以考虑使用Redis的主从复制或集群模式,或者使用其他分布式计数系统。
    4. 数据持久化:Redis默认将数据存储在内存中,但为了持久化数据,可以将数据定期快照到磁盘或使用持久化选项,如AOF(Append-Only File)。
    5. 性能优化:处理大量计数请求可能会对性能造成压力。需要优化Redis配置、考虑使用缓存层、分布式计数系统或负载均衡策略,以应对高负载情况。
    6. 并发控制:并发操作可能导致计数不一致。要确保计数的原子性,可以使用Redis的事务或乐观锁等技术。
    7. 监控和日志:建立监控和日志系统,以实时追踪计数系统的性能和运行状况,以及检测潜在的问题。
    8. 容量规划:考虑计数系统的容量规划,包括数据存储需求、内存和硬盘空间等,以支持未来的增长。
    9. 数据清理:定期清理不再需要的计数数据,以防止数据膨胀和内存占用过多。

    总之,开发一个真实的计数系统是一个复杂的任务,需要考虑众多因素。选择合适的技术栈、设计良好的架构、实施安全性和防作弊措施、确保高可用性以及建立监控和维护策略都是成功实现计数系统的重要步骤。这些挑战需要仔细的规划和实施,以满足特定项目的需求。

    3. 共享会话(Session)

    在一个分布式 Web 服务中,用户的会话信息通常存储在各自的服务器上,这包括用户的登录状态和其他会话相关数据。然而,由于负载均衡的需要,用户的请求会被分发到不同的服务器上,而不同服务器上的会话数据并不共享。

    这就导致了一个问题:如果用户的请求被均衡到不同的服务器上,用户在刷新页面或发送下一个请求时可能会发现自己需要重新登录,这种体验对用户来说是不可接受的

    例如下图所示的 Session 分散储存:

    为了解决这个问题,可以使用Redis将用户的 Session 信息进行集中管理。在这种模式下,只要确保 Redis 是高可用和可扩展的,不论用户被均衡到哪台 Web 服务器上,都可以集中从 Redis 中查询、更新Session信息。

    4. 手机验证码

    为了增强用户登录的安全性,许多应用会采取以下步骤:

    • 在每次用户尝试登录时,要求用户输入其手机号,并通过向其手机发送验证码来进行二次验证。这个验证码需要用户再次输入,以确保登录请求来自于用户本人。
    • 此外,为了防止滥用短信接口和提高安全性,通常会限制用户每分钟获取验证码的频率,例如,在一分钟内,同一手机号最多只能获取验证码5次。

    这种流程可以有效地降低恶意登录和滥发验证码的风险,同时保障用户的账户安全。

    短信验证码:

    此功能可以用以下伪代码说明基本实现思路:

    String 发送验证码(phoneNumber) {
        key = "shortMsg:limit:" + phoneNumber;
        // 设置过期时间为 1 分钟(60 秒)
        // 使用 NX,只在不存在 key 时才能设置成功
        bool r = Redis 执行命令:set key 1 ex 60 nx
        if (r == false) {
            // 说明之前设置过该手机号的验证码了
            long c = Redis 执行命令:incr key
            if (c > 5) {
                // 说明超过了一分钟 5 次的限制了
                // 限制发送
                return null;
            }
        }
        // 说明要么之前没有设置过手机号的验证码;要么次数没有超过 5 次
        String validationCode = 生成随机的 6 位数的验证码();
        validationKey = "validation:" + phoneNumber;
        // 验证码 5 分钟(300 秒)内有效
        Redis 执行命令:set validationKey validationCode ex 300;
        // 返回验证码,随后通过手机短信发送给用户
        return validationCode;
    }
    
    // 验证用户输入的验证码是否正确
    bool 验证验证码(phoneNumber, validationCode) {
        validationKey = "validation:" + phoneNumber;
        String value = Redis 执行命令:get validationKey;
        if (value == null) {
            // 说明没有这个手机号的验证码记录,验证失败
            return false;
        }
        if (value == validationCode) {
            return true;
        } else {
            return false;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    以上介绍了使用 Redis 的字符串数据类型在缓存、计数、会话管理和手机验证码等场景中的应用。然而,Redis 的字符串类型的适用场景远不止这些,开发人员可以根据字符串类型的特点以及提供的命令,充分发挥自己的创造力和想象力,将其应用到各种业务场景中。

  • 相关阅读:
    这些女强人,颠覆了整个世界
    【Java习作】提取汉字拼音首字母(Java版)
    如何提高webpack的构建速度?
    手持振弦采集仪VH03各种接口使用说明
    2022.06青少年软件编程(Python)等级考试试卷(五级)
    python聚类分析如何可视化?
    DPDK EAL
    reqable(小黄鸟)+雷电抓包安卓APP
    wireshark打开tcpdump抓的包 vwr: Invalid data length runs past the end of the record
    《优化接口设计的思路》系列:第二篇—接口用户上下文的设计与实现
  • 原文地址:https://blog.csdn.net/qq_61635026/article/details/132744418