Redis中key的特点

Redis中的key是二进制安全的,意味着你可以使用任何二进制序列作为一个key,从“foo”这样的字符串到JPEG文件的内容。空字符串也是一个有效的key(Redis keys are binary safe, this means that you can use any binary sequence as a key, from a string like "foo" to the content of a JPEG file. The empty string is also a valid key)。

key的其他规则:

  • 特别长的key不是一个好主意。例如,一个1024字节的key不仅在内存方面是一个坏主意,而且在数据集中查找key可能需要进行几次代价高昂的key的比较。即使现在的任务是匹配一个比较大的value是否存在,对它进行hash作为key是一个好主意,特别是从内存和带宽的角度来看。
  • 非常短的key常常也不是一个好主意。例如"u1000flw"比 "user:1000:followers"更短,但是,后者可读性更好。尽管短key明显消费更好的内存,你的工作是找到合适的平衡。
  • 尝试坚持一个模式。例如"object-type:id" 模式的例子:"user:1000"。
  • key最大不能超过521MB

前言

字符串类型是Redis中最简单的数据类型,Redis中值类型为字符串时,最大不能超过512MB。Redis的字符串类型是二进制安全的,除了普通字符串(整型,浮点型,json,xml),还可以存储图片、音视频。

常用命令

设置值

set key value [expiration EX seconds|PX milliseconds] [NX|XX]

set命令的几个选项

  • EX seconds:为键设置秒级过期时间

  • PX milliseconds:为键设置ms级过期时间

  • NX:仅当key不存在时,可以设置成功,用于新增

  • XX:仅当key存在时,可以设置成功,用于更新

NXXX参数示例:当key1不存在时,使用XX参数set key1失败,通过NX参数set key1成功,之后清空数据,再次使用NX参数,set key1成功

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> set key1 value1 XX
(nil)
127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> set key1 value2 NX
(nil)
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> set key1 value2 NX
OK
复制代码

EX参数示例:设置key1过期时间为3s,之后通过ttl key1获取key1的过期时间剩余1s,1s之后,使用keys *获取所有的key

127.0.0.1:6379> set key1 value1 EX 3
OK
127.0.0.1:6379> ttl key1
(integer) 1
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> 

复制代码

批量设置值

mset key value [key value ...]

示例:

127.0.0.1:6379> mset a 1 b 2 c 3
OK
127.0.0.1:6379> keys *
1) "a"
2) "b"
3) "c"
复制代码

设置值及其过期时间

setex key seconds value

示例:

127.0.0.1:6379> setex key1 3 value1
OK
127.0.0.1:6379> ttl key1
(integer) 1
127.0.0.1:6379> keys *
(empty list or set)
复制代码

如果key 不存在,则设置值

setnx key value

示例:

127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> setnx key1 value2
(integer) 0
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> setnx key1 value2
(integer) 1
复制代码

获取值

示例:

get key

127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> get key1
"value1"
复制代码

批量获取值

mget key [key ...]

示例:

127.0.0.1:6379> mset a 1 b 2 c 3
OK
127.0.0.1:6379> mget a b c d
1) "1"
2) "2"
3) "3"
4) (nil)
复制代码

计数操作(原子命令)

自增操作

incr key

示例:当key不存在时,默认从0递增,操作成功,返回最新的值

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> incr key1
(integer) 1
127.0.0.1:6379> incr key1
(integer) 2
127.0.0.1:6379> incr key1
(integer) 3
复制代码

incrby key increment

示例:当key不存在时,默认key的值为0,操作成功,返回最新的值

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> incrby key1 3
(integer) 3
127.0.0.1:6379> incrby key1 3
(integer) 6
127.0.0.1:6379> incrby key1 4
(integer) 10
复制代码

自减操作

decr key

decrby key increment

浮点类型自增操作

incrbyfloat key increment

bit操作

BITOP operation destkey key [key ...]

operation 可以是 AND 、 OR 、 NOT 、 XOR 四种操作中的任意一种

BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑与,并将结果保存到 destkey

BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey

BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey

BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey

AND 示例:a的ascii码为97(0110 0001),b的ascii码为98(0110 0010),所以求与的结果是0110 0000,即ascii码为96的字符("`")

127.0.0.1:6379> mset key1 a key2 b
OK
127.0.0.1:6379> BITOP AND key1 key1 key2
(integer) 1
127.0.0.1:6379> get key1
"`"
复制代码

OR 示例:a|b=c

127.0.0.1:6379> mset key1 a key2 b
OK
127.0.0.1:6379> BITOP OR key1 key1 key2
(integer) 1
127.0.0.1:6379> get key1
"c"
复制代码

XOR 示例:a^b=\x03 异或的规则是相同等于0,不同等于1,0110 0001^0110 0010=\x03

127.0.0.1:6379> mset key1 a key2 b
OK
127.0.0.1:6379> BITOP XOR key1 key1 key2
(integer) 1
127.0.0.1:6379> get key1
"\x03"
复制代码

NOT 示例:a按位取反,结果是\x9e

127.0.0.1:6379> set key1 a
OK
127.0.0.1:6379> BITOP NOT key1 key1
(integer) 1
127.0.0.1:6379> get key1
"\x9e"
复制代码

SETBIT操作

setbit key offset value

示例:将a的从左到右第6位设置为1,第7位设置为0,则a -> b

127.0.0.1:6379> set key1 a
OK
127.0.0.1:6379> setbit key1 6 1
(integer) 0
127.0.0.1:6379> setbit key1 7 0
(integer) 1
127.0.0.1:6379> get key1
"b"
复制代码

GETBIT操作

getbit key offset

示例:

127.0.0.1:6379> set key1 a
OK
127.0.0.1:6379> getbit key1 6
(integer) 0
127.0.0.1:6379> getbit key1 7
(integer) 1
复制代码

BITCOUNT操作

含义:计算给定字符串中,被设置为 1 的比特位的数量(索引的单位为byte)

bitcount key [start end]

示例:

127.0.0.1:6379>  set key1 ab
OK
127.0.0.1:6379> bitcount key1 0 1    #a和b中bit位为1的数量
(integer) 6
127.0.0.1:6379> bitcount key1 0 0    #a中bit位为1的数量
(integer) 3
127.0.0.1:6379> bitcount key1 0 -1   #end为-1表示查找到最后一个字节
(integer) 6
复制代码

不常用命令

追加命令

append key value

示例:

127.0.0.1:6379> set key1 a
OK
127.0.0.1:6379> append key1 2
(integer) 2
127.0.0.1:6379> get key1
"a2"
复制代码

字符串长度

strlen key

示例:

127.0.0.1:6379> set key1 abc
OK
127.0.0.1:6379> strlen key1
(integer) 3
复制代码

获取指定范围的字符

getrange key start end

示例:

127.0.0.1:6379> set key1 abcdefg
OK
127.0.0.1:6379> getrange key1 2 4
"cde"
复制代码

设置指定范围的字符

setrange key offset value

示例:

127.0.0.1:6379> setrange key1 2 123
(integer) 7
127.0.0.1:6379> get key1
"ab123fg"
复制代码

设置新值并返回旧值

getset key value

示例:

127.0.0.1:6379> set key1 abc
OK
127.0.0.1:6379> getset key1 123
"abc"
复制代码

存储实现

Redis是KV数据库,通过hashtable实现,每个键值对都有一个dictEntry(dict.h)

typedef struct dictEntry {
    void *key;   //key定义
    union {
        void *val;  //value定义
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;  //下一个键值对节点
} dictEntry;
复制代码

redis中的字符串类型数据存储没有使用C语言中的字符串,而是存储在自定义的SDS(Simple Dynamic String)中。 redis中常用的5种数据类型的值都保存在redisObject中,其中的*ptr指向实际的数据结构,如果值时String,则指向的就是SDS

redisObject的定义在server.h中

typedef struct redisObject {
    unsigned type:4;   //对象的类型, 包括: OBJ_STRING、 OBJ_LIST、 OBJ_HASH、 OBJ_SET、 OBJ_ZSET
    unsigned encoding:4;  //具体的数据结构
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;  //引用计数
    void *ptr;    //指向对象实际数据结构
} robj;
复制代码

内部编码

字符串类型的内部编码有3种

  1. int,存储8个字节的长整型(long,2^63-1)
  2. embstr,存储小于44个字节的字符串(3.2 版本之前是 39 字节)
  3. raw,存储大于44个字节的字符串(3.2 版本之前是 39 字节)

其中,embstrraw都是使用SDS存储

字符串的实现

SDS是redis中字符串的实现,在3.2之后的版本,SDS有多种结构(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */  //字符串长度,buf已用长度
    uint8_t alloc; /* excluding the header and null terminator */  //为buf分配的总长度,剩余长度=alloc-len
    unsigned char flags; /* 3 lsb of type, 5 unused bits */  //低3位表示类型标志
    char buf[];  //保存具体的字符串
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
复制代码

为什么使用SDS实现字符串?

C语言只能使用字符数组实现字符串

  1. 使用字符数组必须分配足够的空间,否则会内存溢出
  2. 如果要获取字符长度,必须遍历字符数组,时间复杂度是O(n)
  3. 对字符串的N次修改(导致字符串长度变化)操作必须重新分配N次内存空间
  4. 读取字符时,读取到第一个\0标记字符串结束,因此不能存储图片、视频等二进制文件,二进制不安全

SDS的特点:

  1. 不用担心内存溢出,SDS会自动扩容
  2. 获取字符串长度时间复杂度为O(1),因为保存了len属性
  3. 减少修改字符串长度时所需的内存重分配次数,通过“空间预分配(sdsMakeRoomFor)”和“惰性空间释放”,防止多次重分配内存
  4. 通过len属性判断是否结束,二进制安全

embstr和raw的区别?

  1. embstr的使用只分配1次内存空间(因为redisObject和SDS是连续的),而raw需要分配2次内存空间(分别为redisObject和SDS分配空间)
  2. embstr创建时分配1次内存空间,所以删除时也比raw少释放一次内存空间,embstr执行查找更方便
  3. embstr是只读的,如果字符串长度增加需要分配内存时,redisObject和SDS都需要重新分配内存

int、embstr和raw在什么情况下转换?

  • 当int数据不再是整型,或int值超过long的范围(-2^63 ~ 2^63-1)时,自动转化为embstr

示例1:值超过int范围

127.0.0.1:6379> set key1 9223372036854775807
OK
127.0.0.1:6379> object encoding key
"int"
127.0.0.1:6379> set key1 9223372036854775808
OK
127.0.0.1:6379> object encoding key1
"embstr"
复制代码

示例2: int数据不再是整型

127.0.0.1:6379> set key1 123
OK
127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> set key1 abc
OK
127.0.0.1:6379> object encoding key1
"embstr"
复制代码
  • 当对int或embstr执行追加操作时,自动转化为raw

示例3:

127.0.0.1:6379> set key1 123
OK
127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> append key1 a
(integer) 4
127.0.0.1:6379> object encoding key1
"raw"
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> set key1 abc
OK
127.0.0.1:6379> object encoding key1
"embstr"
127.0.0.1:6379> append key1 d
(integer) 4
127.0.0.1:6379> object encoding key1
"raw"
复制代码

注意:编码转换在redis写入数据时完成,且转换过程不可逆,只能从小编码转化为大编码(不包括重新set)

应用

Bit操作应用

  • 统计活跃用户
  • 用户在线状态
  • 用户签到

Bitmap的优势

  1. 节约空间
  2. 位操作,运算速度快
  3. 更新和查询时间复杂度为O(1)
  4. 方便扩容

以用户签到为例:

如果使用int类型记录用户是否签到,则每个用户需要占用4个字节,如果是1亿用户量,每天占用的内存就是100000000*4/1024/1024=381MB,

如果使用bitmap来记录,每个用户占用1bit,8个用户占用一个字节内存,与一个用户占用4个字节的int相比,内存占用比是1:32,每天占用内存就是100000000/8/1024/1024=11.9MB;

如果要统计本周每天都有签到的用户,使用BITOP AND destkey key [key ...]命令(BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ... "day_7_online_users)

bitmap的最大offset等于2^32-1(512MB)。在一台2010MacBookPro上,offset为2^32-1(分配512MB)需要~300ms,offset为2^30-1(分配128MB)需要~80ms,offset为2^28-1(分配32MB)需要~30ms,offset为2^26-1(分配8MB)需要8ms。

INCR 操作应用

  • 用于生成全局ID,秒杀场景限流
  • 统计文章阅读量,微博点赞数