You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DynamoDB并发更新时如何锁定条目?安全获取更新后数据的方法

DynamoDB高并发更新问题及解决方案

问题背景

在金融交易系统中,有两张DynamoDB表:

  • Accounts表:
    • id: string(主键)
    • balance: Number(账户余额)
  • Transactions表:
    • id: Number(主键)
    • amount: Number(交易金额)
    • balanceAfter: Number(交易后余额)
    • accountId: string(关联账户ID)

执行借记交易时,需要更新Accounts余额并在Transactions中记录交易后余额。当账户初始余额100,同时执行两笔50的交易时,最终余额变成50而非0,且两条交易记录的balanceAfter都是50。现需解决两个核心问题:

  1. 如何在DynamoDB更新时实现类似关系型数据库的事务锁,避免并发更新异常?
  2. 执行UPDATE后,如何安全获取已更新的条目?

原PHP代码在低并发下正常,但高并发出现异常:

<?php

require './vendor/autoload.php';

use Aws\DynamoDb\DynamoDbClient;
use Aws\Credentials\CredentialProvider;

function executeDebitTransaction($accountId, $transactionAmount) 
{
  $provider = CredentialProvider::defaultProvider();

  $client = DynamoDbClient::factory(array(
    'version' => '2012-08-10',
    'credentials' => $provider,
    'region'  => 'sa-east-1'
  ));

  $response = $client->getItem(array(
    'TableName' => 'Accounts',
    'Key' => array(
        'id' => array( 'S' => $accountId ))
    )
  );
  
  $currentBalance = $response['Item']['balance']['N'];
  
  $newbalance = (string)((int)$currentBalance - (int)$transactionAmount);
  
  $response = $client->updateItem(array(
    'TableName' => 'accounts',
      'Key' => array(
        'id' => array( 'S' => $accountId )
      ),
    'ExpressionAttributeValues' =>  array (
      ':amount' => array('N' => $transactionAmount),
    ),
    'UpdateExpression' => 'SET balance = balance - :amount'
  ));
  
  // Generate random ID
  $id = (string)(random_int(1, 1000000000));

  $client->putItem(array(
    'TableName' => 'Transactions',
    'Item' => array(
      'id' => array('N' => $id),
      'amount' => array('N' => $transactionAmount),
      'balanceAter' => array('N' => $newbalance), // 原代码拼写错误:balanceAter → balanceAfter
      'accountId' => $transactionAmount // 原代码错误:应传入$accountId而非$transactionAmount
    )
  ));
}

$accountId = 'A1469CCD-10B8-4D31-83A2-86B71BF39EA8';
$debitAmount = '50';
executeDebitTransaction($accountId, $debitAmount);

问题解答

1. 实现DynamoDB并发更新的原子性(事务锁)

DynamoDB支持事务API(TransactWriteItems),可在一个原子操作中完成多表写入/更新,确保操作要么全部成功,要么全部失败,避免数据不一致。

原代码的核心问题是getItem和updateItem之间存在并发窗口:两个请求同时读取到初始余额100,各自减50后更新为50。解决方案:

  • 用UpdateItem的条件表达式,确保更新基于最新状态,避免覆盖并发更新;
  • 结合事务API,将余额更新和交易记录写入绑定为原子操作。

2. 安全获取更新后的条目

在UpdateItem中设置ReturnValues参数为ALL_NEW,直接从更新响应中获取最新的账户数据,无需额外调用getItem,彻底消除并发窗口。


修正后的PHP代码

<?php

require './vendor/autoload.php';

use Aws\DynamoDb\DynamoDbClient;
use Aws\Credentials\CredentialProvider;

function executeDebitTransaction($accountId, $transactionAmount) 
{
  $provider = CredentialProvider::defaultProvider();

  $client = DynamoDbClient::factory([
    'version' => '2012-08-10',
    'credentials' => $provider,
    'region'  => 'sa-east-1'
  ]);

  // 生成唯一交易ID(推荐用UUID替代随机数,避免冲突)
  $transactionId = (string)random_int(1, 1000000000);
  
  try {
    // 事务操作:更新账户余额 + 写入交易记录
    $client->transactWriteItems([
      'TransactItems' => [
        [
          'Update' => [
            'TableName' => 'Accounts',
            'Key' => [
              'id' => ['S' => $accountId]
            ],
            'ExpressionAttributeValues' => [
              ':amount' => ['N' => $transactionAmount],
              ':minBalance' => ['N' => '0'] // 可选:确保余额不为负
            ],
            'UpdateExpression' => 'SET balance = balance - :amount',
            'ConditionExpression' => 'balance >= :amount', // 条件:余额足够支付
            'ReturnValuesOnConditionCheckFailure' => 'ALL_OLD',
            'ReturnValues' => 'ALL_NEW' // 返回更新后的完整账户数据
          ]
        ],
        [
          'Put' => [
            'TableName' => 'Transactions',
            'Item' => [
              'id' => ['N' => $transactionId],
              'amount' => ['N' => $transactionAmount],
              'balanceAfter' => ['N' => '$[0].balance.N'], // 引用Update操作返回的余额
              'accountId' => ['S' => $accountId]
            ]
          ]
        ]
      ]
    ]);
  } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) {
    // 处理异常:余额不足、并发冲突等
    echo "交易失败: " . $e->getMessage();
    throw $e;
  }
}

$accountId = 'A1469CCD-10B8-4D31-83A2-86B71BF39EA8';
$debitAmount = '50';
executeDebitTransaction($accountId, $debitAmount);

关键优化点说明

  • 事务原子性:通过TransactWriteItems绑定余额更新和交易记录写入,保证操作的一致性;
  • 条件更新:ConditionExpression确保余额足够支付,同时拦截并发更新导致的错误覆盖;
  • 直接获取更新数据:ReturnValues => 'ALL_NEW'避免额外查询,消除二次并发窗口;
  • 修复原代码错误:修正了字段拼写和赋值错误;
  • 冲突处理:并发冲突时事务会抛出异常,可通过重试机制保证最终一致性。

内容的提问来源于stack exchange,提问作者Thalys Wolf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 15:18:03