Fixed #27683 -- Made MySQL default to the read committed isolation level.

Thanks Shai Berger for test help and Adam Johnson for review.
This commit is contained in:
Tim Graham 2017-02-01 15:34:17 -05:00 committed by GitHub
parent c4e18bb1ce
commit 924af638e4
5 changed files with 40 additions and 13 deletions

View File

@ -217,7 +217,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
kwargs['client_flag'] = CLIENT.FOUND_ROWS kwargs['client_flag'] = CLIENT.FOUND_ROWS
# Validate the transaction isolation level, if specified. # Validate the transaction isolation level, if specified.
options = settings_dict['OPTIONS'].copy() options = settings_dict['OPTIONS'].copy()
isolation_level = options.pop('isolation_level', None) isolation_level = options.pop('isolation_level', 'read committed')
if isolation_level: if isolation_level:
isolation_level = isolation_level.lower() isolation_level = isolation_level.lower()
if isolation_level not in self.isolation_levels: if isolation_level not in self.isolation_levels:

View File

@ -449,8 +449,14 @@ this entry are the four standard isolation levels:
* ``'serializable'`` * ``'serializable'``
or ``None`` to use the server's configured isolation level. However, Django or ``None`` to use the server's configured isolation level. However, Django
works best with read committed rather than MySQL's default, repeatable read. works best with and defaults to read committed rather than MySQL's default,
Data loss is possible with repeatable read. repeatable read. Data loss is possible with repeatable read.
.. versionchanged:: 2.0
In older versions, the MySQL database backend defaults to using the
database's isolation level (which defaults to repeatable read) rather
than read committed.
.. _transaction isolation level: https://dev.mysql.com/doc/refman/en/innodb-transaction-isolation-levels.html .. _transaction isolation level: https://dev.mysql.com/doc/refman/en/innodb-transaction-isolation-levels.html

View File

@ -227,6 +227,15 @@ The end of upstream support for Oracle 11.2 is Dec. 2020. Django 1.11 will be
supported until April 2020 which almost reaches this date. Django 2.0 supported until April 2020 which almost reaches this date. Django 2.0
officially supports Oracle 12.1+. officially supports Oracle 12.1+.
Default MySQL isolation level is read committed
-----------------------------------------------
MySQL's default isolation level, repeatable read, may cause data loss in
typical Django usage. To prevent that and for consistency with other databases,
the default isolation level is now read committed. You can use the
:setting:`DATABASES` setting to :ref:`use a different isolation level
<mysql-isolation-level>`, if needed.
:attr:`AbstractUser.last_name <django.contrib.auth.models.User.last_name>` ``max_length`` increased to 150 :attr:`AbstractUser.last_name <django.contrib.auth.models.User.last_name>` ``max_length`` increased to 150
---------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------------

View File

@ -70,6 +70,15 @@ class MySQLTests(TestCase):
self.isolation_values[self.other_isolation_level] self.isolation_values[self.other_isolation_level]
) )
def test_default_isolation_level(self):
# If not specified in settings, the default is read committed.
with get_connection() as new_connection:
new_connection.settings_dict['OPTIONS'].pop('isolation_level', None)
self.assertEqual(
self.get_isolation_level(new_connection),
self.isolation_values[self.read_committed]
)
def test_isolation_level_validation(self): def test_isolation_level_validation(self):
new_connection = connection.copy() new_connection = connection.copy()
new_connection.settings_dict['OPTIONS']['isolation_level'] = 'xxx' new_connection.settings_dict['OPTIONS']['isolation_level'] = 'xxx'

View File

@ -375,18 +375,17 @@ class AtomicMySQLTests(TransactionTestCase):
@skipIf(threading is None, "Test requires threading") @skipIf(threading is None, "Test requires threading")
def test_implicit_savepoint_rollback(self): def test_implicit_savepoint_rollback(self):
"""MySQL implicitly rolls back savepoints when it deadlocks (#22291).""" """MySQL implicitly rolls back savepoints when it deadlocks (#22291)."""
Reporter.objects.create(id=1)
Reporter.objects.create(id=2)
other_thread_ready = threading.Event() main_thread_ready = threading.Event()
def other_thread(): def other_thread():
try: try:
with transaction.atomic(): with transaction.atomic():
Reporter.objects.create(id=1, first_name="Tintin") Reporter.objects.select_for_update().get(id=1)
other_thread_ready.set() main_thread_ready.wait()
# We cannot synchronize the two threads with an event here # 1) This line locks... (see below for 2)
# because the main thread locks. Sleep for a little while.
time.sleep(1)
# 2) ... and this line deadlocks. (see below for 1)
Reporter.objects.exclude(id=1).update(id=2) Reporter.objects.exclude(id=1).update(id=2)
finally: finally:
# This is the thread-local connection, not the main connection. # This is the thread-local connection, not the main connection.
@ -394,14 +393,18 @@ class AtomicMySQLTests(TransactionTestCase):
other_thread = threading.Thread(target=other_thread) other_thread = threading.Thread(target=other_thread)
other_thread.start() other_thread.start()
other_thread_ready.wait()
with self.assertRaisesMessage(OperationalError, 'Deadlock found'): with self.assertRaisesMessage(OperationalError, 'Deadlock found'):
# Double atomic to enter a transaction and create a savepoint. # Double atomic to enter a transaction and create a savepoint.
with transaction.atomic(): with transaction.atomic():
with transaction.atomic(): with transaction.atomic():
# 1) This line locks... (see above for 2) Reporter.objects.select_for_update().get(id=2)
Reporter.objects.create(id=1, first_name="Tintin") main_thread_ready.set()
# The two threads can't be synchronized with an event here
# because the other thread locks. Sleep for a little while.
time.sleep(1)
# 2) ... and this line deadlocks. (see above for 1)
Reporter.objects.exclude(id=2).update(id=1)
other_thread.join() other_thread.join()