简体   繁体   English

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

[英]Update DynamoDB Atomic Counter with Python / Boto

I am trying to update an atomic count counter with Python Boto 2.3.0, but can find no documentation for the operation.我正在尝试使用 Python Boto 2.3.0 更新原子计数计数器,但找不到该操作的文档。

It seems there is no direct interface, so I tried to go to "raw" updates using the layer1 interface, but I was unable to complete even a simple update.似乎没有直接的接口,所以我尝试使用 layer1 接口进行“原始”更新,但我什至无法完成一个简单的更新。

I tried the following variations but all with no luck我尝试了以下变化,但都没有运气

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"}}})      

They all produce the same error:它们都产生相同的错误:

  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'}

I also investigated the API docs here but they were pretty spartan.我还调查了此处的 API 文档但它们非常简陋。

I have done a lot of searching and fiddling, and the only thing I have left is to use the PHP API and dive into the code to find where it "formats" the JSON body, but that is a bit of a pain.我已经做了很多搜索和摆弄,我唯一剩下的就是使用 PHP API 并深入代码以找到它“格式化”JSON 主体的位置,但这有点麻烦。 Please save me from that pain!请救我脱离那痛苦!

Sorry, I misunderstood what you were looking for.抱歉,我误解了您要查找的内容。 You can accomplish this via layer2 although there is a small bug that needs to be addressed.您可以通过 layer2 完成此操作,尽管有一个需要解决的小错误。 Here's some Layer2 code:这是一些 Layer2 代码:

>>> 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}

This results in the same over-the-wire request as you are performing in your Layer1 code, as shown by the following debug output.这会产生与您在 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"}}}

If you want to avoid the initial GetItem call, you could do this instead:如果你想避免最初的 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}

Which will update the item if it already exists or create it if it doesn't yet exist.如果项目已经存在,它将更新该项目,如果尚不存在则创建它。

For those looking for the answer I have found it.对于那些寻找答案的人,我已经找到了。 First IMPORTANT NOTE, I am currently unaware of what is going on BUT for the moment, to get a layer1 instance I have had to do the following:第一个重要说明,我目前不知道发生了什么,但是为了获得 layer1 实例,我必须执行以下操作:

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) 

Essentially instantiating a layer2 FIRST and THEN a layer 1. Maybe Im doing something stupid but at this point Im just happy to have it working.... I'll sort the details later.本质上首先实例化第 2 层,然后实例化第 1 层。也许我在做一些愚蠢的事情,但此时我很高兴让它工作......我稍后会对细节进行排序。 THEN...to actually do the atomic update call:然后...实际执行原子更新调用:

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

Note in the example above Dynamo will ADD 20 to what ever the current value is and this operation will be atomic meaning other operations happening at the "same time" will be correctly "scheduled" to happen after the new value has been established as +20 OR before this operation is executed.请注意,在上面的示例中,Dynamo 会将当前值加 20,并且此操作将是原子的,这意味着在“同时”发生的其他操作将被正确“安排”在新值被确定为 +20 后发生OR 在执行此操作之前。 Either way the desired effect will be accomplished.无论哪种方式,都将达到预期的效果。

Be certain to do this on the instance of the layer1 connection as the layer2 will throw errors given it expects a different set of parameter types.一定要在 layer1 连接的实例上这样做,因为 layer2 会抛出错误,因为它需要一组不同的参数类型。

Thats all there is to it!!!!这里的所有都是它的!!!! Just so folks know, I figured this out using the PHP SDK.只是让人们知道,我使用 PHP SDK 解决了这个问题。 Takes a very short time to install and set up AND THEN when you do a call, the debug data will actually show you the format of the HTTP request body so you will be able to copy/model your layer1 parameters after the example.安装和设置需要很短的时间,然后当您进行调用时,调试数据实际上会向您显示 HTTP 请求正文的格式,因此您将能够在示例之后复制/建模您的 layer1 参数。 Here is the code I used to do the atomic update in PHP:这是我用来在 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);

?>

Hopefully this will help other up until the Boto layer2 interface catches up...or someone simply figures out how to do it in level2 :-)希望这会对其他人有所帮助,直到 Boto layer2 界面赶上......或者有人只是想出如何在 level2 中做到这一点:-)

I'm not sure this is truly an atomic counter, since when you increment the value of 1, another call call could increment the number by 1, so that when you "get" the value, it is not the value that you would expect.我不确定这是否真的是一个原子计数器,因为当您增加 1 的值时,另一个调用可能会将数字增加 1,因此当您“获取”该值时,它不是您期望的值.

For instance, putting the code by garnaat, which is marked as the accepted answer, I see that when you put it in a thread, it does not work:比如把代码放在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!'

Note: If you want this to truly work, change the code to this:注意:如果您希望这真正起作用,请将代码更改为:

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))

Hope this helps!希望这可以帮助!

There is no high-level function in DynamoDB for atomic counters. DynamoDB 中没有用于原子计数器的高级函数。 However, you can implement an atomic counter using the conditional write feature.但是,您可以使用条件写入功能实现原子计数器。 For example, let's say you a table with an string hash key called like this.例如,假设您有一个表,它有一个字符串哈希键,这样调用。

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

You now write an item to that table that includes an attribute called 'n' whose value is zero.现在,您将一个项目写入该表,其中包含一个名为“n”的属性,其值为 0。

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

Now, if I want to update the value of my counter, I would perform a conditional write operation that will bump the value of 'n' to 1 iff it's current value agrees with my idea of it's current value.现在,如果我想更新我的计数器的值,我将执行一个有条件的写操作,如果它的当前值与我对它的当前值的想法一致,则将“n”的值提高到 1。

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

This will set the value of 'n' in the item to 1 but only if the current value in the DynamoDB is zero.这会将项目中“n”的值设置为 1,但前提是 DynamoDB 中的当前值为零。 If the value was already incremented by someone else, the write would fail and I would then need to increment by local counter and try again.如果该值已被其他人递增,则写入将失败,然后我需要通过本地计数器递增并重试。

This is kind of complicated but all of this could be wrapped up in some code to make it much simpler to use.这有点复杂,但所有这些都可以包含在一些代码中,以使其更易于使用。 I did a similar thing for SimpleDB that you can find here:我为 SimpleDB 做了类似的事情,你可以在这里找到:

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

I should probably try to update that example to use DynamoDB我可能应该尝试更新该示例以使用 DynamoDB

You want to increment a value in dynamodb then you can achieve this by using:您想在 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" ])

This implementetion needs a extra counter table that will just keep the last used value for you.此实现需要一个额外的计数器表,它只会为您保留上次使用的值。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM