Redis String数据结构概述
Redis的String类型是最基本、最常用 的数据类型,可以存储任何形式的数据,包括字符串、整数、浮点数、甚至二进制数据。String类型的值最大可以存储512MB的数据。
String类型的特点
- 二进制安全:可以存储任何二进制数据,不限于可打印字符
- 多种编码方式:根据存储内容自动选择最优编码,节省内存
- 丰富的操作:支持字符串操作、数值计算、位操作等
- 原子性操作:所有操作都是原子的,适合计数器等场景
应用场景
场景 | 说明 |
---|---|
缓存 | 存储用户会话、页面缓存、对象序列化数据 |
计数器 | 文章阅读量、用户点赞数、在线人数等 |
分布式锁 | 利用SETNX命令实现分布式锁 |
限流器 | 结合过期时间实现API限流 |
位图 | 用户签到、特征标记等 |
Redis String的底层实现
Redis String的底层实现主要采用三种编码方式:int 、embstr 、raw。Redis会根据存储的值自动选择最合适的编码方式。
底层数据结构
SDS(简单动态字符串)
Redis使用SDS而不是C语言原生字符串来表示字符串值:
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中
+---------------------+
示例:
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]:设置键值对
bash127.0.0.1:6379> SET user:1:name "Tom" EX 3600 OK
-
GET key:获取键对应的值
bash127.0.0.1:6379> GET user:1:name "Tom"
-
DEL key:删除键
bash127.0.0.1:6379> DEL user:1:name (integer) 1
-
EXISTS key:判断键是否存在
bash127.0.0.1:6379> EXISTS user:1:name (integer) 0
批量操作命令
-
MSET key value [key value ...]:批量设置键值对
bash127.0.0.1:6379> MSET user:1:name "Tom" user:1:age 25 user:1:city "Shanghai" OK
-
MGET key [key ...]:批量获取值
bash127.0.0.1:6379> MGET user:1:name user:1:age user:1:city 1) "Tom" 2) "25" 3) "Shanghai"
数字操作命令
-
INCR key:将键的整数值加1
bash127.0.0.1:6379> INCR counter (integer) 1
-
DECR key:将键的整数值减1
bash127.0.0.1:6379> DECR counter (integer) 0
-
INCRBY key increment:将键的值加上整数增量
bash127.0.0.1:6379> INCRBY counter 5 (integer) 5
-
INCRBYFLOAT key increment:将键的值加上浮点数增量
bash127.0.0.1:6379> INCRBYFLOAT price 1.5 "6.5"
字符串操作命令
-
APPEND key value:将值追加到现有值的末尾
bash127.0.0.1:6379> APPEND greeting " World" (integer) 11
-
STRLEN key:获取值的长度
bash127.0.0.1:6379> STRLEN greeting (integer) 11
-
GETRANGE key start end:获取子字符串
bash127.0.0.1:6379> GETRANGE greeting 0 4 "Hello"
-
SETRANGE key offset value:从偏移量开始覆盖字符串
bash127.0.0.1:6379> SETRANGE greeting 6 "Redis" (integer) 11
位操作命令
-
SETBIT key offset value:设置或清除位的值
bash127.0.0.1:6379> SETBIT user:1:login:2023 10 1 (integer) 0
-
GETBIT key offset:获取位的值
bash127.0.0.1:6379> GETBIT user:1:login:2023 10 (integer) 1
-
BITCOUNT key [start end]:统计值为1的位数
bash127.0.0.1:6379> BITCOUNT user:1:login:2023 (integer) 1
高级操作命令
-
SETEX key seconds value:设置键值对并指定过期时间(秒)
bash127.0.0.1:6379> SETEX session:123 3600 "user_data" OK
-
PSETEX key milliseconds value:设置键值对并指定过期时间(毫秒)
bash127.0.0.1:6379> PSETEX temp:key 5000 "temporary_data" OK
-
SETNX key value:键不存在时才设置
bash127.0.0.1:6379> SETNX lock:resource 1 (integer) 1
-
GETSET key value:设置新值并返回旧值
bash127.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);
}