Redis的String详解
Redis String数据结构概述
Redis的String类型是最基本、最常用的数据类型,可以存储任何形式的数据,包括字符串、整数、浮点数、甚至二进制数据。String类型的值最大可以存储512MB的数据。
String类型的特点
- 二进制安全:可以存储任何二进制数据,不限于可打印字符
- 多种编码方式:根据存储内容自动选择最优编码,节省内存
- 丰富的操作:支持字符串操作、数值计算、位操作等
- 原子性操作:所有操作都是原子的,适合计数器等场景
应用场景
场景 | 说明 |
---|---|
缓存 | 存储用户会话、页面缓存、对象序列化数据 |
计数器 | 文章阅读量、用户点赞数、在线人数等 |
分布式锁 | 利用SETNX命令实现分布式锁 |
限流器 | 结合过期时间实现API限流 |
位图 | 用户签到、特征标记等 |
Redis String的底层实现
Redis String的底层实现主要采用三种编码方式:int、embstr、raw。Redis会根据存储的值自动选择最合适的编码方式。
底层数据结构
SDS(简单动态字符串)
Redis使用SDS而不是C语言原生字符串来表示字符串值:
struct sdshdr {int len; // 字符串已使用的长度int free; // 字符串未使用的长度char buf[]; // 字符数组,用于保存字符串
};
SDS的优势:
- O(1)时间复杂度获取字符串长度
- 避免缓冲区溢出
- 减少内存重分配次数
- 二进制安全
三种编码方式
INT编码
适用条件:
- 存储的值是整数
- 整数值在
LONG_MIN
到LONG_MAX
之间(64位系统为-263到263-1)
内存布局:
+---------------------+
| redisObject |
| type: String |
| encoding: INT |
| ptr: 12345 | <- 整数值直接存储在ptr中
+---------------------+
示例:
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在内存中连续存储
- 只需要一次内存分配
- 更好的缓存局部性
示例:
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分开存储
- 需要两次内存分配
- 适用于长字符串
示例:
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"
编码转换规则
编码方式会根据操作自动转换:
# 初始为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]:设置键值对
127.0.0.1:6379> SET user:1:name "Tom" EX 3600 OK
-
GET key:获取键对应的值
127.0.0.1:6379> GET user:1:name "Tom"
-
DEL key:删除键
127.0.0.1:6379> DEL user:1:name (integer) 1
-
EXISTS key:判断键是否存在
127.0.0.1:6379> EXISTS user:1:name (integer) 0
批量操作命令
-
MSET key value [key value …]:批量设置键值对
127.0.0.1:6379> MSET user:1:name "Tom" user:1:age 25 user:1:city "Shanghai" OK
-
MGET key [key …]:批量获取值
127.0.0.1:6379> MGET user:1:name user:1:age user:1:city 1) "Tom" 2) "25" 3) "Shanghai"
数字操作命令
-
INCR key:将键的整数值加1
127.0.0.1:6379> INCR counter (integer) 1
-
DECR key:将键的整数值减1
127.0.0.1:6379> DECR counter (integer) 0
-
INCRBY key increment:将键的值加上整数增量
127.0.0.1:6379> INCRBY counter 5 (integer) 5
-
INCRBYFLOAT key increment:将键的值加上浮点数增量
127.0.0.1:6379> INCRBYFLOAT price 1.5 "6.5"
字符串操作命令
-
APPEND key value:将值追加到现有值的末尾
127.0.0.1:6379> APPEND greeting " World" (integer) 11
-
STRLEN key:获取值的长度
127.0.0.1:6379> STRLEN greeting (integer) 11
-
GETRANGE key start end:获取子字符串
127.0.0.1:6379> GETRANGE greeting 0 4 "Hello"
-
SETRANGE key offset value:从偏移量开始覆盖字符串
127.0.0.1:6379> SETRANGE greeting 6 "Redis" (integer) 11
位操作命令
-
SETBIT key offset value:设置或清除位的值
127.0.0.1:6379> SETBIT user:1:login:2023 10 1 (integer) 0
-
GETBIT key offset:获取位的值
127.0.0.1:6379> GETBIT user:1:login:2023 10 (integer) 1
-
BITCOUNT key [start end]:统计值为1的位数
127.0.0.1:6379> BITCOUNT user:1:login:2023 (integer) 1
高级操作命令
-
SETEX key seconds value:设置键值对并指定过期时间(秒)
127.0.0.1:6379> SETEX session:123 3600 "user_data" OK
-
PSETEX key milliseconds value:设置键值对并指定过期时间(毫秒)
127.0.0.1:6379> PSETEX temp:key 5000 "temporary_data" OK
-
SETNX key value:键不存在时才设置
127.0.0.1:6379> SETNX lock:resource 1 (integer) 1
-
GETSET key value:设置新值并返回旧值
127.0.0.1:6379> GETSET counter 100 "50"
Java中操作Redis String
StringRedisTemplate配置
@Configuration
public class RedisConfig {@Beanpublic 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;}
}
基本操作示例
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 {@Autowiredprivate 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);}
}
数字操作示例
@Component
public class RedisNumberService {@Autowiredprivate 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);}
}
批量操作示例
@Component
public class RedisBatchService {@Autowiredprivate 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);}
}
位操作示例
@Component
public class RedisBitService {@Autowiredprivate 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));}
}
高级操作示例
@Component
public class RedisAdvancedService {@Autowiredprivate 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);}
}
实际应用
@Service
public class UserSessionService {@Autowiredprivate 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 {@Autowiredprivate 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对象创建源码
// 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);elsereturn 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;
}
编码转换源码
// 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编码,尝试转换为EMBSTRif (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命令处理源码
// redis/src/t_string.cvoid 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命令处理源码
// redis/src/t_string.cvoid getCommand(client *c) {getGenericCommand(c);
}int getGenericCommand(client *c) {robj *o;// 查找键if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)return C_OK;// 检查类型是否为Stringif (o->type != OBJ_STRING) {addReply(c,shared.wrongtypeerr);return C_ERR;} else {addReplyBulk(c,o);return C_OK;}
}
INCR命令处理源码
// redis/src/t_string.cvoid 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);
}