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);
}
相关推荐
lwprain3 小时前
图数据库neo4j desktop2.0初探
数据库·neo4j
心态特好3 小时前
Jwt非对称加密的应用场景
java
七七七七073 小时前
【Linux 系统】打开文件和文件系统
linux·运维·spring
先鱼鲨生3 小时前
【MySQL】认识数据库以及MySQL安装
数据库·mysql
敢敢J的憨憨L4 小时前
GPTL(General Purpose Timing Library)使用教程
java·服务器·前端·c++·轻量级计时工具库
周杰伦_Jay4 小时前
【终端使用MySQL】MySQL 数据库核心操作全解析:从入门到性能优化
数据库·mysql·性能优化
刘一哥GIS4 小时前
Windows环境搭建:PostGreSQL+PostGIS安装教程
数据库·python·arcgis·postgresql·postgis
云和数据.ChenGuang4 小时前
uri: mongodb://jack:123456@localhost://27017 数据库访问其他的写法
数据库·mongodb·oracle
sg_knight4 小时前
Spring Cloud与RabbitMQ深度集成:从入门到生产级实战
java·spring boot·spring·spring cloud·消息队列·rabbitmq·stream