2
头图

一、Redis 简介

Redis 是(key-value)的 NoSQL 数据库,所有的 key 都是 String ,它的 value 可以是 String、hash、list、set、zset(有序集合)、Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等数据类型,这些类型都支持 push/pop、add/remove 及取交集和差集。而且这些操作都是原子性的。

Redis 的数据是缓存在内存中,但是 Redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中。在此基础上实现了 master-slave (主从)同步

主从复制

Redis 提供复制功能,能实现多个相同数据的 Redis 副本,复制功能是分布式 Redis 的基础

删除数据命令

# 删除指定的 key 数据
del key
# 根据 value 选择非阻塞删除,也就是现在是将 keys 从 keyspace 元数据中删除,真正的删除会在后续异步操作
unlink key 

二、Redis 的数据类型

2.1 String

redis 中最基本的数据结构,所有的 key 都是 String 。String 类型的 Value 可以是 String、数字、jpg图片或者序列化的对象(值不能超过 512MB)

常见命令

  1. set key value [ex seconds][px milliseconds][nx|xx]: 设置给定键和值

  2. get key : 获取值
  3. del key :删除存储在给定键中的值
  4. incr key : 将 key 对应的值加1
  5. decr key : 将 key 对应的值减1
  6. incrby key amount: 将key 对应的值加上整数
  7. decrby key amount:将key 对应的值减去整数

应用场景

  • 缓存:可以将常见的字符串、图片等信息缓存在 redis,mysql 作为持久化层。降低 mysql 的读写压力

  • 计数器: 实现快速计数、查询缓存,同时数据可以异步落地到其他数据源。
  • 共享Session:分布式服务器将用户的 Session 进行集中的管理,每次用户更新或者查询登录信息都直接从 Redis 中集中获取。

2.2 Hash

哈希类型指的是 value 本身又是一个键值对结构,比如 value = {{field1, value1}, ... {fieldN, valueN}}。

常见命令

  1. hset hash-key sub-key1 value1 :添加键值对
  2. hget hash-key key1 : 获取制定散列键的值
  3. hgetall hash-key :获取哈希中包含的所有键值对
  4. hdel hash-key sub-key1: 在哈希中移除这个键

应用场景

  • 缓存:能够更加直观,相比 String 来说更加节省空间。比如缓存用户信息

2.3 List

Redis 中的 List 采用双端链表来实现,可以用来存储多个有序的字符创,列表最多可以存储 2^32 - 1 个元素(element)。可以对列表两端插入(push)和弹出(pop),还可以获取制定范围的元素列表,获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色。

常见命令

rpush, lpush 分别是右边和左边插入,linsert 命令会从列表中找到等于某个值的元素,在其前或者后插入新的元素。可以转换成其他的数据结构:

  • lpush+lpop=Stack(栈)
  • lpush+rpop=Queue(队列)
  • lpush+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息队列)

应用场景

  • 消息队列lpush + brpop组合可以实现阻塞队列,生产者使用 lpush 从左侧插入元素,多个消费者使用 brpop 阻塞式抢列表尾部的元素。保证消费的负载均衡和高可用性

2.4 set

set 类型是用来保存多个字符串元素,但是 set 中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。它的底层是通过哈希表来实现的,因此添加、删除、查找的复杂度都是 O(1)

常见命令

  1. sadd key value : 向集合中添加一个或者多个成员
  2. scard key : 获取集合中的成员数
  3. smember key member : 返回集合中的所有成员
  4. sismember key member : 判断 member 元素是否是集合 key 的成员

应用场景

  • 标签:给用户添加标签,所有这样有同一标签或者类似的可以推荐关注的事情或者关注的人

2.5 zset

有序集合 zset 相对于 set 而言,其内部的元素可以进行排序,它是通过给每个元素设置一个分数来作为排序的依据。

常见命令

  1. zadd zset-key int member1 : 将一个带有给定分值的成员添加到有序的集合中
  2. zrange zset-key 0-1 : 根据元素在有序集合中所处的位置,从有序集合中获取对应的元素
  3. zrem zset-key member1 : 如果给定元素存在于有序集合中,就移除该元素

应用场景

  • 排行榜:榜单可以按照用户关注数,更新时间等打分,并做排行

2.6 HyperLogLogs

HyperLogLog并不是一种新的数据结构(实际类型为字符串类 型),而是一种基数算法,通过HyperLogLog可以利用极小的内存空间 完成独立总数的统计,比如注册IP u数,每日访问IP 数等等。它是一个基于基数估算的算法,只能比较准确的估算出基数,可以使用少量固定的内存去存储并识别集合中的唯一元素。而且这个估算的技术并不一定准确,它是一个带有0.81%标准错误的近似值(对于一些可以接受容错的业务场景可以忽略不计)

例如2016-03-06的访问用户是 uuid-1、uuid-2、uuid-3、uuid-4,2016-03-05的访问用户是uuid-4、uuid-5、uuid-6、uuid-7,如图所示。

常用命令

  1. pfadd : 用于在基数统计中添加元素,添加成功会返回1
  2. pfcount:用于计算一个或者多个 HyperLogLogs 的独立总数
  3. pfmerge:求出多个HyperLogLogs 的并集并赋值给 destkey

应用场景

  • IP 数 : 用于统计某个时段的 IP 或者用户数

2.7 Bitmaps

它本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作

Bitmaps 相当于一个以位为单位的数组,数组的每个单元只能存储0 和 1 , 数组的下标在 Bitmaps 中叫做偏移量。

常用命令

  1. setbit key offset value : 设置键和偏移量的值
  2. getbit key offset: 获取键的第 offset 位的值
  3. bitcount key : 统计该键的次数值

应用场景

  • 活跃用户分析: 存储和统计一天中活跃的用户

2.8 Geo

Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位 置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能

常用命令

  1. geoadd: 添加地理位置信息

  2. geopos: 获取地理位置信息

  3. geodist: 获取两个地理位置的距离

  4. georadius: 获取范围内的信息位置集合-->附近的人

  5. geohash: 将二维经纬度转换为一维的字符串

  6. zrem: 删除地理位置信息(实际上是利用 zset 中的命令实现对位置信息的删除)

应用场景

  • 附近的人
  • 推算两地之间的距离

三、Redis 的数据结构

为什么 Redis 会设计 RedisObject 对象,因为操作数据类型的命令除了要对键的类型进行检查以外,还需要根据数据类型的不同编码进行多态处理,所以 Redis 构建了自己的类型系统,主要有:

  • redisObject 的对象机制
  • redisObject 对象的类型检查和多态
  • 对 redisObject 进行分配、共享和销毁的机制

3.1 redisObject 的对象机制

/*
* Redis对象
*/
typedef struct redisObject {
    //类型
    unsigned type:4;
    
    //编码方式
    unsigned encoding:4;
    
    //LRU 记录最后一次访问时间
    unsigned lru:LRU_BITS;
    
    //引用计数
    int refcount;
    
    //指向底层数据结构实例
    void *ptr
    
} robj;

type属性

记录了对象所保存的值类型,也就是常用的五个数据类型

/*
* 对象类型
*/
#define OBJ_STRING 0 // 字符串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集
#define OBJ_HASH 4 // 哈希表

encoding 属性

记录了对象所保存的值的编码,表示数据类型对应的编码类型

/*
* 对象编码
*/
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* 注意:版本2.6后不再使用. */
#define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,旧版本2.x中String的底层之一. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

* ptr 指针

它指向实际保存值的数据结构,而数据结构类型是由前面的 encoding 和 type 两个属性来决定。如下图,数据类型和编码类型决定指向实际的数据结构。

lru 属性

记录的是对象最后一次被命令程序访问的时间,那么如何实现对对象的回收,这里引入一个概念:空转时长

空转时长,也就是当前系统时间减去 键的值对象的 LRU 时间。如果服务器用于回收内存的算法是 Volatile-lru 或者 allkeys-lru。那么当服务器占用的内存树超过了 maxmemory 选项所设置的上限值时,空转时长较高的那部分键会优先被服务器所释放。

refcount 属性

用于计数,对指向这个对象的引用计数。

比如创建了一个值为 100 的 key A ,使用 OBJECT REFCOUNT 命令查看 key A 的值对象的引用计数 refcount ,发现引用计数为 2,说明这个值对象被两个程序所引用,两个程序共享了这个值对象的 key

那么当对象的 refcount 值为 0 时,这个对象将会被内存回收释放,这也是对象的销毁机制。(对应 JVM 里面的引用计数法标记)

3.2 redis 命令的类型检查和多态

redis 当执行一个处理数据类型命令时,比如 LPOP key 命令redis 执行的步骤:

  1. 根据给定的 key,在数据库字典中查找对应的 redisObject 对象,没找到就返回null
  2. 检查找到的 redisObject 的 type 属性和执行命令所需要的类型是否相同,如果不相同就返回类型错误
  3. 根据 redisObject 的 encoding 属性所指定的编码,选择合适的操作函数来处理底层的数据结构
  4. 最后返回命令的操作结构

3.3 redisObject 对象共享和销毁

共享对象的出现是为了避免重复分配的麻烦。通过 refcount 来表示对象所引用的次数。比如创键一个 值为 100 的 key A,然后再创建一个值为 100 的 key B ,这个时候共享对象的引用计数值变为了 3

redis> SET A 100
OK
redis> SET B 100
OK
redis> OBJECT REFCOUNT A
(integer) 3

此外共享对象不单单只有字符串键可以使用, 那些在数据结构中嵌套了字符串对象的对象(linkedlist 编码的列表对象、 hashtable 编码的哈希对象、 hashtable 编码的集合对象、以及 zset 编码的有序集合对象)都可以使用这些共享对象。

为什么redis 不共享包含value 为字符串的对象?

当服务器考虑将一个共享对象设置为键的值对象时, 程序需要先检查给定的共享对象和键想创建的目标对象是否完全相同, 只有在共享对象和目标对象完全相同的情况下, 程序才会将共享对象用作键的值对象, 而一个共享对象保存的值越复杂, 验证共享对象和目标对象是否相同所需的复杂度就会越高, 消耗的 CPU 时间也会越多:

  • 如果共享对象是保存整数值的字符串对象, 那么验证操作的复杂度为 O(1) ;
  • 如果共享对象是保存字符串值的字符串对象, 那么验证操作的复杂度为 O(N) ;
  • 如果共享对象是包含了多个值(或者对象的)对象, 比如列表对象或者哈希对象, 那么验证操作的复杂度将会是 O(N^2) 。

因此, 尽管共享更复杂的对象可以节约更多的内存, 但受到 CPU 时间的限制, Redis 只对包含整数值的字符串对象进行共享。

引用计数及对象的销毁

前面谈到过,redisObject 中带有一个 refcount 属性,表示这个对象被引用了多少次。

  • 当对象被新程序共享时,其 refcount 值加1;
  • 当使用完一个对象后或者消除一个对象的引用后,程序将对象的 refcount 值减1
  • 当对象的 refcount 降为0 时,这个 redisObject 结构以及它所引用的数据结构的内存都会被释放

四、参考资料


归思君
1.2k 声望209 粉丝

阿里云社区专家博主,华为云社区云享专家,一个会点前端的java工程师。