Redis的String详解

Redis String数据结构概述

Redis的String类型是最基本、最常用 的数据类型,可以存储任何形式的数据,包括字符串、整数、浮点数、甚至二进制数据。String类型的值最大可以存储512MB的数据。

String类型的特点

  • 二进制安全:可以存储任何二进制数据,不限于可打印字符
  • 多种编码方式:根据存储内容自动选择最优编码,节省内存
  • 丰富的操作:支持字符串操作、数值计算、位操作等
  • 原子性操作:所有操作都是原子的,适合计数器等场景

应用场景

场景 说明
缓存 存储用户会话、页面缓存、对象序列化数据
计数器 文章阅读量、用户点赞数、在线人数等
分布式锁 利用SETNX命令实现分布式锁
限流器 结合过期时间实现API限流
位图 用户签到、特征标记等

Redis String的底层实现

Redis String的底层实现主要采用三种编码方式:intembstrraw。Redis会根据存储的值自动选择最合适的编码方式。

底层数据结构

SDS(简单动态字符串)

Redis使用SDS而不是C语言原生字符串来表示字符串值:

c 复制代码
struct sdshdr {
    int len;        // 字符串已使用的长度
    int free;       // 字符串未使用的长度
    char buf[];     // 字符数组,用于保存字符串
};

SDS的优势

  • O(1)时间复杂度获取字符串长度
  • 避免缓冲区溢出
  • 减少内存重分配次数
  • 二进制安全

三种编码方式

INT编码

适用条件

  • 存储的值是整数
  • 整数值在LONG_MINLONG_MAX之间(64位系统为-263到263-1)

内存布局

复制代码
+---------------------+
|     redisObject     |
| type: String        |
| encoding: INT       |
| ptr: 12345          |  <- 整数值直接存储在ptr中
+---------------------+

示例

bash 复制代码
127.0.0.1:6379> SET counter 100
OK
127.0.0.1:6379> OBJECT ENCODING counter
"int"

EMBSTR编码

适用条件

  • 存储的值是字符串
  • 字符串长度小于等于44字节(Redis 5.0+)

内存布局

复制代码
+---------------------+---------------------+
|     redisObject     |       sdshdr        |
| type: String        | len: 5, free: 0     |
| encoding: EMBSTR    | buf: "Hello"        |
| ptr: --------------→|                     |
+---------------------+---------------------+

特点

  • RedisObject和SDS在内存中连续存储
  • 只需要一次内存分配
  • 更好的缓存局部性

示例

bash 复制代码
127.0.0.1:6379> SET short_str "Hello"
OK
127.0.0.1:6379> OBJECT ENCODING short_str
"embstr"

RAW编码

适用条件

  • 存储的值是字符串
  • 字符串长度大于44字节

内存布局

复制代码
+---------------------+     +---------------------+
|     redisObject     |     |       sdshdr        |
| type: String        |     | len: 50, free: 10   |
| encoding: RAW       |----→| buf: "Long string..."|
| ptr: --------------→|     |                     |
+---------------------+     +---------------------+

特点

  • RedisObject和SDS分开存储
  • 需要两次内存分配
  • 适用于长字符串

示例

bash 复制代码
127.0.0.1:6379> SET long_str "This is a very long string that exceeds 44 bytes in length..."
OK
127.0.0.1:6379> OBJECT ENCODING long_str
"raw"

编码转换规则

编码方式会根据操作自动转换:

bash 复制代码
# 初始为INT编码
127.0.0.1:6379> SET num 100
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"

# 追加操作后转换为RAW编码
127.0.0.1:6379> APPEND num "abc"
(integer) 6
127.0.0.1:6379> OBJECT ENCODING num
"raw"

# EMBSTR在修改时会转换为RAW
127.0.0.1:6379> SET str "hello"
OK
127.0.0.1:6379> OBJECT ENCODING str
"embstr"
127.0.0.1:6379> APPEND str " world"
(integer) 11
127.0.0.1:6379> OBJECT ENCODING str
"raw"

Redis String操作命令

基本操作命令

  • SET key value EX seconds PX milliseconds NX\|XX:设置键值对

    bash 复制代码
    127.0.0.1:6379> SET user:1:name "Tom" EX 3600
    OK
  • GET key:获取键对应的值

    bash 复制代码
    127.0.0.1:6379> GET user:1:name
    "Tom"
  • DEL key:删除键

    bash 复制代码
    127.0.0.1:6379> DEL user:1:name
    (integer) 1
  • EXISTS key:判断键是否存在

    bash 复制代码
    127.0.0.1:6379> EXISTS user:1:name
    (integer) 0

批量操作命令

  • MSET key value key value ...:批量设置键值对

    bash 复制代码
    127.0.0.1:6379> MSET user:1:name "Tom" user:1:age 25 user:1:city "Shanghai"
    OK
  • MGET key key ...:批量获取值

    bash 复制代码
    127.0.0.1:6379> MGET user:1:name user:1:age user:1:city
    1) "Tom"
    2) "25"
    3) "Shanghai"

数字操作命令

  • INCR key:将键的整数值加1

    bash 复制代码
    127.0.0.1:6379> INCR counter
    (integer) 1
  • DECR key:将键的整数值减1

    bash 复制代码
    127.0.0.1:6379> DECR counter
    (integer) 0
  • INCRBY key increment:将键的值加上整数增量

    bash 复制代码
    127.0.0.1:6379> INCRBY counter 5
    (integer) 5
  • INCRBYFLOAT key increment:将键的值加上浮点数增量

    bash 复制代码
    127.0.0.1:6379> INCRBYFLOAT price 1.5
    "6.5"

字符串操作命令

  • APPEND key value:将值追加到现有值的末尾

    bash 复制代码
    127.0.0.1:6379> APPEND greeting " World"
    (integer) 11
  • STRLEN key:获取值的长度

    bash 复制代码
    127.0.0.1:6379> STRLEN greeting
    (integer) 11
  • GETRANGE key start end:获取子字符串

    bash 复制代码
    127.0.0.1:6379> GETRANGE greeting 0 4
    "Hello"
  • SETRANGE key offset value:从偏移量开始覆盖字符串

    bash 复制代码
    127.0.0.1:6379> SETRANGE greeting 6 "Redis"
    (integer) 11

位操作命令

  • SETBIT key offset value:设置或清除位的值

    bash 复制代码
    127.0.0.1:6379> SETBIT user:1:login:2023 10 1
    (integer) 0
  • GETBIT key offset:获取位的值

    bash 复制代码
    127.0.0.1:6379> GETBIT user:1:login:2023 10
    (integer) 1
  • BITCOUNT key start end:统计值为1的位数

    bash 复制代码
    127.0.0.1:6379> BITCOUNT user:1:login:2023
    (integer) 1

高级操作命令

  • SETEX key seconds value:设置键值对并指定过期时间(秒)

    bash 复制代码
    127.0.0.1:6379> SETEX session:123 3600 "user_data"
    OK
  • PSETEX key milliseconds value:设置键值对并指定过期时间(毫秒)

    bash 复制代码
    127.0.0.1:6379> PSETEX temp:key 5000 "temporary_data"
    OK
  • SETNX key value:键不存在时才设置

    bash 复制代码
    127.0.0.1:6379> SETNX lock:resource 1
    (integer) 1
  • GETSET key value:设置新值并返回旧值

    bash 复制代码
    127.0.0.1:6379> GETSET counter 100
    "50"

Java中操作Redis String

StringRedisTemplate配置

java 复制代码
@Configuration
public class RedisConfig {
    
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        
        // 设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        return template;
    }
}

基本操作示例

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisStringService {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    // 设置值
    public void set(String key, String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }
    
    // 设置值并指定过期时间
    public void setWithExpire(String key, String value, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
    }
    
    // 获取值
    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }
    
    // 设置值(仅当键不存在时)
    public Boolean setIfAbsent(String key, String value) {
        return stringRedisTemplate.opsForValue().setIfAbsent(key, value);
    }
    
    // 删除键
    public Boolean delete(String key) {
        return stringRedisTemplate.delete(key);
    }
    
    // 判断键是否存在
    public Boolean hasKey(String key) {
        return stringRedisTemplate.hasKey(key);
    }
    
    // 设置过期时间
    public Boolean expire(String key, long timeout, TimeUnit unit) {
        return stringRedisTemplate.expire(key, timeout, unit);
    }
    
    // 获取剩余过期时间
    public Long getExpire(String key, TimeUnit unit) {
        return stringRedisTemplate.getExpire(key, unit);
    }
}

数字操作示例

java 复制代码
@Component
public class RedisNumberService {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    // 递增操作
    public Long increment(String key) {
        return stringRedisTemplate.opsForValue().increment(key);
    }
    
    // 递增指定值
    public Long incrementBy(String key, long delta) {
        return stringRedisTemplate.opsForValue().increment(key, delta);
    }
    
    // 递减操作
    public Long decrement(String key) {
        return stringRedisTemplate.opsForValue().decrement(key);
    }
    
    // 递减指定值
    public Long decrementBy(String key, long delta) {
        return stringRedisTemplate.opsForValue().decrement(key, delta);
    }
    
    // 浮点数递增
    public Double incrementByFloat(String key, double delta) {
        return stringRedisTemplate.opsForValue().increment(key, delta);
    }
}

批量操作示例

java 复制代码
@Component
public class RedisBatchService {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    // 批量设置
    public void multiSet(Map<String, String> keyValueMap) {
        stringRedisTemplate.opsForValue().multiSet(keyValueMap);
    }
    
    // 批量获取
    public List<String> multiGet(List<String> keys) {
        return stringRedisTemplate.opsForValue().multiGet(keys);
    }
    
    // 批量设置(仅当所有键都不存在时)
    public Boolean multiSetIfAbsent(Map<String, String> keyValueMap) {
        return stringRedisTemplate.opsForValue().multiSetIfAbsent(keyValueMap);
    }
}

位操作示例

java 复制代码
@Component
public class RedisBitService {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    // 设置位值
    public Boolean setBit(String key, long offset, boolean value) {
        return stringRedisTemplate.opsForValue().setBit(key, offset, value);
    }
    
    // 获取位值
    public Boolean getBit(String key, long offset) {
        return stringRedisTemplate.opsForValue().getBit(key, offset);
    }
    
    // 统计位值为1的数量
    public Long bitCount(String key) {
        return stringRedisTemplate.opsForValue().size(key) == null ? 
               0 : stringRedisTemplate.execute((RedisCallback<Long>) connection -> 
               connection.bitCount(key.getBytes()));
    }
    
    // 在指定范围内统计位值为1的数量
    public Long bitCount(String key, long start, long end) {
        return stringRedisTemplate.execute((RedisCallback<Long>) connection -> 
               connection.bitCount(key.getBytes(), start, end));
    }
}

高级操作示例

java 复制代码
@Component
public class RedisAdvancedService {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    // 获取并设置
    public String getAndSet(String key, String value) {
        return stringRedisTemplate.opsForValue().getAndSet(key, value);
    }
    
    // 获取字符串长度
    public Long size(String key) {
        return stringRedisTemplate.opsForValue().size(key);
    }
    
    // 追加字符串
    public Integer append(String key, String value) {
        return stringRedisTemplate.opsForValue().append(key, value);
    }
    
    // 获取子字符串
    public String getRange(String key, long start, long end) {
        return stringRedisTemplate.opsForValue().get(key, start, end);
    }
    
    // 设置子字符串
    public void setRange(String key, String value, long offset) {
        stringRedisTemplate.opsForValue().set(key, value, offset);
    }
}

实际应用

java 复制代码
@Service
public class UserSessionService {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    private static final String SESSION_PREFIX = "session:";
    private static final long SESSION_TIMEOUT = 30 * 60; // 30分钟
    
    // 创建用户会话
    public void createUserSession(String sessionId, String userData) {
        String key = SESSION_PREFIX + sessionId;
        stringRedisTemplate.opsForValue().set(key, userData, SESSION_TIMEOUT, TimeUnit.SECONDS);
    }
    
    // 获取用户会话
    public String getUserSession(String sessionId) {
        String key = SESSION_PREFIX + sessionId;
        return stringRedisTemplate.opsForValue().get(key);
    }
    
    // 刷新会话过期时间
    public void refreshSession(String sessionId) {
        String key = SESSION_PREFIX + sessionId;
        stringRedisTemplate.expire(key, SESSION_TIMEOUT, TimeUnit.SECONDS);
    }
    
    // 删除用户会话
    public void deleteUserSession(String sessionId) {
        String key = SESSION_PREFIX + sessionId;
        stringRedisTemplate.delete(key);
    }
}

@Service
public class ArticleService {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    private static final String VIEW_COUNT_PREFIX = "article:view:";
    
    // 增加文章阅读量
    public Long incrementArticleView(Long articleId) {
        String key = VIEW_COUNT_PREFIX + articleId;
        return stringRedisTemplate.opsForValue().increment(key);
    }
    
    // 获取文章阅读量
    public Long getArticleViewCount(Long articleId) {
        String key = VIEW_COUNT_PREFIX + articleId;
        String count = stringRedisTemplate.opsForValue().get(key);
        return count == null ? 0L : Long.parseLong(count);
    }
}

底层源码

Redis对象创建源码

c 复制代码
// redis/src/object.c

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44

// 创建String对象
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

// 创建INT编码的String对象
robj *createStringObjectFromLongLong(long long value) {
    robj *o;
    
    // 尝试使用共享的整数对象
    if (value >= 0 && value < OBJ_SHARED_INTEGERS) {
        o = shared.integers[value];
    } else {
        // 创建新的整数对象
        o = createObject(OBJ_STRING, NULL);
        o->encoding = OBJ_ENCODING_INT;
        o->ptr = (void*)((long)value);
    }
    return o;
}

// 创建EMBSTR编码的String对象
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    
    // 设置SDS属性
    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

编码转换源码

c 复制代码
// redis/src/object.c

// 检查编码并尝试转换
robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;
    
    // 确保是RAW或EMBSTR编码
    if (!sdsEncodedObject(o)) return o;
    
    // 尝试转换为INT编码
    len = sdslen(s);
    if (len <= 20 && string2l(s,len,&value)) {
        // 如果值在共享整数范围内,使用共享对象
        if ((value >= 0 && value < OBJ_SHARED_INTEGERS) && server.maxmemory == 0) {
            decrRefCount(o);
            return shared.integers[value];
        } else {
            // 转换为INT编码
            o->encoding = OBJ_ENCODING_INT;
            sdsfree(o->ptr);
            o->ptr = (void*)value;
            return o;
        }
    }
    
    // 如果字符串很小且是RAW编码,尝试转换为EMBSTR
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;
        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }
    
    // 尝试缩减SDS的未使用空间
    if (sdsavail(s) > len/10) {
        o->ptr = sdsRemoveFreeSpace(o->ptr);
    }
    
    return o;
}

SET命令处理源码

c 复制代码
// redis/src/t_string.c

void setCommand(client *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_SET_NO_FLAGS;
    
    // 解析命令参数
    for (j = 3; j < c->argc; j++) {
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
        
        if ((a[0] == 'n' || a[0] == 'N') &&
            (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
            !(flags & OBJ_SET_XX)) {
            flags |= OBJ_SET_NX;
        } else if ((a[0] == 'x' || a[0] == 'X') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_NX)) {
            flags |= OBJ_SET_XX;
        } else if (!strcasecmp(c->argv[j]->ptr,"ex") && next) {
            unit = UNIT_SECONDS;
            expire = next;
            j++;
        } else if (!strcasecmp(c->argv[j]->ptr,"px") && next) {
            unit = UNIT_MILLISECONDS;
            expire = next;
            j++;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }
    
    // 检查NX/XX条件
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,c->argv[1]) != NULL) ||
        (flags & OBJ_SET_XX && lookupKeyWrite(c->db,c->argv[1]) == NULL)) {
        addReply(c, shared.nullbulk);
        return;
    }
    
    // 设置键值对
    setKey(c->db,c->argv[1],c->argv[2]);
    server.dirty++;
    
    // 设置过期时间
    if (expire) {
        setExpire(c,c->db,c->argv[1],
            mstime()+strtoll(expire->ptr,NULL,10)*((unit == UNIT_SECONDS) ? 1000 : 1));
    }
    
    addReply(c, shared.ok);
}

GET命令处理源码

c 复制代码
// redis/src/t_string.c

void getCommand(client *c) {
    getGenericCommand(c);
}

int getGenericCommand(client *c) {
    robj *o;
    
    // 查找键
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return C_OK;
    
    // 检查类型是否为String
    if (o->type != OBJ_STRING) {
        addReply(c,shared.wrongtypeerr);
        return C_ERR;
    } else {
        addReplyBulk(c,o);
        return C_OK;
    }
}

INCR命令处理源码

c 复制代码
// redis/src/t_string.c

void incrCommand(client *c) {
    incrDecrCommand(c,1);
}

void incrDecrCommand(client *c, long long incr) {
    long long value, oldvalue;
    robj *o, *new;
    
    // 查找现有值
    o = lookupKeyWrite(c->db,c->argv[1]);
    if (o != NULL && checkType(c,o,OBJ_STRING)) return;
    if (getLongLongFromObjectOrReply(c,o,&value,NULL) != C_OK) return;
    
    // 检查溢出
    oldvalue = value;
    if ((incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) ||
        (incr > 0 && oldvalue > 0 && incr > (LLONG_MAX-oldvalue))) {
        addReplyError(c,"increment or decrement would overflow");
        return;
    }
    
    value += incr;
    
    // 如果原值是INT编码且新值也在共享整数范围内,使用共享对象
    if (o && o->refcount == 1 && o->encoding == OBJ_ENCODING_INT &&
        (value < 0 || value >= OBJ_SHARED_INTEGERS) &&
        value >= LONG_MIN && value <= LONG_MAX) {
        new = o;
        o->ptr = (void*)((long)value);
    } else {
        new = createStringObjectFromLongLong(value);
        if (o) {
            dbReplace(c->db,c->argv[1],new);
        } else {
            dbAdd(c->db,c->argv[1],new);
        }
    }
    
    signalModifiedKey(c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_STRING,"incrby",c->argv[1],c->db->id);
    server.dirty++;
    addReply(c,shared.colon);
    addReply(c,new);
    addReply(c,shared.crlf);
}
相关推荐
广州灵眸科技有限公司11 分钟前
xfce桌面触摸校准:基于灵眸科技EASY-EAl-Orin-Nano
数据库·windows·科技
大模型码小白1 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司2 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地3 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
憧憬成为web高手3 小时前
皮卡丘靶场速通--sql 2
数据库·sql·mybatis
段一凡-华北理工大学3 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
二宝哥4 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
oradh5 小时前
Oracle XTTS实现跨版本迁移和升级(Oracle 11g单库升级至19C RAC集群)
数据库·oracle·11g升级19c·xtts跨版本迁移和升级
莫逸风5 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
隔窗听雨眠6 小时前
Spring Boot在云原生时代的编程范式革新研究
spring boot·后端·云原生