如何在数据库中使龙卷风请求原子化

How to make a tornado request atomic in the Database

我有一个用 Tornado 异步框架编写的 python 应用程序。当一个 HTTP 请求进来时,这个方法被调用:

@classmethod
def my_method(cls, my_arg1):

    # Do some Database Transaction #1
    x = get_val_from_db_table1(id=1, 'x')
    y = get_val_from_db_table2(id=7, 'y')
    x += x + (2 * y) 

    # Do some Database Transaction #2
    set_val_in_db_table1(id=1, 'x', x)

    return True

这三个数据库操作是相互关联的。这是一个并发应用程序,因此多个此类 HTTP 调用可以同时发生并访问同一个数据库。

出于数据完整性的目的,重要的是调用此方法中的三个数据库操作时没有其他进程读取或写入中间的那些数据库行。

如何确保此方法具有数据库原子性? Tornado 有装饰器吗?

因为你想 运行 这三个数据库操作一个接一个地进行,函数 my_method 必须 非异步.

但这也意味着 my_method 将阻止服务器。你绝对不想要那个。我能想到的一种方法是 运行 在另一个线程中使用此函数。这不会阻止服务器,并且会在操作 运行ning 期间继续接受新请求。而且,由于它将是非异步的,因此可以保证数据库的原子性。

以下是帮助您入门的相关代码:

import concurrent.futures

executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
# Don't set `max_workers` more than 1, because then multiple 
# threads will be able to perform db operations

class MyHandler(...):
    @gen.coroutine
    def get(self):

        yield executor.submit(MyHandler.my_method, my_arg1)
        # above, `yield` is used to wait for 
        # db operations to finish
        # if you don't want to wait and return
        # a response immediately remove the 
        # `yield` keyword

        self.write('Done')

    @classmethod
    def my_method(cls, my_arg1):
        # do db stuff ...
        return True

同步数据库访问

你还没有说明你是如何访问你的数据库的。如果,这很可能,您在 get_val_from_db_table1 和朋友(例如 pymysql) and my_method is blocking (doesn't return control to IO loop) then you block your server (which has implications on performance and responsiveness of your server) but effectively serialise your clients and only one can execute my_method at a time. So in terms of data consistency you don't need to do anything, but generally it's a bad design. You can solve both with @xyres's solution in short term (at cost of keeping in mind thread-safely concerns because most of Tornado's functionality isn't thread-safe)中有同步数据库访问。

异步数据库访问

如果您在 get_val_from_db_table1 和朋友中有异步数据库访问(例如 tornado-mysql) then you can use tornado.locks.Lock。这是一个示例:

from tornado import web, gen, locks, ioloop


_lock = locks.Lock()

def synchronised(coro):
    async def wrapper(*args, **kwargs):  
        async with _lock:
            return await coro(*args, **kwargs)

    return wrapper


class MainHandler(web.RequestHandler):

    async def get(self):
        result = await self.my_method('foo')
        self.write(result)

    @classmethod
    @synchronised
    async def my_method(cls, arg):
        # db access
        await gen.sleep(0.5)
        return 'data set for {}'.format(arg)


if __name__ == '__main__':
    app = web.Application([('/', MainHandler)])
    app.listen(8080)
    ioloop.IOLoop.current().start()

注意上面说的是正常的单进程Tornado应用。如果你使用tornado.process.fork_processes,那么你只能使用multiprocessing.Lock