如果在更新时没有指定TTL,Cassandra的主键TTL将被设置为0,但如果指定了TTL,则主键上的TTL不会更改。

7

在Cassandra中,这种行为似乎不符合直觉,我想知道为什么会发生这种情况,并可能绕过它。


想象一下,我有一个带有三列的表:pk,主键,类型为textfoo,类型为bigint,和另一个text类型的bar

insert into keyspace.table (pk, foo, bar) values ('first', 1, 'test') using ttl 60;

这将在我的表中创建一行,其生存时间为60秒。看起来是这样的:

  pk  | foo | bar
------------------
first |  1  | test

现在我做的是:

update keyspace.table using ttl 10 set bar='change' where pk='first';

接下来,我看着这一行文字,它经历了以下变化:

  pk  | foo | bar
--------------------
first |  1  | change
first |  1  | <<null>>  // after 10 seconds
   << deleted >>        // after the initial 60 seconds

一切都很好。我想要的是仅仅改变bar的存活时间,而不是其他东西,尤其不是主键。这种行为是可以预期的。


然而,如果我的更新没有ttl,或者设置为0:

update keyspace.table set bar='change' where pk='first';

然后我看到这种行为随着时间而改变。
  pk  | foo | bar
--------------------
first |  1  | change
first |  0  | change   // after the initial 60 seconds

换句话说,该行永远不会被删除。因为 "foo" 没有被更改,所以它的生存时间仍然有效,在其过期后该值被删除(设置为0)。但是,pk 的生存时间确实已经更改了。这完全出乎意料。
如果我不在更新中指定生存时间,为什么主键的生存时间只会更改一次?如何解决这个问题,使主键的生存时间仅在明确要求更改时才更改?
编辑:我还发现,如果我使用比最初生存时间更长的生存时间,它似乎也会更改主键的生存时间。
update keyspace.table using ttl 70 set bar='change' where pk='first';

  pk  | foo | bar
--------------------
first |  1  | change
first |  0  | change   // after the initial 60 seconds
   << deleted >>       // after the 70 seconds
2个回答

11
您正在经历的效果是由Cassandra使用的存储模型引起的。
在您的示例中,如果表没有任何聚簇列,则表中的每一行都映射到数据存储中的一行(通常称为“Thrift row”,因为这是通过Thrift API公开的存储模型)。在表中不属于主键的每个列(因此在您的示例中,foobar列)都映射到Thrift行中的一列。除此之外,还会创建一个不在CQL行中可见的额外列作为标记,表示该行存在。
TTL过期发生在Thrift列的级别上,而不是CQL列。当您插入一行时,您插入的所有列以及行本身的特殊标记都得到相同的TTL。
如果您更新一行,则只有您更新的列会获得新的TTL。行标记不改变。
运行具有SELECT的查询时,返回至少存在一个列特殊行标记的所有行。这意味着具有最高TTL的列定义了CQL行的可见时间长度,除非行本身的标记(仅在使用INSERT语句时才更改)具有更长的TTL。
如果您想确保行的主键与新列值一样被更新为相同的TTL,则解决方法很简单:在更新行时使用INSERT语句。这将具有与使用UPDATE完全相同的效果,但它还将更新行标记的TTL。
这种解决方法的唯一缺点是它无法与轻量级事务(IF子句中的INSERTUPDATE语句)结合使用。如果您需要这些与TTL结合使用,则必须使用更复杂的解决方案,但我想这会是一个单独的问题。如果你想要更新某行的一些列,但仍希望在最初插入时指定的TTL过期后整个行消失,Cassandra并不直接支持这种操作。唯一的方法是先查询其中一个列的TTL,然后在UPDATE操作中使用该TTL来找到该行剩余的TTL。例如,你可以使用“SELECT TTL(foo) FROM table1 WHERE pk = 'first';”。然而,这会影响性能,因为它会增加延迟(你必须等待SELECT结果才能运行UPDATE)。
作为替代方案,你可以添加一个列作为“行存在”标记,只在INSERT时使用并且从不在UPDATE中使用。然后,你可以简单地忽略该列为null的行,但这种过滤需要在客户端实现,并且如果无法在UPDATE中指定TTL,则更新的列永远不会被删除。

如果正在使用复合主键,会发生什么变化? - OrangeDog
1
使用复合主键不应该改变任何东西。就低级别(Thrift)存储模型而言,复合主键实际上只是一个元组。在CQL级别上对不同列的映射只是语法糖。 - Sebastian Marsching
如果有聚集键呢? - OrangeDog
2
基本上,即使有聚簇键,情况仍然如此。但是,我在回答中提出的一些主张不再有效。例如,如果有聚簇键,则所有具有相同分区键的CQL行实际上将映射到同一个Thrift行。如果您想找出特定一组CQL行和列如何映射到底层数据存储,请使用“cassandra_cli”查看数据是个好主意。这将显示通过CQL接口不可见的内部细节。 - Sebastian Marsching
感谢您的解释。您能否详细说明一下轻量级事务方面的复杂解决方法?为了确保在使用UPDATE命令设置TTL后删除行,而不是使用INSERT进行数据的初始插入,我使用了UPDATE。这是否意味着我的记录根本没有行标记,这会造成任何问题吗? - Ervis Zyka
1
LWT的问题在于当行还不存在时必须使用INSERT,而当行已经存在时则需要使用UPDATE。如果在没有IF子句的情况下使用INSERT或UPDATE,则不是LWT。混合使用LWT和非LWT操作可能会导致许多问题,因此不建议这样做(请参见http://docs.datastax.com/en/cassandra/3.x/cassandra/dml/dmlLtwtTransactions.html)。 - Sebastian Marsching

4
经过一些测试,以下是预期的结果。TTL具有列的细粒度。
  • 在进行更新操作时,如果没有指定TTL,则列TTL设置为0。此操作不会影响其他列TTL。
  • 我们无法在单个CQL命令中更新列值并保留旧列值TTL。
  • 当所有列TTL都过期时,将删除行(或主/分区键)。如果列具有TTL或0,则行不会被删除。

截至今天(Cassandra 2.1),以下是如何更新列值并保留其TTL的方法:

SELECT TTL(col1) FROM table1 where pk=1;
// read the ttl value fetched.
UPDATE table1 USING TTL <the_ttl_value> set col1='change' where pk=1;

这并没有真正回答问题。如果您有不同的问题,可以通过点击提问来提出。一旦您拥有足够的声望,您还可以添加赏金以吸引更多关注此问题的人。 - Jordan
@Jordan,我在这里编辑并恢复了我的先前回答。这样做是正确的吗? - Alan Boudreault
这并没有回答我的问题,即为什么有时候行会被删除,而有时候不会。不过我很感激您抽出时间来提交该问题,我正在密切关注它。 - 2rs2ts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接