diff --git a/core/lib/Drupal/Core/Command/DbDumpCommand.php b/core/lib/Drupal/Core/Command/DbDumpCommand.php
index ac544d068bddcff2347828a8d02f54594797eacb..0f3f610d7bfbe13bbe31b99c022ef52f318a66e0 100644
--- a/core/lib/Drupal/Core/Command/DbDumpCommand.php
+++ b/core/lib/Drupal/Core/Command/DbDumpCommand.php
@@ -410,9 +410,15 @@ protected function getTemplate() {
 use Drupal\Core\Database\Database;
 
 $connection = Database::getConnection();
+// Ensure any tables with a serial column with a value of 0 are created as
+// expected.
+$sql_mode = $connection->query("SELECT @@sql_mode;")->fetchField();
+$connection->query("SET sql_mode = '$sql_mode,NO_AUTO_VALUE_ON_ZERO'");
 
 {{TABLES}}
 
+// Reset the SQL mode.
+$connection->query("SET sql_mode = '$sql_mode'");
 ENDOFSCRIPT;
     return $script;
   }
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index ba319da7a1a05aa4c3f8bede70822d183ed8417b..208aa422e5f9b97c69880fcfa9889628ed6ff41d 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -1057,7 +1057,14 @@ protected function mapToStorageRecord(ContentEntityInterface $entity, $table_nam
         // SQL database drivers.
         // @see https://www.drupal.org/node/2279395
         $value = SqlContentEntityStorageSchema::castValue($definition->getSchema()['columns'][$column_name], $value);
-        if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
+        $empty_serial = empty($value) && $this->isColumnSerial($table_name, $schema_name);
+        // The user entity is a very special case where the ID field is a serial
+        // but we need to insert a row with an ID of 0 to represent the
+        // anonymous user.
+        // @todo https://drupal.org/i/3222123 implement a generic fix for all
+        //   entity types.
+        $user_zero = $this->entityTypeId === 'user' && $value === 0;
+        if (!$empty_serial || $user_zero) {
           $record->$schema_name = $value;
         }
       }
diff --git a/core/modules/user/src/UserStorage.php b/core/modules/user/src/UserStorage.php
index a2adfe911858085c96ba72442b8e4fd9a75fbd00..5ca52d5e99c8430b5ed500f53530e3656fd62c89 100644
--- a/core/modules/user/src/UserStorage.php
+++ b/core/modules/user/src/UserStorage.php
@@ -18,21 +18,24 @@ class UserStorage extends SqlContentEntityStorage implements UserStorageInterfac
    * {@inheritdoc}
    */
   protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
-    // The anonymous user account is saved with the fixed user ID of 0.
-    // Therefore we need to check for NULL explicitly.
-    if ($entity->id() === NULL) {
-      $entity->uid->value = $this->database->nextId($this->database->query('SELECT MAX([uid]) FROM {' . $this->getBaseTable() . '}')->fetchField());
-      $entity->enforceIsNew();
+    // The anonymous user account is saved with the fixed user ID of 0. MySQL
+    // does not support inserting an ID of 0 into serial field unless the SQL
+    // mode is set to NO_AUTO_VALUE_ON_ZERO.
+    // @todo https://drupal.org/i/3222123 implement a generic fix for all entity
+    //   types.
+    if ($entity->id() === 0) {
+      $database = \Drupal::database();
+      if ($database->databaseType() === 'mysql') {
+        $sql_mode = $database->query("SELECT @@sql_mode;")->fetchField();
+        $database->query("SET sql_mode = '$sql_mode,NO_AUTO_VALUE_ON_ZERO'");
+      }
     }
-    return parent::doSaveFieldItems($entity, $names);
-  }
+    parent::doSaveFieldItems($entity, $names);
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function isColumnSerial($table_name, $schema_name) {
-    // User storage does not use a serial column for the user id.
-    return $table_name == $this->revisionTable && $schema_name == $this->revisionKey;
+    // Reset the SQL mode if we've changed it.
+    if (isset($sql_mode, $database)) {
+      $database->query("SET sql_mode = '$sql_mode'");
+    }
   }
 
   /**
diff --git a/core/modules/user/src/UserStorageSchema.php b/core/modules/user/src/UserStorageSchema.php
index 6ac14d26d89e453bcaa2529672288b84cf33980c..d172fb3a6af9f3653654250e9749ad53a926d57a 100644
--- a/core/modules/user/src/UserStorageSchema.php
+++ b/core/modules/user/src/UserStorageSchema.php
@@ -26,16 +26,6 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
     return $schema;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function processIdentifierSchema(&$schema, $key) {
-    // The "users" table does not use serial identifiers.
-    if ($key != $this->entityType->getKey('id')) {
-      parent::processIdentifierSchema($schema, $key);
-    }
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/user/tests/src/Functional/UidUpdateToSerialTest.php b/core/modules/user/tests/src/Functional/UidUpdateToSerialTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a5e9e5e40d8ec38f8cb6b3397c6b63d8c0a231ec
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/UidUpdateToSerialTest.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\Tests\user\Functional;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+// cSpell:ignore refobjid regclass attname attrelid attnum refobjsubid objid
+// cSpell:ignore classid
+
+/**
+ * Tests user_update_9301().
+ *
+ * @group user
+ */
+class UidUpdateToSerialTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles[] = __DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.0.0.bare.standard.php.gz';
+  }
+
+  /**
+   * Tests user_update_9301().
+   */
+  public function testDatabaseLoaded() {
+    $key_value_store = \Drupal::keyValue('entity.storage_schema.sql');
+    $id_schema = $key_value_store->get('user.field_schema_data.uid', []);
+    $this->assertSame('int', $id_schema['users']['fields']['uid']['type']);
+
+    $this->runUpdates();
+
+    $key_value_store = \Drupal::keyValue('entity.storage_schema.sql');
+    $id_schema = $key_value_store->get('user.field_schema_data.uid', []);
+    $this->assertSame('serial', $id_schema['users']['fields']['uid']['type']);
+
+    $connection = \Drupal::database();
+    if ($connection->driver() == 'pgsql') {
+      $seq_name = $connection->makeSequenceName('users', 'uid');
+      $seq_owner = $connection->query("SELECT d.refobjid::regclass as table_name, a.attname as field_name
+        FROM pg_depend d
+        JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
+        WHERE d.objid = :seq_name::regclass
+        AND d.refobjsubid > 0
+        AND d.classid = 'pg_class'::regclass", [':seq_name' => 'public.' . $seq_name])->fetchObject();
+      $this->assertEquals($connection->tablePrefix('users') . 'users', $seq_owner->table_name);
+      $this->assertEquals('uid', $seq_owner->field_name);
+
+      $seq_last_value = $connection->query("SELECT last_value FROM $seq_name")->fetchField();
+      $maximum_uid = $connection->query('SELECT MAX([uid]) FROM {users}')->fetchField();
+      $this->assertEquals($maximum_uid + 1, $seq_last_value);
+    }
+  }
+
+}
diff --git a/core/modules/user/user.install b/core/modules/user/user.install
index 8375992ce7e63480cb6a800c307a12386dd8ec66..c9aa52936ae765a44e354c835dd3a3501cfe0171 100644
--- a/core/modules/user/user.install
+++ b/core/modules/user/user.install
@@ -5,6 +5,8 @@
  * Install, update and uninstall functions for the user module.
  */
 
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+
 /**
  * Implements hook_schema().
  */
@@ -97,3 +99,38 @@ function user_install() {
 function user_update_last_removed() {
   return 8100;
 }
+
+/**
+ * Change the users table to use an serial uid field.
+ */
+function user_update_9301(&$sandbox) {
+  if (!\Drupal::entityTypeManager()->getStorage('user') instanceof SqlContentEntityStorage) {
+    return t('The user entity storage is not using an SQL storage, update skipped.');
+  }
+
+  $connection = \Drupal::database();
+  $connection->schema()->dropPrimaryKey('users');
+  if ($connection->databaseType() === 'mysql') {
+    $sql_mode = $connection->query("SELECT @@sql_mode;")->fetchField();
+    $connection->query("SET sql_mode = '$sql_mode,NO_AUTO_VALUE_ON_ZERO'");
+  }
+  $connection->schema()->changeField('users', 'uid', 'uid', ['type' => 'serial', 'not null' => TRUE], ['primary key' => ['uid']]);
+  if (isset($sql_mode)) {
+    $connection->query("SET sql_mode = '$sql_mode'");
+  }
+
+  // Update the last installed schema to reflect the change of field type.
+  $installed_storage_schema = \Drupal::keyValue('entity.storage_schema.sql');
+  $field_schema_data = $installed_storage_schema->get('user.field_schema_data.uid');
+  $field_schema_data['users']['fields']['uid']['type'] = 'serial';
+  $installed_storage_schema->set('user.field_schema_data.uid', $field_schema_data);
+
+  // The new PostgreSQL sequence for the uid field needs to start with the last
+  // used user ID + 1 and the sequence must be owned by uid field.
+  // @todo https://drupal.org/i/3028706 implement a generic fix.
+  if ($connection->driver() == 'pgsql') {
+    $maximum_uid = $connection->query('SELECT MAX([uid]) FROM {users}')->fetchField();
+    $seq = $connection->makeSequenceName('users', 'uid');
+    $connection->query("ALTER SEQUENCE " . $seq . " RESTART WITH " . ($maximum_uid + 1) . " OWNED BY {users}.uid");
+  }
+}
diff --git a/core/tests/Drupal/Tests/UpdatePathTestTrait.php b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
index 0795d9417a1cc5a92db1eb12e4cc6c3bcecf58e4..ecdb20b484696ff45ec57fc5fe9fb3438e8247f2 100644
--- a/core/tests/Drupal/Tests/UpdatePathTestTrait.php
+++ b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests;
 
+use Drupal\Core\Database\Database;
 use Drupal\Core\Url;
 
 /**
@@ -107,6 +108,15 @@ protected function runUpdates($update_url = NULL) {
         $this->kernel->updateModules($module_handler_list, $module_handler_list);
       }
 
+      // Close any open database connections. This allows DB drivers that store
+      // static information to refresh it in the update runner.
+      // @todo https://drupal.org/i/3222121 consider doing this in
+      //   \Drupal\Core\DrupalKernel::initializeContainer() for container
+      //   rebuilds.
+      foreach (Database::getAllConnectionInfo() as $key => $info) {
+        Database::closeConnection(NULL, $key);
+      }
+
       // If we have successfully clicked 'Apply pending updates' then we need to
       // clear the caches in the update test runner as this has occurred as part
       // of the updates.