目的:提供在同一区域内对跨表的数据项执行原子和可序列化操作的能力,同时保证性能的可预测性,并且不影响非事务性的工作负载。
因此,没有采用传统意义上的交互式事务,而是引入了两个新的单请求操作实现一次性事务,即TransactGetItems和TransactWriteItems。
无需其他操作,直接使用api即可支持事务
TransactGetItems
只读事务API,实现从一致的快照中检索多个条目,事务读和任何写操作之间是序列化的
使用
async function testTransactGet() {const params = {TransactItems: [{Get: {TableName: userTableName,Key: {user_id: '577fb67c-9330-4336-b0b3-9c7a59ef67a3',},ProjectionExpression: 'user_id,email,creation_time',},},{Get: {TableName: orderTableName,Key: {id: '1234',},ProjectionExpression: 'id,user_id,creation_time,esim_info',},},{Get: {TableName: orderTableName,Key: {id: 'de886376-fcfb-4acf-b24c-dd19ba13d768',},ProjectionExpression: 'id,user_id,creation_time,esim_info',},},],}const resp = await dynamoDb.transactGet(params)if (resp.$metadata.httpStatusCode !== 200) {console.log(`error`)} else {console.log(JSON.stringify(resp))}
}
{"$metadata": {"httpStatusCode": 200,"requestId": "5COQSMVUIM6SKDJE127IPLK627VV4KQNSO5AEMVJF66Q9ASUAAJG","attempts": 1,"totalRetryDelay": 0},"Responses": [{"Item": {"email": "jing.zhang@getnomad.app","user_id": "577fb67c-9330-4336-b0b3-9c7a59ef67a3","creation_time": "2023-12-14T11:12:36Z"}},{},{"Item": {"esim_info": {"matching_id": "JQ-20G3W1-1LSANYC","iccid": "8932042000006259105","require_manual_activate": false,"qr_data": "LPA:1$rsp.truphone.com$JQ-20G3W1-1LSANYC","smdp_url": "rsp.truphone.com","support_direct_install": false,"status": "Released","trust_installation_status": false},"user_id": "b5f20d65-6f5c-4512-8de5-4eb50dbebfc4","id": "de886376-fcfb-4acf-b24c-dd19ba13d768","creation_time": "2024-03-23T06:08:17Z"}}]
}
- 只能使用主键索引查询,使用其他字段会抛异常:TransactionCanceledException, The provided key element does not match the schema
- 同一事务内不能存在相同的条目,属于语法错误,会抛异常:ValidationException: Transaction request cannot include multiple operations on one item
- 多个项目的结果按序返回
TransactWriteItems
事务写API,允许在一个或多个表中原子地创建、删除或更新多个条目,支持:Put、Update、Delete 和 ConditionCheck 操作
- Put — 启动 PutItem 操作以创建一个新项目,或者将旧项目替换为新项目
- Update — 启动 UpdateItem 操作以编辑现有项目的属性,或者将新项目添加到表中
- Delete — 启动 DeleteItem 操作,以删除表中由其主键标识的单个项目
- ConditionCheck — 检查项目是否存在,或者检查项目特定属性的条件
使用
async function testUpdateCondition(clientRequestToken, userTagId) {const params = {TransactItems: [{ConditionCheck: {TableName: `test.sys.users`,Key: {user_id: '4ceae1dd-d331-4535-8835-6451f5d5269f',},ConditionExpression: `attribute_exists(user_id)`,},},{Put: {TableName: `dev.sys.user_tag`,Item: {id: userTagId,email: 'jing.zhang+testt1@getnomad.app',organization_id: 'dea6024b-09ea-4835-9335-4d689be48aae',account_tag: 'BT',operator_user_id: '577fb67c-9330-4336-b0b3-9c7a59ef67a3',creation_time: moment().utc().format(),},},},{Update: {TableName: orderTableName,Key: {id: '161dd505-1ed7-4384-a666-fa672f942cd5',},UpdateExpression: 'SET assigned = :expect_value,user_id = :expect_user_id',ExpressionAttributeValues: { ':expect_value': true, ':expect_user_id': '4ceae1dd-d331-4535-8835-6451f5d5269f', ':current_assigned': false },ConditionExpression: 'assigned = :current_assigned',},},],ClientRequestToken: clientRequestToken,}const resp = await dynamoDb.transactWrite(params)console.log(JSON.stringify(resp))
}
- ConditionCheck 用于条件检测,必须结合ConditionExpression
幂等-ClientRequestToken
- 使用方主动传入参数ClientRequestToken
- 幂等有效期为十分钟
- 十分钟内使用相同token发起相同参数的请求,会返回成功,实际未做更改
async function testUpdateCondition(clientRequestToken, userTagId) {const params = {TransactItems: [{Update: {TableName: userTableName,Key: {user_id: '577fb67c-9330-4336-b0b3-9c7a59ef67a3',},UpdateExpression: 'SET subscribe_to_feed = :expect_value',ExpressionAttributeValues: { ':expect_value': true },},},],ClientRequestToken: clientRequestToken,ReturnConsumedCapacity: 'INDEXES',}const resp = await dynamoDb.transactWrite(params)console.log(JSON.stringify(resp))
}
{"$metadata": {"httpStatusCode": 200,"requestId": "KNPADV761709N5G9F9D0P6RMQVVV4KQNSO5AEMVJF66Q9ASUAAJG","attempts": 1,"totalRetryDelay": 0},"ConsumedCapacity": [{"CapacityUnits": 6,"TableName": "test.sys.users","WriteCapacityUnits": 6}]
}
{"$metadata": {"httpStatusCode": 200,"requestId": "7J4BO6MQPCIAGTLMU1ICFJ5LRFVV4KQNSO5AEMVJF66Q9ASUAAJG","attempts": 1,"totalRetryDelay": 0},"ConsumedCapacity": [{"CapacityUnits": 2,"ReadCapacityUnits": 2,"TableName": "test.sys.users"}]
}
相同token相同request 十分钟内再次请求结果成功,但只消耗了2RCU,无WCU的消耗
- 十分钟内使用相同token发起不同参数的请求会报错:IdempotentParameterMismatchException: Specified idempotent token was used with different request parameters within the idempotency window
dynamoose
await dynamoose.transaction([// User.transaction.get({ user_id: '577fb67c-9330-4336-b0b3-9c7a59ef67a3' }),User.transaction.condition({ user_id: '4ceae1dd-d331-4535-8835-6451f5d5269f' }, new dynamoose.Condition().where('email').exists()),UserTag.transaction.create({id: '02f15ff7-38df-4250-a963-3aee1fec9fb3',email: 'jing.zhang+testt1@getnomad.app',organization_id: 'dea6024b-09ea-4835-9335-4d689be48aae',account_tag: 'BT',operator_user_id: '577fb67c-9330-4336-b0b3-9c7a59ef67a3',creation_time: moment().utc().format(),}),Order.transaction.update({ id: '161dd505-1ed7-4384-a666-fa672f942cd5' }, { assigned: true, user_id: '4ceae1dd-d331-4535-8835-6451f5d5269f' }),
])
‼️ dynamoose.transaction()入参支持get,condition,put,update,delete;但不支持同时执行get和write,需要使用方自主将两种操作分开调用
决定是write和get的方式:
- 设置setting.type,但是type定义只有内部声明,没有export,用起来会编译错误,但不影响实际执行
TransactionType
- 由dynamoose自主决定,有一条是write即为write,最终由事务校验报错:ValidationException: Invalid Request: TransactWriteRequest should contain Delete or Put or Update request
并发事务的处理
事务冲突
对相同数据的事务并发操作,存在事务冲突,即TransactionConflictException:
- 事务写和普通写同时执行且包含相同的数据,即 正在进行的TransactWriteItems请求 与 put/update/delete请求包含有相同的数据
- 事务写和事务写同时执行且包含相同的数据,即TransactWriteItems 请求中的某个项目是另一个正在进行的 TransactWriteItems 请求的一部分
- 事务读和写操作(事务写/普通写)同时执行且包含相同的数据,即TransactGetItems 请求中的某个项目是正在进行的 TransactWriteItems/BatchWriteItem/PutItem/UpdateItem/DeleteItem 请求的一部分
隔离级别
可序列化
当前操作在前一个操作完成之前不会开始执行
- 在任何事务操作与任何标准写入操作(PutItem、UpdateItem 或 DeleteItem)之间
- 在任何事务操作与任何标准读取操作 (GetItem) 之间
- 在 TransactWriteItems 操作与 TransactGetItems 操作之间
读已提交
- 事务操作与批量普通读(BatchGetItem、Query 或 Scan)之间均为读取已提交,一定不能读取到事务未提交的数据,预期能读取到提交事务的最新结果
- BatchGetItem和TransactWriteItems是读取已提交,但BatchGetItem中的单个读取和TransactWriteItems整体是序列化的
性能
事务操作对非事务操作的影响只是占据了一定的读写容量,而对于按需的容量模式可以忽略
- 事务操作和非事务操作是互不干扰的