根据我的从业经验, 用户对一个(分布式)数据库系统, 最看重这些特性:
- 多副本, 数据安全性
- 能提供强一致性更好, 但基于性能考虑, 最终一致性(异步复制)有广泛的用户需求
- 节点容灾, 部分服务器故障, 不影响整体系统的运行
- 方案选择, 倾向于增加无状态 proxy 层
- "伪装"成一个单机数据库实例, 底层细节对用户隐藏
- 平滑扩容, 加机器不影响业务, 不需要业务在代码和架构上做改变
- 数据全球同步, 最终一致
- 一个大区的日志序列是顺序 apply, 但多个大区的日志序列是乱序 apply
- TTL, 能设置数据的过期时间, 定期清理数据释放空间, 主要是运维需求, 节省用户的运维工作量
- 不提供一致性, 及时性, 只有运维性
- TTL是一种外部运维特性, 所以在存储系统之外向存储系统发送删除指令, 是运维操作, 不是内在特性
- 分页操作, 例如 MySQL 的 limit, Redis 的 zrange, 虽然技术上性能局限性比较突出, 但好用, 用户喜欢用
- 批量操作(性能), 例如 mget, mset, mdel, 主要是方便用户使用, 性能也有优势
- Pipeline 也算批量操作
- 批量操作如果不承诺原子性, 可以在客户端实现
- 可选持久化策略
- auto: 只需要 write(), 不要求 fsync()
- strong: 共识必须在磁盘 fsync() 之后
- 可选写一致性级别
- 默认是单大区内(raft组)强一致性
- 默认大区间是最终一致性(跨地域同步)
- 提供 sync_write() 让用户等待同步进度, 在写数据之后调用
- 可选读一致性级别
- 默认是单大区内(raft组)强一致性
- 默认大区间是最终一致性(跨地域同步)
- 用户可选 stale read: 读单机
- 提供 sync_read() 记用户等待同步进度, 在读之前调用
- 提供事务能力(锁+快照), 多个写操作之间有关联和依赖
- 悲观事务(读写, Transaction), 交互式 2PC, 有 commit point
- begin() -> commit()
- 原子写入/乐观事务(只写, AtomicWrite), 非交互式 2PC, 写操作之间无因果依赖关系
- atomic() -> commit()
- 若有参与者 prepare 失败(包括 CAS 冲突)则全部回滚
- 相比悲观事务, 可以并发 prepare
- 批量写入(只写, MultiWrite), 1PC, 忽略 CAS 冲突, 最终全部会被执行, LWW
- multi() -> exec()
- 真正执行时, 如果对象当操作的时间戳更新, 则直接丢弃操作(认为操作已经完成, 然后被覆盖)
- hdel 操作的 CAS 比对的是元素的 mtime
- hclear 操作的 CAS 比对的是容器的 ctime, 异步地遍历每一个元素检查 mtime 决定是否删除
- 提供 lock 指令
- 以事务启动时的时间戳作为所有参与者的 mtime
- 读操作忽略未决资源(最终原子性, read committed, 不是 monotonic atomic view)
- 悲观事务(读写, Transaction), 交互式 2PC, 有 commit point
- 提供 binlog 和 redo log 订阅接口
- CAS 操作, CAS+原子性, 可以取代锁, 提供事务一致性保证
- MVCC 底层数据保留多个版本, 但带来的问题非常多, 例如容器计数也要保留多个版本
相关文章:
TTL 实现原理
TTL 功能做成外挂, 将数据的 version 和 ttl 保存到过期队列中, 按时间消费这个队列, 采用 CAS 接口删除数据. 不要把 ttl 和数据放到一起, 要分开放.
version 要注意被重置的问题. 例如, 一个 key 被删除后, 它的 version 会被重置为 0.
TTL 队列在 apply 的时候, 无论 leader 或者 follower 都维护, 但只在 leader 节点扫描处理.
状态机处理流程:
过期扫描流程:
过期队列和数据是分开独立存储的, 队列也要保存 apply 进度. 先 apply 队列, 再 apply 数据.
如果同时在每一个数据项的 meta 里保存 ttl. 相当于 ttl 信息冗余两份. 这样, 队列就可以异步 apply.