Cassandra是一个分布式、高可用性、高性能的NoSQL数据库,适用于大规模数据存储和处理的应用,具有无中心化、线性可扩展性和灵活的数据模型。
作为分布式的数据库,必然存在一致性和可用性之间的权衡,意味着在某些情况下可能会牺牲一致性以获得更高的可用性,或者反之。本文简单介绍Cassandra可能存在数据不一致的几种情况。
- 时间戳不同步
默认情况下,Cassandra的数据操作依赖于客户端时间戳。以更新操作为例,客户端在UPDATE字段A的时候,会从本地获取时间戳t1,如果此时客户端中A字段的最新时间戳为t0,如果t0 > t1,那么本次更新从客户端视角来看是不成功的。
其实,t1时间戳的UPDATE操作也会被记录在数据库中,数据库关于A字段的操作的时间戳记录就有两条(t0和t1)。但是当客户端进行SELECT操作时,只会返回最新时间戳t0的记录(Cassandra读策略机制决定),因此就造成了读写不一致的情况。
注意:以时间戳t1进行更新时,返回结果与正常更新的返回结果一致,因此无法从客户端视角进行判断。
Cassandra提供了自定义时间戳和共享时间戳机制,使其避免依赖客户端时间戳。本文以自定义时间戳为例,提供两种解决方案。
(1)在CQL中使用USING TIMESTAMP字段设置自定义时间戳,一定要是正确的毫秒为单位的时间戳,但要注意的是,INSERT操作时,只要满足条件,可以指定任意值。进行UPDATE操作时,USING TIMESTAMP必须基于数据库中要更新字段的最新时间戳进行设定,具体实现如下:
LOCK()
status_update_timestamp = SELECT writetimestamp(status) FROM snapshot WHERE clientid = ? AND name = ?
UPDATE USING TIMESTAMP = {Status_update_timestamp + 1} status SET ? WHERE clientid = ? AND name = ?;
UNLOCK()
如上所示,假设要更新status字段,先通过SELECT查询该字段的最新时间戳,然后使用USING TIMESTAMP,这样保证UPDATE操作附带的时间戳一定是最新的,保证此次操作生效。注意:这两个操作一定要具有事务特性,可以通过显示加锁的方式进行实现,选取锁的粒度要合适。
- 在Cassandra中,提供了一种Lightweight Transactions(LWTs)轻量级事务机制,可以简单理解为类似于CAS的无锁操作。在UPDATE语句中使用LWTs,可以确保操作一定成功,具体是当UPDATE满足IF条件时才能成功,并且会自适应分配一个合适的时间戳,以满足UPDATE操作。这意味着,即使客户端时间戳不同步,只要符合LWTs机制的条件,UPDATE操作一定生效。并且,带有LWTs机制的UPDATE语句返回的结果中,APPLIED字段会有TRUE和FALSE两种状态,表明此次更新操作是否成功。因此,配合重试过程,可以达到同样的效果,具体如下:
While (true)
{
UPDATE status SET ? WHERE clientid = ? AND name = ? IF EXIST;
If (UPDATE is success) {
Break;
}
}
- 可调控的读写一致性
在Cassandra中,强调的是“最终一致性”,即如果在一致性级别较低时,Cassandra只能保证存储数据的最终一致,可能在某个时刻,数据库中所有副本之间的数据并不一致。
客户端在发起请求(读or写)时,可以显示指定一致性级别(默认为ONE),简单分为ONE,QUORUM和ALL。以单数据中心为例,假设数据中心有3个节点,replication_factors为3(表示数据的副本数量,用户创建键空间时指定),那么写请求的在三种一致性级别下的情况分别是:
- ONE:客户端向3个(所有)节点都发送写请求,但是只要有任意一个节点返回写成功,那么本次客户端的写请求就算成功;
- QUORUM:客户端向3个(所有)节点都发送写请求,必须满足QUORUM数量的节点返回写成功,那么本次客户端的写请求就算成功;
- ALL:客户端向3个(所有)节点都发送写请求,必须所有节点返回写成功,那么本次客户端的写请求才算成功;
读请求在三种一致性级别下的请求分别是:
- ONE:客户端向3个(所有)节点都发送读请求,只要任意一个节点返回数据记录,就直接返回该节点的记录;
- QUORUM:客户端向3个(所有)节点都发送请求,只要任意QUORUM数量的节点有数据返回,Cassandran会返回时间戳记录最新的数据;(QUORUM = replication_factors / 2 + 1)
- ALL:客户端向3个(所有)节点都发送请求,必须所有节点返回数据,并且Cassandran会返回记录中具有最新时间戳的数据;
综上所述,客户端在进行读写请求时,可以自定义指定读写的一致性级别,ONE级别为最低级别,效率高,一致性差,如过读写均为ONE,那么很大概率就会读写不一致。因此,只要保证下列条件,就能保证客户端视角的读写一致性:
读请求节点 + 写请求节点 >= replication_factors;
即保证读写一致性级别均在QUORUM及以上即可,保证客户端视角的读写一致性。
- 二级索引的异步更新
二级索引的创建和更新,均是由后台程序异步的扫描主表变化进行索引表的修改,因此存在一定的时间延迟,可能存在索引更新失败的情况。
INSERT操作时,二级索引初次创建时,会有时间延迟,会有一段主表与索引表不一致的时间段,这时候读取会有读写不一致的情况。一旦索引建立后,UPDATE操作会引起索引表的更新,这时候就会采用在读取时延迟应用检查以确保主表和索引表的数据一致性。
另外:如果WHERE条件中不包含全部主键,使用索引键进行查询时,返回结果不唯一(0、1或多个)。如果受索引延迟更新的影响,则可能读到(0,或其他记录行)。
如,有字段id, name, status三个字段,id和name为主键,status为索引键
SELECT * FROM table WHERE id = ? and status = ?
如这条查询语句,where中只有id主键,那么查询结果就可能不唯一。因此根据合适的场景,设定二级索引,要保证高基数字段设置二级索引这个前提。