Optimistic “locking” with Django transactions

2019-06-08 23:14发布

问题:

I have a function fn() that needs to atomically do some database work that relies on some set of data not changing during its execution (true most of the time).

What is the correct way to implement this in Django? Basically I'd like to do something like this:

def ensure_fn_runs_successfully():
  # While commit unsuccessful, keep trying
  while not fn():
    pass

@transaction.atomic
def fn():
  data = read_data_that_must_not_change()

  ... do some operations with the data and perform database operations ...

  # Assume it returns true if commit was successful, otherwise false
  return commit_only_if_the_data_actually_didnt_change()

@transaction.atomic takes care of part of the problem (database should only ever see the state before fn runs or after fn runs successfully), but I'm not sure if there exists a good primitive to do the commit_only_if_the_data_actually_didnt_change, and retrying the operation if it fails.

To verify the data didn't change, it would be enough to just check the count of items returned for a query is the same as it was at the beginning of the function; however, I don't know if there are any primitives that let you make the check and commit decision at the same time / without race condition.

回答1:

If you are in a transaction block, the only thing that can change the data that you are reading are other operations within that same transaction block. So as long as fn() does not make any changes to data, you are guaranteed that the data will not change unless fn() changes it. That is the problem that transactions are meant to solve.

If data can change within the confines of fn() just keep track of the places where it changes or keep track of the final result.

@transaction.atomic
def fn():
  data = read_data_that_must_not_change()
  original_data = copy.copy(data)
  ... do some operations with the data and perform database operations ...

  # Assume it returns true if commit was successful, otherwise false
  if data != original_data:
    raise Exception('Oh no!  Data changed!') 
    # raising in exception is how you prevent transaction.atomic
    # from committing
  return commit_only_if_the_data_actually_didnt_change()

And then handle the exception in you while loop like so:

while True:
    try:
        fn()
        break
    except:
        time.sleep(10) # ten second cool off
        pass