01_一个键值数据库包含什么
从基础入手,知道 Redis
里面可以存怎么样的数据,对数据可以做怎么样的操作
(也就是数据模型和操作接口)
这些看似简单,却是 Redis
缓存、秒杀、分布式锁等场景的重要基础
可以存哪些数据?
对于键值数据库而言,基本的数据模型是 K-V
模型
Key 可以是 String 类型,而 Value 是 String、整型等基本数据类型或其他复杂类型
不同键值数据库支持的 Key 类型一般差别不大,而 Value 类型则有较大差别
例如,Memcached
支持的 Value 类型仅为 String 类型,而 Redis
支持的 Value 包括了 String、哈希表、列表、集合等
从使用的角度来说,不同 Value 类型的实现,不仅可以支撑不同业务的数据需求,而且也隐含着不同数据结构在性能、空间效率等方面的差异,从而导致不同的 Value 操作之间存在着差异
可以对数据做什么操作?
基本操作无外乎增删改查
但是在实际执行时,会根据 Key 是否存在而执行相应的新写或更新流程,所以增改可能是一条命令
在实际业务场景中,可能还会有查询一个用户在一段时间内的访问记录(即 SCAN 操作,根据一段 Key 的范围返回相应的 Value 值)
还可能会在黑白名单应用中,需要判断某个用户是否存在(即 EXISTS 操作,用于判断某个 Key 是否存在)
采用什么访问模式?
大体来说,一个键值数据库包括了访问框架、索引模块、操作模块和存储模块四部分
访问模式通常有两种
- 通过函数库调用的方式供外部应用使用
- 通过网络框架以 Socket 通信的形式对外提供键值对操作,这种形式可以提供广泛的键值存储服务(网络框架中包括 Socket Server 和协议解析)
Redis
采用的就是通过网络框架访问
通过网络框架提供键值存储服务,一方面扩大了数据库的受用面,但是也给性能、运行模型提供了不同的设计选择,带来一些潜在的问题
举个例子,当客户端发送一个如下的命令后,该命令会被封装在网络包中发送到键值数据库PUT hello world
键值数据库网络框架接收到网络包,并按照相应的协议进行解析之后,就可以知道,客户端想写入一个键值对,并开始实际的写入流程
此时,我们会遇到一个系统设计上的问题,简单来说,就是网络连接的处理、网络请求的解析,以及数据存取的处理,是用一个线程、多个线程,还是多个进程来交互处理呢?该如何进行设计和取舍呢(即 I/O 模型设计)
举个例子,一个线程既要处理网络连接、解析请求,又要完成数据存取,一旦某一步操作发生阻塞,整个线程就会阻塞住,这就减低了系统响应速度
如何定位键值对的位置?
当数据库解析了客户端发来的请求,知道了要进行的键值对操作,此时,数据库需要查找所要操作的键值对是否存在,这依赖于键值数据库的索引模块
索引的作用是让兼职数据库根据 Key 找到相应 Value 的存储位置,进而执行操作
索引的类型有很多,常见的有哈希表、B+树、字典树等。不同的索引结构在性能、空间消耗、并发控制等方面具有不同的特征
像 Redis
就是采用哈希表作为 K-V
索引,很大一部分原因在于,其键值数据基本是保存在内存中的,而内存的高性能随机访问特性可以很好地与哈希表 O(1)
的操作复杂度相匹配
不同操作的具体逻辑是怎么样的?
- 对于 GET/SCAN 操作而言,根据 value 的存储位置返回 value 即可
- 对于 PUT 一个新的键值对数据而言,数据库需要为该键值对分配内存空间
- 对于 DELETE 操作,数据库需要删除键值对并释放相应的内存空间(这个过程由分配器完成)
如何实现重启后快速提供服务?
键值数据库虽然依赖于内存保存数据,但如果需要重启后快速重新提供服务,则需要增加持久化功能,将键值数据通过调用本地文件系统的操作接口保存在磁盘上
但是,在什么时机保存这份数据呢?
- 一种方式是,对于每一个键值对,数据库都将其落盘保存到文件中,数据更加可靠,但是会造成很大的性能问题
- 另一种方式则是周期性地把数据保存到文件中,这样可以避免频繁写盘操作的性能影响。但是潜在代价就是数据有丢失的风险