Partitioning(分区)
对于非常大的数据集,或非常高的吞吐量,仅复制是不够的:我们需要将数据进行分区(partitions),也称分片(sharding)
Partitioning of Key Range(根据键范围分区)
缺点是某些特定的访问模式会导致热点
Partitioning by Hash of Key(根据键的散列分区):
由于偏斜和热点的风险,许多分布式数据存储使用哈希函数来确定键的分区
缺点:失去了键范围分区的一个很好的属性,高效执行范围查询的能力。曾经相邻的键分散在所有分区里
可通过组合索引方式解决:
哈希分区可以帮助减少热点。但不能完全避免,有一些极端场景。
如社交网站上,拥有数百万粉丝的名人在做某事时可能引发一场风暴。此事件可能导致大量写入同一个键(
键可能是名人ID,或人们谈论的热点话题)。
大多数数据系统无法自动补偿这种高度倾斜的负载。一个简单的办法是在主键的开始或结尾添加一个随机数,
只要一个两位数的十进制随机数就可以将主键分散为100种不同的主键,从而存储在不同分区中。
当有二级索引时,两种方法对数据库进行分区:document-based(基于文档)、term-based(基于关键字)
假设一个二手车网站,每条数据都有一个唯一ID,称之为文档ID--并用文档ID对数据库进行分区(如,分区0中
的ID 0到499,分区1中的ID 500到999)
document-based:
此索引方法中,每个分区维护自己的二级索引,不关心其他分区的数据。因此也称这种索引为本地索引(local index)
因为特定查询可能需要访问所有分区(如搜索红色汽车),此查询方法称为scatter/gatter(分散/聚集),可能使二级
索引上的读取查询相当昂贵。
term-based:
我们可以构建一个覆盖所有数据的全局索引。但不能把这个索引只存在一个节点上,这样会出现性能瓶颈,违背了
分区的目的。全局索引也必须进行分区,但可采用与主键不同的分区方式。
全局索引的缺点在于写入速度较慢和复杂,因为写入单个文档可能影响索引的多个分区(文档中每个关键词可能位于
不同的分区或节点上)
分区再平衡
随着时间推移,数据库会有各种变化
- 查询吞吐量增加,要添加更多的CPU处理负载
- 数据集大小增加,要添加更多的磁盘和RAM
- 机器出现故障,需要其他机器接管
这些更改都需要数据和请求从一个节点移动到另一个节点。这个过程称为再平衡(rebalancing)
几种平衡策略
1.固定数量的分区:
创建比节点更多的分区,如运行在10个节点上的数据库被分为1000个分区。有新节点加入时,新节点可以从当前
每个节点拿一些分区,知道分区再次公平分配。
2.动态分区:分区数量与数据集的大小成正比
3.按节点比例分区:分区数与节点数成比例。
服务发现(service discovery)
客户发出请求时,如何知道要连接哪个节点?如想读或写键"foo",需要连接哪个IP地址和端口号?
有几种不同的解决方案:
1.允许客户联系任何节点。
2.首先将所有客户端请求发到路由层,路由层决定哪个节点处理,并相应转发。
3.要求客户端知道分区和节点的分配。
以上所有情况的关键问题是,做出路由决策的组件(节点、路由层或客户端)如何了解分区--节点之间的分配关系变化?
许多分布式数据系统都依赖于一个独立的协调服务,比如ZooKeeper来跟踪集群元数据。每个节点在ZooKeeper
中注册自己,ZooKeeper维护分区到节点的可靠映射。其他参与者(如路由层或分区感知客户端)可在ZooKeeper中
订阅此信息。只要分区分配发生改变,ZooKeeper就会通知路由层使路由信息保持最新状态。
第七章:事务(transaction)
数十年来,事务一直是简化这些问题的首选机制。
但不应将事务当作理所当然,事务不是自然的规律,它们是为简化编程模型访问数据库设计的。
并不是所有的应用都需要事务,有时弱化事务保证或完全放弃事务也是有好处的(如获得更高性能)
事务的ACID:Atomicity原子性、Consistency一致性、隔离性Isolation、持久性Durability
Atomicity:能够在出错时中止事务,丢弃该事务进行的所有写入变更的能力。
Consistency:you have certain statements about your data (invariants) that must always be true。但一致性的概念取决于应用程序对不变量的定义。一致性是应用程序的属性,其他三者不是。
Isolation:同时执行的事务是相互隔离的,它们不能互相冒犯。然而实践中很少用Isolation,有性能损失。
Durability:事务一旦成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据都不会丢失
read-commited,为了避免dirty-read、dirty-write
implementing read-commit:
prevent dirty-write:using row-level locks;Only one transaction can hold that lock。
prevent dirty-read:database remembers both the old-commited value and the new value set by the transaction that currency holds the write lock.
read-commited has problems: nonrepeatable read or read skew
Snapshot solation(快照隔离) is the most common solution to this problem.
To implements snapshot isolation,database potentially keep several different commited versions of an object,
this technique is known as multiversion concurrency control(MVCC).
how MVCC-based snapshot isolation is implemented:
the lost update problem : it is a common problem,a variety of solutions have been developed.
1.Atomic write operations(原子写) : such as UPDATE counters SET value = value + 1 WHERE key = 'foo'
2.Explicit locking(显式锁) : for update语句
3.Automatically detecting lost updates : allow them to execute in parallel,if the transaction manager detects
a lost update,abort it.
4.Compare-and-Set(CAS) : apply in database that don't provide transactions.
5.Conflict resolution and replication : 复制数据库中,防止丢失的更新需要考虑另一个维度:由于多节点存在数据副本且不同节点的数据可能被并发修改,因此需要一些额外的步骤防止丢失更新。
Write Skew and Phantoms(写偏差和幻读):
两个事务读取相同的对象,然后更新另一些对象,就可能发生写偏差。
所有类似的例子遵循类似的模式:
1.一个SELECT查询出符合条件的行,并检查是否符合一些要求。
2.按照第一个查询的结果,应用代码决定是否继续。
3.如果决定继续,执行写入操作,提交事务。
一个事务中的写入改变另一个事务的搜索结果,称为幻读(Phantoms)
解决方案:A serializable isolation(可序列化的隔离级别) level is much preferable in most cases.
Most databases that provide serializability today use one of three techniques:
1.Literally executing transactions in a serial order(Redis implements this).
2.两相锁定(2PL, two-phase locking),几十年来唯一可行的选择。
3.乐观并发控制技术,例如可序列化的快照隔离(serializable snapshot isolation)
为了高效执行方法1,可在存储过程中封装事务(Encapsulating transactions in stored procedures)
With stored procedures and in-memory data,executing all transactions on a single thread becomes feasible.