diff --git a/core/includes/session.inc b/core/includes/session.inc
index b9b8fbc..9ffd3d1 100644
--- a/core/includes/session.inc
+++ b/core/includes/session.inc
@@ -105,9 +105,6 @@ function _drupal_session_read($sid) {
   // We found the client's session record and they are an authenticated,
   // active user.
   if ($user && $user->uid > 0 && $user->status == 1) {
-    // This is done to unserialize the data member of $user.
-    $user->data = unserialize($user->data);
-
     // Add roles element to $user.
     $user->roles = array();
     $user->roles[DRUPAL_AUTHENTICATED_RID] = DRUPAL_AUTHENTICATED_RID;
diff --git a/core/modules/block/block.install b/core/modules/block/block.install
index ee72e0f..2f9eaa6 100644
--- a/core/modules/block/block.install
+++ b/core/modules/block/block.install
@@ -233,6 +233,10 @@ function block_update_dependencies() {
   $dependencies['block'][8002] = array(
     'user' => 8002,
   );
+  // Migrate users.data after User module prepared the tables.
+  $dependencies['block'][8004] = array(
+    'user' => 8011,
+  );
   return $dependencies;
 }
 
@@ -324,6 +328,23 @@ function block_update_8003() {
 }
 
 /**
+ * Migrate {users}.data into {users_data}.
+ */
+function block_update_8004() {
+  $query = db_select('_d7_users_data', 'ud');
+  $query->addField('ud', 'uid');
+  $query->addExpression("'block'", 'module');
+  $query->addExpression("'block'", 'name');
+  // Take over the extracted and serialized value in {_d7_users_data} as-is.
+  $query->addField('ud', 'value');
+  $query->addExpression('1', 'serialized');
+
+  db_insert('users_data')
+    ->from($query)
+    ->execute();
+}
+
+/**
  * @} End of "addtogroup updates-7.x-to-8.x".
  * The next series of updates should start at 9000.
  */
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index 2978146..698ae80 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -599,6 +599,7 @@ function block_form_user_profile_form_alter(&$form, &$form_state) {
   $account = $form_state['controller']->getEntity($form_state);
   $rids = array_keys($account->roles);
   $result = db_query("SELECT DISTINCT b.* FROM {block} b LEFT JOIN {block_role} r ON b.module = r.module AND b.delta = r.delta WHERE b.status = 1 AND b.custom <> 0 AND (r.rid IN (:rids) OR r.rid IS NULL) ORDER BY b.weight, b.module", array(':rids' => $rids));
+  $account_data = user_data_get('block', $account->uid, 'block');
 
   $blocks = array();
   foreach ($result as $block) {
@@ -607,7 +608,7 @@ function block_form_user_profile_form_alter(&$form, &$form_state) {
       $blocks[$block->module][$block->delta] = array(
         '#type' => 'checkbox',
         '#title' => check_plain($data[$block->delta]['info']),
-        '#default_value' => isset($account->data['block'][$block->module][$block->delta]) ? $account->data['block'][$block->module][$block->delta] : ($block->custom == 1),
+        '#default_value' => isset($account_data[$block->module][$block->delta]) ? $account_data[$block->module][$block->delta] : ($block->custom == 1),
       );
     }
   }
@@ -639,11 +640,11 @@ function block_field_extra_fields() {
 }
 
 /**
- * Implements hook_user_presave().
+ * Implements hook_user_update().
  */
-function block_user_presave($account) {
+function block_user_update($account) {
   if (isset($account->block)) {
-    $account->data['block'] = $account->block;
+    user_data_set('block', $account->uid, 'block', $account->block);
   }
 }
 
@@ -805,6 +806,10 @@ function block_block_list_alter(&$blocks) {
     $block_langcodes[$record->module][$record->delta][$record->type][$record->langcode] = TRUE;
   }
 
+  if ($user->uid) {
+    $user_data = user_data_get('block', $user->uid, 'block');
+  }
+
   foreach ($blocks as $key => $block) {
     if (!isset($block->theme) || !isset($block->status) || $block->theme != $theme_key || $block->status != 1) {
       // This block was added by a contrib module, leave it in the list.
@@ -822,8 +827,8 @@ function block_block_list_alter(&$blocks) {
 
     // Use the user's block visibility setting, if necessary.
     if ($block->custom != BLOCK_CUSTOM_FIXED) {
-      if ($user->uid && isset($user->data['block'][$block->module][$block->delta])) {
-        $enabled = $user->data['block'][$block->module][$block->delta];
+      if ($user->uid && isset($user_data[$block->module][$block->delta])) {
+        $enabled = $user_data[$block->module][$block->delta];
       }
       else {
         $enabled = ($block->custom == BLOCK_CUSTOM_ENABLED);
diff --git a/core/modules/contact/contact.install b/core/modules/contact/contact.install
index f956242..bbffc2c 100644
--- a/core/modules/contact/contact.install
+++ b/core/modules/contact/contact.install
@@ -84,6 +84,17 @@ function contact_install() {
 }
 
 /**
+ * Implements hook_update_dependencies().
+ */
+function contact_update_dependencies() {
+  // Migrate users.data after User module prepared the tables.
+  $dependencies['contact'][8001] = array(
+    'user' => 8011,
+  );
+  return $dependencies;
+}
+
+/**
  * Moves contact setting from variable to config.
  *
  * @ingroup config_upgrade
@@ -95,3 +106,25 @@ function contact_update_8000() {
     'contact_threshold_window' => 'flood.interval',
   ));
 }
+
+/**
+ * Migrate {users}.data into {users_data}.
+ */
+function contact_update_8001() {
+  // Migrate in two passes; once all users who disabled their personal contact
+  // form, and once who enabled it.
+  foreach (array(serialize('0'), serialize('1')) as $enabled => $serialized_setting) {
+    $query = db_select('_d7_users_data', 'ud');
+    $query->addField('ud', 'uid');
+    $query->addExpression("'contact'", 'module');
+    $query->addExpression("'enabled'", 'name');
+    $query->condition('value', $serialized_setting);
+    $query->addExpression($enabled, 'value');
+    $query->addExpression(0, 'serialized');
+
+    db_insert('users_data')
+      ->from($query)
+      ->execute();
+  }
+}
+
diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module
index 985abbd..06ed55d 100644
--- a/core/modules/contact/contact.module
+++ b/core/modules/contact/contact.module
@@ -132,14 +132,20 @@ function _contact_personal_tab_access($account) {
     return TRUE;
   }
 
-  // If the requested user has disabled their contact form, or this preference
-  // has not yet been saved, do not allow users to contact them.
-  if (empty($account->data['contact'])) {
+  // If requested user has been blocked, do not allow users to contact them.
+  if (empty($account->status)) {
     return FALSE;
   }
 
-  // If requested user has been blocked, do not allow users to contact them.
-  if (empty($account->status)) {
+  // If the requested user has disabled their contact form, do not allow users
+  // to contact them.
+  $account_data = user_data_get('contact', $account->uid, 'enabled');
+  if (isset($account_data) && empty($account_data)) {
+    return FALSE;
+  }
+  // If the requested user did not save a preference yet, deny access if the
+  // configured default is disabled.
+  elseif (!config('contact.settings')->get('user_default_enabled')) {
     return FALSE;
   }
 
@@ -222,19 +228,22 @@ function contact_form_user_profile_form_alter(&$form, &$form_state) {
     '#collapsible' => TRUE,
   );
   $account = $form_state['controller']->getEntity($form_state);
+  $account_data = user_data_get('contact', $account->uid, 'enabled');
   $form['contact']['contact'] = array(
     '#type' => 'checkbox',
     '#title' => t('Personal contact form'),
-    '#default_value' => !empty($account->data['contact']) ? $account->data['contact'] : FALSE,
+    '#default_value' => isset($account_data) ? $account_data : config('contact.settings')->get('user_default_enabled'),
     '#description' => t('Allow other users to contact you via a <a href="@url">personal contact form</a> which keeps your e-mail address hidden. Note that some privileged users such as site administrators are still able to contact you even if you choose to disable this feature.', array('@url' => url("user/$account->uid/contact"))),
   );
 }
 
 /**
- * Implements hook_user_presave().
+ * Implements hook_user_update().
  */
-function contact_user_presave($account) {
-  $account->data['contact'] = isset($account->contact) ? $account->contact : config('contact.settings')->get('user_default_enabled');
+function contact_user_update($account) {
+  if (isset($account->contact)) {
+    user_data_set('contact', $account->uid, 'enabled', (int) $account->contact);
+  }
 }
 
 /**
diff --git a/core/modules/openid/lib/Drupal/openid/Tests/OpenIDRegistrationTest.php b/core/modules/openid/lib/Drupal/openid/Tests/OpenIDRegistrationTest.php
index debb9cd..51c2160 100644
--- a/core/modules/openid/lib/Drupal/openid/Tests/OpenIDRegistrationTest.php
+++ b/core/modules/openid/lib/Drupal/openid/Tests/OpenIDRegistrationTest.php
@@ -77,7 +77,6 @@ function testRegisterUserWithEmailVerification() {
     $this->assertEqual($user->mail, 'john@example.com', 'User was registered with right email address.');
     $this->assertEqual($user->timezone, 'Europe/London', 'User was registered with right timezone.');
     $this->assertEqual($user->preferred_langcode, 'pt', 'User was registered with right language.');
-    $this->assertFalse($user->data, 'No additional user info was saved.');
 
     $this->submitLoginForm($identity);
     $this->assertRaw(t('You must validate your email address for this account before logging in via OpenID.'));
@@ -126,7 +125,6 @@ function testRegisterUserWithoutEmailVerification() {
     $this->assertEqual($user->mail, 'john@example.com', 'User was registered with right email address.');
     $this->assertEqual($user->timezone, 'Europe/London', 'User was registered with right timezone.');
     $this->assertEqual($user->preferred_langcode, 'pt-br', 'User was registered with right language.');
-    $this->assertFalse($user->data, 'No additional user info was saved.');
 
     $this->drupalLogout();
 
@@ -171,7 +169,6 @@ function testRegisterUserWithInvalidSreg() {
     $user = user_load_by_name('john');
     $this->assertTrue($user, 'User was registered with right username.');
     $this->assertEqual($user->preferred_langcode, language_default()->langcode, 'User language is site default.');
-    $this->assertFalse($user->data, 'No additional user info was saved.');
 
     // Follow the one-time login that was sent in the welcome e-mail.
     $this->drupalGet($reset_url);
@@ -211,7 +208,6 @@ function testRegisterUserWithoutSreg() {
     $user = user_load_by_name('john');
     $this->assertTrue($user, 'User was registered with right username.');
     $this->assertEqual($user->preferred_langcode, language_default()->langcode, 'User language is site default.');
-    $this->assertFalse($user->data, 'No additional user info was saved.');
 
     // Follow the one-time login that was sent in the welcome e-mail.
     $this->drupalGet($reset_url);
diff --git a/core/modules/overlay/overlay.install b/core/modules/overlay/overlay.install
index 2df860b..a2ef679 100644
--- a/core/modules/overlay/overlay.install
+++ b/core/modules/overlay/overlay.install
@@ -17,3 +17,47 @@ function overlay_enable() {
     $_SESSION['overlay_enable_redirect'] = 1;
   }
 }
+
+/**
+ * Implements hook_update_dependencies().
+ */
+function overlay_update_dependencies() {
+  // Migrate users.data after User module prepared the tables.
+  $dependencies['overlay'][8000] = array(
+    'user' => 8011,
+  );
+  return $dependencies;
+}
+
+/**
+ * Migrate {users}.data into {users_data}.
+ */
+function overlay_update_8000() {
+  // Migrate 'overlay' in two passes; once all users who disabled overlay, and
+  // once who enabled it.
+  foreach (array(serialize('0'), serialize('1')) as $enabled => $serialized_setting) {
+    $query = db_select('_d7_users_data', 'ud');
+    $query->addField('ud', 'uid');
+    $query->addExpression("'overlay'", 'module');
+    $query->addExpression("'enabled'", 'name');
+    $query->condition('value', $serialized_setting);
+    $query->addExpression($enabled, 'value');
+    $query->addExpression(0, 'serialized');
+
+    db_insert('users_data')
+      ->from($query)
+      ->execute();
+  }
+  // Migrate 'overlay_message_dismissed'.
+  $query = db_select('_d7_users_data', 'ud');
+  $query->addField('ud', 'uid');
+  $query->addExpression("'overlay'", 'module');
+  $query->addExpression("'message_dismissed'", 'name');
+  $query->condition('value', serialize('1'));
+  $query->addExpression(1, 'value');
+  $query->addExpression(0, 'serialized');
+
+  db_insert('users_data')
+    ->from($query)
+    ->execute();
+}
diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module
index 6d78e43..3b0e7bd 100644
--- a/core/modules/overlay/overlay.module
+++ b/core/modules/overlay/overlay.module
@@ -87,6 +87,7 @@ function overlay_theme() {
 function overlay_form_user_profile_form_alter(&$form, &$form_state) {
   $account = $form_state['controller']->getEntity($form_state);
   if (user_access('access overlay', $account)) {
+    $account_data = user_data_get('overlay', $account->uid, 'enabled');
     $form['overlay_control'] = array(
       '#type' => 'fieldset',
       '#title' => t('Administrative overlay'),
@@ -97,7 +98,7 @@ function overlay_form_user_profile_form_alter(&$form, &$form_state) {
       '#type' => 'checkbox',
       '#title' => t('Use the overlay for administrative pages.'),
       '#description' => t('Show administrative pages on top of the page you started from.'),
-      '#default_value' => isset($account->data['overlay']) ? $account->data['overlay'] : 1,
+      '#default_value' => isset($account_data) ? $account_data : 1,
     );
   }
 }
@@ -107,7 +108,7 @@ function overlay_form_user_profile_form_alter(&$form, &$form_state) {
  */
 function overlay_user_presave($account) {
   if (isset($account->overlay)) {
-    $account->data['overlay'] = $account->overlay;
+    user_data_set('overlay', $account->uid, 'enabled', $account->overlay);
   }
 }
 
@@ -126,7 +127,8 @@ function overlay_init() {
 
   // Only act if the user has access to the overlay and a mode was not already
   // set. Other modules can also enable the overlay directly for other uses.
-  $use_overlay = !isset($user->data['overlay']) || $user->data['overlay'];
+  $user_data = user_data_get('overlay', $user->uid, 'enabled');
+  $use_overlay = !isset($user_data) || $user_data;
   if (empty($mode) && user_access('access overlay') && $use_overlay) {
     $current_path = current_path();
     // After overlay is enabled on the modules page, redirect to
@@ -354,9 +356,7 @@ function overlay_user_dismiss_message() {
     throw new AccessDeniedHttpException();
   }
 
-  $account = user_load($user->uid);
-  $account->data['overlay_message_dismissed'] = 1;
-  $account->save();
+  user_data_set('overlay', $user->uid, 'message_dismissed', 1);
   drupal_set_message(t('The message has been dismissed. You can change your overlay settings at any time by visiting your profile page.'));
   // Destination is normally given. Go to the user profile as a fallback.
   drupal_goto('user/' . $user->uid . '/edit');
@@ -378,7 +378,13 @@ function overlay_user_dismiss_message() {
 function overlay_disable_message() {
   global $user;
 
-  if (!empty($user->uid) && empty($user->data['overlay_message_dismissed']) && (!isset($user->data['overlay']) || $user->data['overlay']) && user_access('access overlay')) {
+  $build = array();
+  if (empty($user->uid) || !user_access('access overlay')) {
+    return $build;
+  }
+
+  $user_data = user_data_get('overlay', $user->uid);
+  if (empty($user_data['message_dismissed']) && (!isset($user_data['enabled']) || $user_data['enabled'])) {
     $build = array(
       '#theme' => 'overlay_disable_message',
       '#weight' => -99,
@@ -418,9 +424,6 @@ function overlay_disable_message() {
       )
     );
   }
-  else {
-    $build = array();
-  }
 
   return $build;
 }
diff --git a/core/modules/user/lib/Drupal/user/UserStorageController.php b/core/modules/user/lib/Drupal/user/UserStorageController.php
index 2660300..359217c 100644
--- a/core/modules/user/lib/Drupal/user/UserStorageController.php
+++ b/core/modules/user/lib/Drupal/user/UserStorageController.php
@@ -29,7 +29,6 @@ function attachLoad(&$queried_users, $load_revision = FALSE) {
       if ($record->picture) {
         $picture_fids[] = $record->picture;
       }
-      $queried_users[$key]->data = unserialize($record->data);
       $queried_users[$key]->roles = array();
       if ($record->uid) {
         $queried_users[$record->uid]->roles[DRUPAL_AUTHENTICATED_RID] = DRUPAL_AUTHENTICATED_RID;
@@ -148,13 +147,6 @@ protected function preSave(EntityInterface $entity) {
     if (isset($entity->roles)) {
       $entity->roles = array_filter($entity->roles);
     }
-
-    // Move account cancellation information into $entity->data.
-    foreach (array('user_cancel_method', 'user_cancel_notify') as $key) {
-      if (isset($entity->{$key})) {
-        $entity->data[$key] = $entity->{$key};
-      }
-    }
   }
 
   /**
@@ -193,6 +185,13 @@ protected function postSave(EntityInterface $entity, $update) {
         $query->execute();
       }
 
+      // Store account cancellation information.
+      foreach (array('user_cancel_method', 'user_cancel_notify') as $key) {
+        if (isset($entity->{$key})) {
+          user_data_set('user', $entity->uid, substr($key, 5), $entity->{$key});
+        }
+      }
+
       // If the user was blocked, delete the user's sessions to force a logout.
       if ($entity->original->status != $entity->status && $entity->status == 0) {
         drupal_session_destroy_uid($entity->uid);
@@ -229,6 +228,9 @@ protected function postDelete($entities) {
     db_delete('users_roles')
       ->condition('uid', array_keys($entities), 'IN')
       ->execute();
+    db_delete('users_data')
+      ->condition('uid', array_keys($entities), 'IN')
+      ->execute();
     db_delete('authmap')
       ->condition('uid', array_keys($entities), 'IN')
       ->execute();
diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php
index 0c3002c..84b26de 100644
--- a/core/modules/user/user.api.php
+++ b/core/modules/user/user.api.php
@@ -233,11 +233,6 @@ function hook_user_operations() {
  *
  * This hook is invoked before the user account is saved to the database.
  *
- * Modules that want to store properties in the serialized {users}.data column,
- * which is automatically loaded whenever a user account object is loaded, may
- * add their properties to $account->data in order to have their data serialized
- * on save.
- *
  * @param $account
  *   The user account object.
  *
@@ -245,10 +240,9 @@ function hook_user_operations() {
  * @see hook_user_update()
  */
 function hook_user_presave($account) {
-  // Make sure that our form value 'mymodule_foo' is stored as
-  // 'mymodule_bar' in the 'data' (serialized) column.
+  // Ensure that our value is an array.
   if (isset($account->mymodule_foo)) {
-    $account->data['mymodule_bar'] = $account->mymodule_foo;
+    $account->mymodule_foo = (array) $account->mymodule_foo;
   }
 }
 
diff --git a/core/modules/user/user.install b/core/modules/user/user.install
index a099f6b..b850f46 100644
--- a/core/modules/user/user.install
+++ b/core/modules/user/user.install
@@ -133,13 +133,6 @@ function user_schema() {
         'default' => '',
         'description' => 'E-mail address used for initial account creation.',
       ),
-      'data' => array(
-        'type' => 'blob',
-        'not null' => FALSE,
-        'size' => 'big',
-        'serialize' => TRUE,
-        'description' => 'A serialized array of name value pairs that are related to the user. Any form values posted during user edit are stored and are loaded into the $user object during user_load(). Use of this field is discouraged and it will likely disappear in a future version of Drupal.',
-      ),
     ),
     'indexes' => array(
       'access' => array('access'),
@@ -272,6 +265,54 @@ function user_schema() {
     ),
   );
 
+  $schema['users_data'] = array(
+    'description' => 'Stores module data as key/value pairs per user.',
+    'fields' => array(
+      'uid' => array(
+        'description' => 'Primary key: {users}.uid for user.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'module' => array(
+        'description' => 'The name of the module declaring the variable.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'name' => array(
+        'description' => 'The identifier of the data.',
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'value' => array(
+        'description' => 'The value.',
+        'type' => 'blob',
+        'not null' => FALSE,
+        'size' => 'big',
+      ),
+      'serialized' => array(
+        'description' => 'Whether value is serialized.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'unsigned' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('uid', 'module', 'name'),
+    'indexes' => array(
+      'module' => array('module'),
+      'name' => array('name'),
+    ),
+    'foreign keys' => array(
+      'uid' => array('users' => 'uid'),
+    ),
+  );
+
   $schema['users_roles'] = array(
     'description' => 'Maps users to roles.',
     'fields' => array(
@@ -331,7 +372,6 @@ function user_install() {
       'mail' => 'placeholder-for-uid-1',
       'created' => REQUEST_TIME,
       'status' => 1,
-      'data' => NULL,
     ))
     ->execute();
 
@@ -599,5 +639,141 @@ function user_update_8009(&$sandbox) {
 }
 
 /**
+ * Create new {users_data} table.
+ */
+function user_update_8010() {
+  // Create the {users_data} table.
+  db_create_table('users_data', array(
+    'description' => 'Stores module data as key/value pairs per user.',
+    'fields' => array(
+      'uid' => array(
+        'description' => 'Primary key: {users}.uid for user.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'module' => array(
+        'description' => 'The name of the module declaring the variable.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'name' => array(
+        'description' => 'The identifier of the data.',
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'value' => array(
+        'description' => 'The value.',
+        'type' => 'blob',
+        'not null' => FALSE,
+        'size' => 'big',
+      ),
+      'serialized' => array(
+        'description' => 'Whether value is serialized.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'unsigned' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('uid', 'module', 'name'),
+    'indexes' => array(
+      'module' => array('module'),
+      'name' => array('name'),
+    ),
+    'foreign keys' => array(
+      'uid' => array('users' => 'uid'),
+    ),
+  ));
+
+  // Create backup table for data migration.
+  // Since the origin/owner of individual values in {users}.data is unknown,
+  // other modules need to migrate their existing values from {_d7_users_data}.
+  db_create_table('_d7_users_data', array(
+    'description' => 'Backup of {users}.data for migration.',
+    'fields' => array(
+      'uid' => array(
+        'description' => 'Primary Key: {users}.uid for user.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'name' => array(
+        'description' => 'The name of the variable.',
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'value' => array(
+        'description' => 'The serialized value of the variable.',
+        'type' => 'blob',
+        'not null' => FALSE,
+        'size' => 'big',
+        'serialize' => TRUE,
+      ),
+    ),
+    'primary key' => array('uid'),
+    'foreign keys' => array(
+      'uid' => array('users' => 'uid'),
+    ),
+  ));
+}
+
+/**
+ * Move existing {users}.data into {_d7_users_data} migration table.
+ */
+function user_update_8011(&$sandbox) {
+  if (!isset($sandbox['progress'])) {
+    $sandbox['progress'] = 0;
+    // The anonymous user cannot have data, so start with uid 1.
+    $sandbox['last'] = 0;
+    $sandbox['max'] = db_query('SELECT COUNT(uid) FROM {users} WHERE uid > 0')->fetchField();
+  }
+
+  // Process 20 user records at a time. E.g., if there are 10 data keys per user
+  // record, that leads to an insert query with 200 values.
+  $result = db_query_range('SELECT uid, data FROM {users} WHERE uid > :uid ORDER BY uid ASC', 0, 20, array(':uid' => $sandbox['last']))->fetchAllKeyed();
+  $query = db_insert('_d7_users_data')->fields(array('uid', 'name', 'value'));
+  $has_values = FALSE;
+  foreach ($result as $uid => $data) {
+    $sandbox['progress']++;
+    $sandbox['last'] = $uid;
+    if (empty($data)) {
+      continue;
+    }
+    $data = unserialize($data);
+    if (!empty($data) && is_array($data)) {
+      $has_values = TRUE;
+      foreach ($data as $name => $value) {
+        $query->values(array(
+          'uid' => $uid,
+          'name' => $name,
+          'value' => serialize($value),
+        ));
+      }
+    }
+  }
+  if ($has_values) {
+    $query->execute();
+  }
+
+  $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+}
+
+/**
+ * Drop {users}.data column.
+ */
+function user_update_8012() {
+  db_drop_field('users', 'data');
+}
+
+/**
  * @} End of "addtogroup updates-7.x-to-8.x".
  */
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index e42002b..ad09671 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -2936,14 +2936,13 @@ function user_node_load($nodes, $types) {
     $uids[$nid] = $node->uid;
   }
 
-  // Fetch name, picture, and data for these users.
-  $user_fields = db_query("SELECT uid, name, picture, data FROM {users} WHERE uid IN (:uids)", array(':uids' => $uids))->fetchAllAssoc('uid');
+  // Fetch name and picture for these users.
+  $user_fields = db_query("SELECT uid, name, picture FROM {users} WHERE uid IN (:uids)", array(':uids' => $uids))->fetchAllAssoc('uid');
 
   // Add these values back into the node objects.
   foreach ($uids as $nid => $uid) {
     $nodes[$nid]->name = $user_fields[$uid]->name;
     $nodes[$nid]->picture = $user_fields[$uid]->picture;
-    $nodes[$nid]->data = $user_fields[$uid]->data;
   }
 }
 
@@ -3079,6 +3078,10 @@ function user_modules_uninstalled($modules) {
    db_delete('role_permission')
      ->condition('module', $modules, 'IN')
      ->execute();
+  // Remove any potentially orphan module data stored for users.
+  db_delete('users_data')
+    ->condition('module', $modules, 'IN')
+    ->execute();
 }
 
 /**
@@ -3182,3 +3185,127 @@ function user_library_info() {
 
   return $libraries;
 }
+
+/**
+ * Returns data stored for a user account.
+ *
+ * @param string $module
+ *   The name of the module the data is associated with.
+ * @param integer $uid
+ *   (optional) The user account ID the data is associated with.
+ * @param string $name
+ *   (optional) The name of the data key.
+ *
+ * @return mixed|array
+ *   The requested user account data, depending on the arguments passed:
+ *   - For $module, $name, and $uid, the stored data is returned, or NULL if no
+ *     data was found.
+ *   - For $module and $uid, an associative array is returned that contains the
+ *     stored data name/value pairs.
+ *   - For $module and $name, an associative array is returned whose keys are
+ *     user IDs and whose values contain the stored data.
+ *   - For $module only, an associative array is returned that contains all
+ *     existing data for $module in all user accounts, keyed by first by user ID
+ *     and $name second.
+ */
+function user_data_get($module, $uid = NULL, $name = NULL) {
+  $query = db_select('users_data', 'ud', array('fetch' => PDO::FETCH_ASSOC))
+    ->fields('ud')
+    ->condition('module', $module);
+  if (isset($uid)) {
+    $query->condition('uid', $uid);
+  }
+  if (isset($name)) {
+    $query->condition('name', $name);
+  }
+  $result = $query->execute()->fetchAllAssoc('uid');
+  // If $module, $name, and $uid was passed, return the value.
+  if (isset($name) && isset($uid)) {
+    if (isset($result[$uid])) {
+      return $result[$uid]['serialized'] ? unserialize($result[$uid]['value']) : $result[$uid]['value'];
+    }
+    return NULL;
+  }
+  // If $module and $uid was passed, return the name/value pairs.
+  elseif (isset($uid)) {
+    $return = array();
+    foreach ($result as $record) {
+      $return[$record['name']] = ($record['serialized'] ? unserialize($record['value']) : $record['value']);
+    }
+    return $return;
+  }
+  // If $module and $name was passed, return the values keyed by uid.
+  elseif (isset($name)) {
+    foreach ($result as $_uid => $record) {
+      $result[$_uid] = ($record['serialized'] ? unserialize($record['value']) : $record['value']);
+    }
+    return $result;
+  }
+  // If only $module was passed, return data keyed by uid and name.
+  else {
+    $return = array();
+    foreach ($result as $_uid => $record) {
+      $return[$_uid][$record['name']] = ($record['serialized'] ? unserialize($record['value']) : $record['value']);
+    }
+    return $return;
+  }
+}
+
+/**
+ * Stores data for a user account.
+ *
+ * @param string $module
+ *   The name of the module the data is associated with.
+ * @param integer $uid
+ *   The user account ID the data is associated with.
+ * @param string $name
+ *   The name of the data key.
+ * @param mixed $value
+ *   The value to store. Non-scalar values are serialized automatically.
+ *
+ * @return void
+ */
+function user_data_set($module, $uid, $name, $value) {
+  $serialized = 0;
+  if (!is_scalar($value)) {
+    $value = serialize($value);
+    $serialized = 1;
+  }
+  db_merge('users_data')
+    ->key(array(
+      'uid' => $uid,
+      'module' => $module,
+      'name' => $name,
+    ))
+    ->fields(array(
+      'value' => $value,
+      'serialized' => $serialized,
+    ))
+    ->execute();
+}
+
+/**
+ * Deletes data stored for a user account.
+ *
+ * @param string $module
+ *   The name of the module the data is associated with.
+ * @param integer $uid
+ *   (optional) The user account ID the data is associated with. If omitted, all
+ *   data for $module is deleted.
+ * @param string $name
+ *   (optional) The name of the data key. If omitted, all data associated with
+ *   $module and $uid is deleted.
+ *
+ * @return void
+ */
+function user_data_del($module, $uid = NULL, $name = NULL) {
+  $query = db_delete('users_data')
+    ->condition('module', $module);
+  if (isset($uid)) {
+    $query->condition('uid', $uid);
+  }
+  if (isset($name)) {
+    $query->condition('name', $name);
+  }
+  $query->execute();
+}
diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc
index 90d804e..f0220ac 100644
--- a/core/modules/user/user.pages.inc
+++ b/core/modules/user/user.pages.inc
@@ -391,13 +391,14 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') {
   $current = REQUEST_TIME;
 
   // Basic validation of arguments.
-  if (isset($account->data['user_cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
+  $account_data = user_data_get('user', $account->uid);
+  if (isset($account_data['cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
     // Validate expiration and hashed password/login.
     if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) {
       $edit = array(
-        'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : config('user.settings')->get('notify.status_canceled'),
+        'user_cancel_notify' => isset($account_data['cancel_notify']) ? $account_data['cancel_notify'] : config('user.settings')->get('notify.status_canceled'),
       );
-      user_cancel($edit, $account->uid, $account->data['user_cancel_method']);
+      user_cancel($edit, $account->uid, $account_data['cancel_method']);
       // Since user_cancel() is not invoked via Form API, batch processing needs
       // to be invoked manually and should redirect to the front page after
       // completion.
