表格存储为您提供了局部事务功能,即您可以创建一个范围不超过一个分区键值的事务,并在该事务内进行读写操作。最终提交事务从而使这些改动永久生效,或者丢弃事务从而放弃这些改动。

在使用局部事务时,您可以先在指定的分区键值上创建一个局部事务,此时表格存储服务端会返回一个事务ID。您可以使用此事务ID在对应的分区键值范围内进行读写操作,然后使用事务ID选择提交该事务,使事务中的所有数据修改生效。或者使用事务ID放弃该事务,则该事务中的所有修改都不会应用到原有数据上。

使用局部事务功能可以实现单行或多行读写的原子操作,从而扩展了使用场景。
说明
  • 事务范围,当前事务范围在一个分区键之内。
  • 分区键的概念请参阅主键和属性
  • 目前局部事务功能处于邀测中,默认关闭。如果需要使用该功能,请通过工单进行申请,或加入钉钉群“表格存储公开交流群”进行咨询。

相关接口

  • StartLocalTransaction:创建一个局部事务。
  • CommitTransaction:提交一个事务。
  • AbortTransaction:丢弃一个事务。
  • PutRowUpdateRowDeleteRowBatchWriteRow等写接口均支持局部事务。
  • GetRowGetRange等读接口均支持局部事务。

典型使用场景

  • 读-写场景(简单场景)

    当您要进行读取-修改-写回(Read-Modify-Write)操作时,您可以选择:

    但这两种方法都存在某些限制:

    • 条件更新只能处理单行单次请求,不能处理数据分布在多行,或需要多次写入的情况。
    • 原子计数器只能处理单行单次请求,且只能进行列值的累加操作。

    使用局部事务可以实现一个分区键值范围内的通用读取-修改-写回流程:

    • 首先使用StartLocalTransaction针对这个分区键值创建一个事务。
    • 使用GetRowGetRange读取数据。请求中需要带上事务ID。
    • 客户端本地修改数据。
    • 使用PutRowUpdateRowDeleteRow、或BatchWriteRow将修改后的数据写回,且请求中需要带上事务ID。
    • 使用CommitTransaction提交该事务。
  • 邮箱场景(复杂场景)

    我们可以使用局部事务来实现对同一个用户邮件的原子操作。

    为了能正常使用局部事务功能,我们在一张物理表上同时使用了一张主表和两张索引表,其主键列为:

    UserID Type IndexField MailID
    主表 用户ID "Main" "N/A" 邮件ID
    Folder表 用户ID "Folder" $Folder 邮件ID
    SendTime表 用户ID "SendTime" $SendTime 邮件ID

    其中,我们用Type列来区分主表和不同的索引表,不同的索引行用IndexField列保存不同含义的字段,而主表本身不用这一列。

    我们可以使用局部事务来完成以下操作:

    • 列出某个用户发送的最近100封邮件:
      • 使用UserID创建一个局部事务,获取事务ID。
      • 使用事务ID对SendTime表调用GetRange,获取100封邮件。
      • 使用事务ID对主表调用BatchGetRow,获取100封邮件的详细信息。
      • 提交或丢弃事务(因为该事务没有任何写操作,两个操作是等同的)。
    • 将某个目录下的所有邮件移到另一个目录下:
      • 使用UserID创建一个局部事务,获取事务ID。
      • 使用事务ID对Folder表调用GetRange,获取若干封邮件。
      • 使用事务ID对Folder表调用BatchWriteRow。每封邮件对应两行写操作,一行是将对应旧Folder的行删掉,另一行是对应新Folder增加一行。
      • 提交事务。
    • 统计某个目录下已读邮件与未读邮件的数量(非最优方案,见下文解释):
      • 使用UserID创建一个局部事务,获取事务ID。
      • 使用事务ID对Folder表调用GetRange,获取若干封邮件。
      • 使用事务ID对主表调用BatchGetRow,获取每封邮件的已读状态。
      • 提交或丢弃事务。

    在这个场景中,我们还可以再增加新的索引表以加速一些常用操作。有了局部事务后,我们不用再担心主表与索引表的状态不一致,极大地降低了开发的难度。比如“统计邮件数量”这个功能在上面的方案中需要读取很多封邮件,开销较大,我们可以用一个新的索引表来保存已读和未读邮件的数量,从而降低开销,加速查询。

注意事项

  • 在事务期间,对应分区键值相当于被锁上,其它不持有事务ID在这个范围内的写请求都会失败。在事务提交或放弃或事务超时后,对应的锁也会被释放。
  • 同一个事务中所有写请求的分区键值必须与创建事务时的分区键值相同,读请求则无此限制。
  • 一个局部事务只能同时被一个请求使用,在事务被使用期间,其它使用此事务ID的操作都会失败。
  • 若用户在事务中写入了未填版本的Cell,该Cell的版本会在写入时(而非提交时)由表格存储的服务端确定,规则与正常的写入一个未填版本的Cell相同。
  • 若用户在事务中写入了未填版本的Cell,该Cell的版本会在提交时由表格存储的服务端确定,规则与正常的写入一个未填版本的Cell相同。
  • 不可使用事务ID访问事务范围(即创建时使用的分区键值)以外的数据。
  • 在创建事务后,若长时间未使用该事务,则表格存储服务端会认为此事务超时,并将事务丢弃。
  • 未提交的事务可能失效,若出现此情况,用户需要重试该事务内的操作。
  • 若创建事务时超时,此请求可能在表格存储的服务端已执行成功,此时用户需要等待该事务超时后重新创建。
  • BatchWriteRow请求中带有事务ID,则此请求中所有行只能操作该事务ID对应的表。
  • 带有事务ID的读写请求失败不会影响事务本身的存活情况,用户可以按照正常的无事务ID的读写请求重试规则进行重试,或主动丢弃掉当前事务。

限制

  • 每个事务中写入的数据量最大为4MB,按正常的写请求数据量计算规则累加。
  • 每个事务中两次读写操作最大间隔为60秒,超过60秒未操作的事务被视为超时。
  • 每个事务从创建开始生命期最长为60秒,超过60秒未提交或丢弃的事务被视为超时。

写入的数据量计算方式请参阅计量项和计费说明

错误码

  • OTSRowOperationConflict:该分区键值已被其它局部事务占用。
  • OTSSessionNotExist:事务ID对应的事务不存在,或该事务已失效或超时。
  • OTSSessionBusy:该事务的上一次请求尚未结束。
  • OTSOutOfTransactionDataSizeLimit:事务内的数据量超过上限。
  • OTSDataOutOfRange: 用户操作数据的分区键超出了事务创建的分区键范围。

计量计费

  • StartLocalTransactionCommitTransactionAbortTransaction请求分别消耗1个单位的写能力单元。
  • 其它读写请求的计量计费与正常的读写请求相同。

关于计量计费详情,请参阅计量项和计费说明

示例代码

  • 创建局部事务

    我们可以调用AsyncClient或SyncClient的startLocalTransaction方法来创建一个局部事务,参数为一个分区键的值,并获得对应的事务ID:

    PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
    primaryKeyBuilder.addPrimaryKeyColumn("pk1", PrimaryKeyValue.fromString("txnKey"));
    PrimaryKey primaryKey = primaryKeyBuilder.build();
    StartLocalTransactionRequest request = new StartLocalTransactionRequest(tableName, primaryKey);
    String txnId = client.startLocalTransaction(request).getTransactionID();
  • 在事务范围内进行读写

    在事务范围内进行读写的方式与正常读写几乎相同,只要填入事务ID即可。

    写入一行数据:

    PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
    primaryKeyBuilder.addPrimaryKeyColumn("pk1", PrimaryKeyValue.fromString("txnKey"));
    primaryKeyBuilder.addPrimaryKeyColumn("pk2", PrimaryKeyValue.fromLong("userId"));
    PrimaryKey primaryKey = primaryKeyBuilder.build();
    RowPutChange rowPutChange = new RowPutChange(tableName, primaryKey);
    rowPutChange.addColumn(new Column("Col", ColumnValue.fromLong(columnValue)));
    
    PutRowRequest request = new PutRowRequest(rowPutChange);
    request.setTransactionId(txnId);
    client.putRow(request);
    读取这行数据:
    PrimaryKeyBuilder primaryKeyBuilder;
    primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
    primaryKeyBuilder.addPrimaryKeyColumn("pk1", PrimaryKeyValue.fromString("txnKey"));
    primaryKeyBuilder.addPrimaryKeyColumn("pk2", PrimaryKeyValue.fromLong("userId"));
    PrimaryKey primaryKey = primaryKeyBuilder.build();
    SingleRowQueryCriteria criteria = new SingleRowQueryCriteria(tableName, primaryKey);
    criteria.setMaxVersions(1); // 设置读取最新版本
    
    GetRowRequest  request = new GetRowRequest(criteria);
    request.setTransactionId(txnId);
    GetRowResponse getRowResponse = client.getRow(request);
  • 提交事务
    CommitTransactionRequest commitRequest = new CommitTransactionRequest(txnId);
    client.commitTransaction(commitRequest);
  • 丢弃事务
    AbortTransactionRequest abortRequest = new AbortTransactionRequest(txnId);
    client.abortTransaction(abortRequest);