如何在Cassandra中以独特和原子的方式实现跨多个表的插入?

Sto*_*ica 6 transactions cassandra

我有一个数据模型,其中域对象有两个必须都是唯一的字段,并且对象必须可以通过两个独立获取.其中一个是随机生成的,因此我们可以假设没有可能的碰撞.另一个是用户选择的.这是我想出的:

CREATE TABLE object_primary (
    generated_value text PRIMARY KEY,
    data blob
);

CREATE TABLE object_unique_index (
    user_value text PRIMARY KEY,
    generated_value text
);
Run Code Online (Sandbox Code Playgroud)

这里我使用object_unique_index作为主表和资源锁的索引,其中资源是用户选择的全局唯一值.

初步想法:

  • 插入object_unique_index必须使用IF NOT EXISTS.因此我不能使用批次.
  • 插入到object_primary中并不是因为生成器已经保证了唯一性.然后,这让我使用自定义TIMESTAMPs,避免创建回读.
  • 如果第一个插入失败,我不应该继续第二个插入.
  • 如果第二个插入失败,我应该回滚第一个插入.
  • 不应将系统置于任一列中存在行而不存在另一列的状态.
  • 我愿意在回滚期间忽略错误,并将清理委托给外部(可能是手动)进程.

似乎很清楚如何继续,但我正在努力解释非条件更新的某些错误情况.所有现有描述都假设您不关心最终结果是什么,并且稍后将再次尝试写入.

UnavailableException:没有足够的节点用于仲裁,但当它们重新联机时,保存的提示将重新运行写入.这是否意味着最终状态将是写入成功?如果是这样,什么读一致性水平允许我看到它?如果没有,我怎么知道最终的状态是什么?

CassandraWriteTimeoutException:有足够的节点用于法定人数,但有些节点没有及时回复.据我所知,这只是一个更模糊的版本UnavailableException.它应该如何处理有什么不同?

我的很多困惑来自这里相互矛盾的陈述:

协调器可以强制结果进入更新前或更新后状态.

[...]

协调器在本地存储更新,并在恢复时将其重新发送到失败的副本,从而迫使它进入客户端最初需要的更新后状态

那么它什么时候迫使它进入更新前状态?如何判断它是否在更新后(因此我忽略它)或更新前(因此我回滚第一个插入)?

有没有办法解决这个问题,而不要求所有插入都是有条件的,从而增加了更多的性能损失并且失去了设置写时间的能力?

Wil*_*ice 7

关于Cassandra错误处理完成的DataStax博客文章涵盖了这个问题中提出的大部分主题,我将在整个答案中提到该文章的部分内容.

不应将系统置于任一列中存在行而不存在另一列的状态.

使用包含对两个表的写入的原子批处理.不要IF NOT EXISTS批处理内部那样使用比较和设置(CAS)操作.我稍后会介绍.

Cassandra 1.2引入了原子批次,它依赖批量日志来保证最终应用批次中的所有突变.这种原子性非常有用,因为它允许开发人员在不同的分区上执行多次写操作,而不必担心将应用哪个部分,哪个部分不会:最终都会写入全部或全部的突变.

重要的是要注意,它并不能让您完全控制写入何时可见,但批次的所有部分(最终)或其中任何部分都不会(永远).

用户选择的值必须是唯一的.(系统指定的值假定始终是唯一的.)

如您所知,轻量级交易(CAS的另一个术语)是保证独特性的唯一方法.我建议创建一个专门用于条件写入目的的第三个表,其定义类似于您在问题中定义的方式; 我会叫它.通过为插入路径专用一个表,没有其他应用程序逻辑会因为竞争条件而"看到"一个不一致的状态,因为没有别的东西应该从这个表读取; 它唯一用于检查.(我假设批处理中的两个表都用于普通应用程序逻辑的操作.) object_unique_indexunique_user_value_for_insertIF NOT EXISTS

INSERT user_value, generated_value INTO unique_user_value_for_insert IF NOT EXISTS;
Run Code Online (Sandbox Code Playgroud)

如果此插入返回结果集where [applied]=false,那么用户提供的名称不是唯一的,您不应该尝试批量插入.如果结果集指示[applied]=true则执行批处理.

使用此CAS插入和上面的批处理,应该涵盖通过此逻辑的正常"快乐"路径.我们仍然需要处理可能的特殊路径.

用UnavailableException

当请求到达协调器并且没有足够的副本来实现请求的一致性级别时,驱动程序将抛出UnavailableException.如果仔细查看此异常,您会发现在触发错误时可以获得已知存在的副本数量,以及请求的一致性级别所需的副本数量.

我不能作为异常的权威说话,但是这个描述听起来像协调器节点任何尝试执行操作之前抛出此异常.如果为true,那么初始CAS插入的失败不需要恢复操作,超出了您的应用程序,认识到插入因唯一性违规以外的原因而未成功.原子批次失败(CAS插入后)表明您需要"撤消"CAS插入.

我通过发送一个具有非常宽松的写一致性级别的DELETE来撤消,CL.ANY以确保删除最有可能被持久化/重放到可能已执行写入的任何可用副本.如果失败,你的集群是不健康的.

CassandraWriteTimeoutException

如果协调器级别的写入超时,则无法知道是否已在非应答副本上应用了突变.... [T]因此,处理此错误将取决于写操作是否是幂等的(CQL中的大多数语句都是这种情况)(对于计数器更新,以及对列表的追加/前置更新).

处理此问题的方法因上述两个操作中的哪一个失败以及异常中显示的信息而有很大差异.我很遗憾不得不从博客中引用这么多,但它有很好的解释.首先,如果CAS插入操作失败,则出现此异常:

如果paxos阶段失败,驱动程序将抛出WriteTimeoutException,其中WriteType.CAS与WriteTimeoutException#getWriteType()一起检索.在这种情况下,您无法知道CAS操作是否已应用,因此您需要重试它才能回退到稳定状态.由于轻量级事务比定期更新昂贵得多,因此驱动程序不会自动为您重试.如果没有足够的副本可用,paxos阶段也可能导致UnavailableException.在这种情况下,重试无效,因为只有SERIAL和LOCAL_SERIAL一致性可用.

这可能是您方案中最复杂的故障.由于"您无法知道CAS操作是否已应用",因此在某些情况下重试 IF NOT EXISTS是不明确的.如果你再试一次,那就成功了,这是最好的情况; 重试的插入值仍然是唯一的,您可以继续批处理.如果IF NOT EXISTS 失败则有两种可能性:

  1. 原始失败的CAS操作部分成功写入一个或多个副本.
  2. 该值实际上并不唯一,原始操作应该返回[applied]=false].

我不相信你可以在没有对其他权威状态进行读操作的情况下消除这些情况之间的歧义.如果您建议使用CAS的"第三个"仅插入表,则查询其他表以查看该名称是否存在现有数据.

或者,如果CAS插入在提交阶段失败:

然后,提交阶段类似于常规Cassandra写入,如果不满足所需副本或确认的数量,它将抛出UnavailableException或WriteTimeoutException.在这种情况下,如果您确保在此事务触及的列上的后续读取语句中使用setConsistencyLevel(ConsistencyLevel.SERIAL),而不是重试整个CAS操作,则可以简单地忽略此错误,因为它将强制Cassandra在继续读取之前提交任何剩余的未提交的Paxos状态.话虽如此,在CAS写入失败后组织应用程序使用SERIAL读取可能并不容易,因此您可能更喜欢另一种替代方法,例如CAS操作的整个重试.

上述信息似乎也适用于批处理情况下此异常的失败,与Paxos事务相比更接近"常规"写入,并附加以下信息:

如果在执行批处理时发生超时,则开发人员具有不同的选项,具体取决于超时的写入类型(请参阅WriteTimeoutException#getWriteType()):
 

BATCH_LOG:协调器等待批处理日志副本确认日志时发生超时.因此,可以应用或不应用批次.默认情况下,当通知发生此类超时时,驱动程序将重试批处理查询一次.因此,如果您收到此错误,您可能想再次重试,但这已经是一个难闻的气味,协调员不幸两次选择副本.

BATCH:在条目成功写入批处理日志后,在达到原子批次中的一个更改的副本时发生超时.因此,Cassandra将确保最终将此批次写入相应的副本,并且开发人员不必执行任何操作.但请注意,此错误仍表示尚未更新所有列.因此,如果要执行的业务逻辑中需要这些写入的直接一致性,您可能需要考虑对最终用户的备用结束或警告消息.

UNLOGGED_BATCH:协调程序在到达副本时遇到超时,写入查询是未记录批处理的一部分.由于没有写入批处理日志条目,因此不保证此批处理是原子的,因此将要或将不应用的批处理部分是未知的.需要重试整个批次才能回到已知状态.

或者,使应用程序足够强大以处理不一致的状态.

在我自己的应用程序中,我没有打扰第三个表.我的过程是:

  1. 插入由系统生成的唯一值键入的记录,而不必担心唯一性.
  2. 根据条件,在给定用户生成的值作为键的情况下插入指向先前ID的记录IF NOT EXISTS.这应该是最后一步.
  3. 如果发生任何故障,请尝试通过删除数据来撤消先前的操作.在这种情况下,我不会将系统生成的ID暴露给用户,除非完全成功,并且(在我的应用程序中)没有任何东西扫描系统生成的ID,因此我的最坏情况是"悬空"数据浪费空间.
  4. 实施定期的后台清理任务以查找并修复不一致问题.


Sto*_*ica 1

William Price 的回答tl;dr

只有无条件插入有任何疑问,因此请切换操作顺序并假设任何异常都会失败。只要您从未给出可能失败的系统生成的 id,它就永远无法使用,因此相应的用户生成的值是否唯一也没关系。

  1. 插入由系统生成的唯一值作为键控的记录,而不必担心唯一性。
  2. 出现任何错误时,删除一致性级别为 的记录ANY并使整个操作失败。
  3. 以用户生成的值作为键,插入指向先前 ID 的记录,条件是 IF NOT EXISTS。
  4. 如前所述,判断是否确实成功,如果没有则执行与之前步骤2中的插入相同的回滚
  5. 实施定期的后台清理任务以查找并修复不一致之处。

因此根本不需要担心解释不明确的异常。