使用Python / Boto更新DynamoDB原子计数器

时间:2012-04-26 13:32:28

标签: python counter atomic boto amazon-dynamodb

我正在尝试使用Python Boto 2.3.0更新原子计数计数器,但是找不到该操作的文档。

似乎没有直接接口,所以我尝试使用layer1接口进行“原始”更新,但我甚至无法完成简单的更新。

我尝试了以下变化,但都没有运气

dynoConn.update_item(INFLUENCER_DATA_TABLE, 
                     {'HashKeyElement': "9f08b4f5-d25a-4950-a948-0381c34aed1c"}, 
                     {'new': {'Value': {'N':"1"}, 'Action': "ADD"}})    

dynoConn.update_item('influencer_data', 
                     {'HashKeyElement': "9f08b4f5-d25a-4950-a948-0381c34aed1c"}, 
                     {'new': {'S' :'hello'}})                                 

dynoConn.update_item("influencer_data", 
                     {"HashKeyElement": "9f08b4f5-d25a-4950-a948-0381c34aed1c"},
                     {"AttributesToPut" : {"new": {"S" :"hello"}}})      

它们都会产生同样的错误:

  File "/usr/local/lib/python2.6/dist-packages/boto-2.3.0-py2.6.egg/boto/dynamodb/layer1.py", line 164, in _retry_handler
    data)
boto.exception.DynamoDBResponseError: DynamoDBResponseError: 400 Bad Request
{u'Message': u'Expected null', u'__type': u'com.amazon.coral.service#SerializationException'}

我还调查了API文档here,但它们非常简洁。

我已经做了很多搜索和摆弄,我唯一剩下的就是使用PHP API并深入研究代码以找到它“格式化”JSON主体的位置,但这有点痛苦。请救我脱离这种痛苦!

5 个答案:

答案 0 :(得分:12)

对不起,我误解了你在找什么。您可以通过layer2完成此操作,尽管有一个小错误需要解决。这是一些第2层代码:

>>> import boto
>>> c = boto.connect_dynamodb()
>>> t = c.get_table('counter')
>>> item = t.get_item('counter')
>>> item
{u'id': 'counter', u'n': 1}
>>> item.add_attribute('n', 20)
>>> item.save()
{u'ConsumedCapacityUnits': 1.0}
>>> item  # Here's the bug, local Item is not updated
{u'id': 'counter', u'n': 1}
>>> item = t.get_item('counter')  # Refetch item just to verify change occurred
>>> item
{u'id': 'counter', u'n': 21}

这导致与您在Layer1代码中执行的相同的线上请求,如以下调试输出所示。

2012-04-27 04:17:59,170 foo [DEBUG]:StringToSign:
POST
/

host:dynamodb.us-east-1.amazonaws.com
x-amz-date:Fri, 27 Apr 2012 11:17:59 GMT
x-amz-security-    token:<removed> ==
x-amz-target:DynamoDB_20111205.UpdateItem

{"AttributeUpdates": {"n": {"Action": "ADD", "Value": {"N": "20"}}}, "TableName": "counter", "Key": {"HashKeyElement": {"S": "counter"}}}

如果你想避免初始的GetItem调用,你可以这样做:

>>> import boto
>>> c = boto.connect_dynamodb()
>>> t = c.get_table('counter')
>>> item = t.new_item('counter')
>>> item.add_attribute('n', 20)
>>> item.save()
{u'ConsumedCapacityUnits': 1.0}

如果该项目已存在,则会更新该项目,如果该项目尚不存在,则会创建该项目。

答案 1 :(得分:5)

对于那些寻找答案的人我已经找到了答案。 首先重要说明,我目前还没有意识到目前发生了什么,要获得一个我必须执行以下操作的第1层实例:

import boto
AWS_ACCESS_KEY=XXXXX
AWS_SECRET_KEY=YYYYY
dynoConn = boto.connect_dynamodb(AWS_ACCESS_KEY, AWS_SECRET_KEY)
dynoConnLayer1 = boto.dynamodb.layer1.Layer1(AWS_ACCESS_KEY, AWS_SECRET_KEY) 

基本上实例化第2层第一层然后第1层。 也许我做了一些愚蠢的事情,但在这一点上我很高兴让它工作.... 我稍后会对细节进行排序。然后......实际进行原子更新调用:

dynoConnLayer1.update_item("influencer_data", 
                    {"HashKeyElement":{"S":"9f08b4f5-d25a-4950-a948-0381c34aed1c"}},
                    {"direct_influence":
                        {"Action":"ADD","Value":{"N":"20"}}
                    }
                );

在上面的示例中注意,Dynamo将ADD 20添加到当前值,并且此操作将是原子意义,这意味着其他操作发生在&#34;同一时间&#34;将正确&#34;安排&#34;在执行此操作之前将新值设置为+20或之后发生。无论哪种方式,都可以达到预期的效果。

确保在layer1连接的实例上执行此操作,因为layer2会抛出错误,因为它需要一组不同的参数类型。

多数人都有!!!!大家都知道,我用PHP SDK来解决这个问题。安装和设置的时间非常短。当您进行呼叫时,调试数据实际上会显示HTTP请求主体的格式,因此您可以在示例后复制/建模您的layer1参数。这是我用来在PHP中进行原子更新的代码:

<?php 
    // Instantiate the class
    $dynamodb = new AmazonDynamoDB();

    $update_response = $dynamodb->update_item(array(
        'TableName' => 'influencer_data',
            'Key' => array(
                'HashKeyElement' => array(
                    AmazonDynamoDB::TYPE_STRING=> '9f08b4f5-d25a-4950-a948-0381c34aed1c'
                )
            ),
            'AttributeUpdates' => array(
                'direct_influence' => array(
                    'Action' => AmazonDynamoDB::ACTION_ADD,
                    'Value' => array(
                        AmazonDynamoDB::TYPE_NUMBER => '20'
                    )
                )
            )
    ));

    // status code 200 indicates success
    print_r($update_response);

?>

希望这会帮助其他人直到Boto layer2界面赶上......或者有人只是想知道如何在level2中做到这一点: - )

答案 2 :(得分:0)

DynamoDB中没有用于原子计数器的高级功能。但是,您可以使用条件写入功能实现原子计数器。例如,假设您有一个带有字符串哈希键的表,就像这样。

>>> import boto
>>> c = boto.connect_dynamodb()
>>> schema = s.create_schema('id', 's')
>>> counter_table = c.create_table('counter', schema, 5, 5)

现在,您可以向该表写入一个项目,该项目包含名为“n”的属性,其值为零。

>>> n = 0
>>> item = counter_table.new_item('counter', {'n': n})
>>> item.put()

现在,如果我想更新计数器的值,我会执行一个条件写操作,如果它的当前值与我对它的当前值的看法一致,那么它会将'n'的值加到1。

>>> n += 1
>>> item['n'] = n
>>> item.put(expected_value={'n': n-1})

这会将项目中“n”的值设置为1,但前提是DynamoDB中的当前值为零。如果该值已被其他人增加,则写入将失败,然后我需要通过本地计数器递增并再次尝试。

这有点复杂,但所有这些都可以包含在一些代码中,以使其更易于使用。我在SimpleDB上做了类似的事情,你可以在这里找到:

http://www.elastician.com/2010/02/stupid-boto-tricks-2-reliable-counters.html

我应该尝试更新该示例以使用DynamoDB

答案 3 :(得分:0)

我不确定这是一个真正的原子计数器,因为当你增加1的值时,另一个调用可以将数字增加1,这样当你“获得”该值时,它不是值你会期待的。

例如,将代码放入garnaat,标记为已接受的答案,我看到当你把它放在一个帖子中时,它不起作用:

class ThreadClass(threading.Thread):
    def run(self):
        conn = boto.dynamodb.connect_to_region(aws_access_key_id=os.environ['AWS_ACCESS_KEY'], aws_secret_access_key=os.environ['AWS_SECRET_KEY'], region_name='us-east-1')
        t = conn.get_table('zoo_keeper_ids')
        item = t.new_item('counter')
        item.add_attribute('n', 1)
        r = item.save() #- Item has been atomically updated!
        # Uh-Oh! The value may have changed by the time "get_item" is called!
        item = t.get_item('counter') 
        self.counter = item['n']
        logging.critical('Thread has counter: ' + str(self.counter))

tcount = 3
threads = []
for i in range(tcount):
    threads.append(ThreadClass())

# Start running the threads:
for t in threads:
    t.start()

# Wait for all threads to complete:
for t in threads:
    t.join()

#- Now verify all threads have unique numbers:
results = set()
for t in threads:
    results.add(t.counter)

print len(results)
print tcount
if len(results) != tcount:
    print '***Error: All threads do not have unique values!'
else:
    print 'Success!  All threads have unique values!'

注意:如果您希望它真正起作用,请将代码更改为:

def run(self):
    conn = boto.dynamodb.connect_to_region(aws_access_key_id=os.environ['AWS_ACCESS_KEY'], aws_secret_access_key=os.environ['AWS_SECRET_KEY'], region_name='us-east-1')
    t = conn.get_table('zoo_keeper_ids')
    item = t.new_item('counter')
    item.add_attribute('n', 1)
    r = item.save(return_values='ALL_NEW') #- Item has been atomically updated, and you have the correct value without having to do a "get"!
    self.counter = str(r['Attributes']['n'])
    logging.critical('Thread has counter: ' + str(self.counter))

希望这有帮助!

答案 4 :(得分:0)

您要在dynamodb中增加一个值,然后可以使用以下方法来实现:

import boto3
import json
import decimal

class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            if o % 1 > 0:
                return float(o)
            else:
                return int(o)
        return super(DecimalEncoder, self).default(o)

ddb = boto3.resource('dynamodb') 
def get_counter():
    table = ddb.Table(TableName)
    try:
            response = table.update_item(                                                             
            Key={
                'haskey' : 'counterName'
            },
            UpdateExpression="set currentValue = currentValue +  :val",
            ExpressionAttributeValues={
                ':val': decimal.Decimal(1)
            }, 
            ReturnValues="UPDATED_NEW"
        )
        print("UpdateItem succeeded:")
    except Exception as e:
        raise e
    print(response["Attributes"]["currentValue" ])

此实现需要一个额外的计数器表,该表将为您保留最后使用的值。