diff --git a/includes/cache-install.inc b/includes/cache-install.inc
index 8bcf8b7..ae873ba 100644
--- a/includes/cache-install.inc
+++ b/includes/cache-install.inc
@@ -23,7 +23,7 @@ class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterfac
     return array();
   }
 
-  function set($cid, $data, $expire = CACHE_PERMANENT) {
+  function set($cid, $data, $expire = CACHE_PERMANENT, $tags = array()) {
   }
 
   function deletePrefix($cid) {
diff --git a/includes/cache.inc b/includes/cache.inc
index 9b60a7e..ee02899 100644
--- a/includes/cache.inc
+++ b/includes/cache.inc
@@ -276,8 +276,14 @@ interface DrupalCacheInterface {
    *     general cache wipe.
    *   - A Unix timestamp: Indicates that the item should be kept at least until
    *     the given time, after which it behaves like CACHE_TEMPORARY.
+   * @param $tags
+   *   An array of tags to be stored with the cache item. These should normally
+   *   identify objects used to build the cache item, which should trigger
+   *   cache invalidation when updated. For example if a cached item represents
+   *   a node, both the node ID and the author's user ID might be passed in as
+   *   tags. For example array('node' => array(123), 'user' => array(92)).
    */
-  function set($cid, $data, $expire = CACHE_PERMANENT);
+  function set($cid, $data, $expire = CACHE_PERMANENT, $tags = array());
 
   /**
    * Delete an item from the cache.
@@ -304,6 +310,15 @@ interface DrupalCacheInterface {
   function deletePrefix($prefix);
 
   /**
+   * Delete all cached items matching cache tags.
+   *
+   * @param $tags
+   *   An array of tags grouped by namespace. The array follows the same format
+   *   as that used to in DrupalCacheInterface::set().
+   */
+  function deleteTags($tags);
+
+  /**
    * Flush all cache items in a bin.
    */
   function flush();
@@ -437,7 +452,7 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
     return $cache;
   }
 
-  function set($cid, $data, $expire = CACHE_PERMANENT) {
+  function set($cid, $data, $expire = CACHE_PERMANENT, $tags = array()) {
     $fields = array(
       'serialized' => 0,
       'created' => REQUEST_TIME,
@@ -451,12 +466,27 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
       $fields['data'] = $data;
       $fields['serialized'] = 0;
     }
+    $insert_values = array();
+    foreach ($tags as $namespace => $values) {
+      foreach ($values as $value) {
+        $insert_values[] = array(
+          'cid' => $cid,
+          'namespace' => $namespace,
+          'value' => $value,
+        );
+      }
+    }
 
     try {
       db_merge($this->bin)
         ->key(array('cid' => $cid))
         ->fields($fields)
         ->execute();
+      $query = db_insert($this->bin . '_tags')->fields(array('cid', 'namespace', 'value'));
+      foreach ($insert_values as $record) {
+        $query->values($record);
+      }
+      $query->execute();
     }
     catch (Exception $e) {
       // The database may not be available, so we'll ignore cache_set requests.
@@ -485,8 +515,42 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
       ->execute();
   }
 
+  function deleteTags($tags) {
+    // MySQL does not use the primary key when performing subqueries with this
+    // syntax. However it supports deleting via JOINS, which is not ANSI SQL
+    // but allows the query to complete considerably faster.
+    // @see http://bugs.mysql.com/bug.php?id=9021
+    // @todo: remove when this bug is fixed in a stable release of MySQL.
+    $connection = Database::getConnection('default');
+    if ($connection->supportsJoinDML()) {
+      foreach ($tags as $namespace => $values) {
+        db_query('DELETE c FROM {' . db_escape_table($this->bin) . '} c INNER JOIN {' . db_escape_table($this->bin . '_tags') . '} t ON t.cid = c.cid AND t.value IN (:values) AND t.namespace = :namespace', array(':values' => $values, ':namespace' => $namespace));
+      }
+    }
+    else {
+      foreach ($tags as $namespace => $values) {
+        // Delete the cids corresponding to the tags.
+        $subquery = db_select($this->bin . '_tags', 't');
+        $subquery->addField('t', 'cid');
+        $subquery->condition('t.value', $values);
+        $subquery->condition('t.namespace', $namespace);
+
+        db_delete($this->bin)
+          ->condition('cid', $subquery, 'IN')
+          ->execute();
+      }
+      // Delete the tags themselves.
+      db_delete($this->bin . '_tags')
+        ->condition('value', $values)
+        ->condition('namespace', $namespace)
+        ->execute();
+
+    }
+  }
+
   function flush() {
     db_truncate($this->bin)->execute();
+    db_truncate($this->bin . '_tags')->execute();
   }
 
   function expire() {
diff --git a/includes/database/database.inc b/includes/database/database.inc
index 6108614..5ea33f3 100644
--- a/includes/database/database.inc
+++ b/includes/database/database.inc
@@ -252,6 +252,15 @@ abstract class DatabaseConnection extends PDO {
   protected $transactionalDDLSupport = FALSE;
 
   /**
+   * Whether this database connection supports JOIN syntax for DML statements.
+   *
+   * Set to FALSE by default because few databases support this feature.
+   *
+   * @var bool
+   */
+  protected $joinDMLSupport = FALSE;
+
+  /**
    * An index used to generate unique temporary table names.
    *
    * @var integer
@@ -1230,6 +1239,17 @@ abstract class DatabaseConnection extends PDO {
   }
 
   /**
+   * Determines if this driver supports JOIN syntax for DML statements.
+   *
+   * @return
+   *   TRUE if this connection supports JOIN syntax for DML statments, FALSE
+   *   otherwise.
+   */
+  public function supportsJoinDML() {
+    return $this->joinDMLSupport;
+  }
+
+  /**
    * Returns the name of the PDO driver for this connection.
    */
   abstract public function databaseType();
diff --git a/includes/database/mysql/database.inc b/includes/database/mysql/database.inc
index 7d5d859..0d77783 100644
--- a/includes/database/mysql/database.inc
+++ b/includes/database/mysql/database.inc
@@ -26,6 +26,9 @@ class DatabaseConnection_mysql extends DatabaseConnection {
     // MySQL never supports transactional DDL.
     $this->transactionalDDLSupport = FALSE;
 
+    // MySQL supports JOIN on DML statements.
+    $this->joinDMLSupport = TRUE;
+
     $this->connectionOptions = $connection_options;
 
     // The DSN should use either a socket or a host/port.
diff --git a/modules/simpletest/tests/cache.test b/modules/simpletest/tests/cache.test
index 1235269..afcec5a 100644
--- a/modules/simpletest/tests/cache.test
+++ b/modules/simpletest/tests/cache.test
@@ -269,6 +269,37 @@ class CacheClearCase extends CacheTestCase {
   }
 
   /**
+   * Test clearing using cache tags.
+   */
+  function testClearTags() {
+    $cache = cache($this->default_bin);
+    $cache->set('test_cid_clear1', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1)));
+    $cache->set('test_cid_clear2', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1)));
+    $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
+                      && $this->checkCacheExists('test_cid_clear2', $this->default_value),
+                      t('Two cache items were created.'));
+    $cache->deleteTags(array('test_tag' => array(1)));
+    $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value)
+                      || $this->checkCacheExists('test_cid_clear2', $this->default_value),
+                      t('Two caches removed after clearing a cache tag.'));
+
+    $cache->set('test_cid_clear1', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1)));
+    $cache->set('test_cid_clear2', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(2)));
+    $cache->set('test_cid_clear3', $this->default_value, CACHE_PERMANENT, array('test_tag_foo' => array(3)));
+    $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
+                      && $this->checkCacheExists('test_cid_clear2', $this->default_value)
+                      && $this->checkCacheExists('test_cid_clear3', $this->default_value),
+                      t('Two cached items were created.'));
+    $cache->deleteTags(array('test_tag_foo' => array(3)));
+    $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
+                      && $this->checkCacheExists('test_cid_clear2', $this->default_value),
+                      t('Cached items not matching the tag were not cleared.'));
+
+    $this->assertFalse($this->checkCacheExists('test_cid_clear3', $this->default_value),
+                      t('Cached item matching the tag was removed.'));
+  }
+
+  /**
    * Test clearing using an array.
    */
   function testClearArray() {
diff --git a/modules/system/system.install b/modules/system/system.install
index 6d80c43..2100d02 100644
--- a/modules/system/system.install
+++ b/modules/system/system.install
@@ -679,12 +679,51 @@ function system_schema() {
     ),
     'primary key' => array('cid'),
   );
+  $schema['cache_tags'] = array(
+    'description' => 'Cache table for tracking cache tags related to the cache bin.',
+    'fields' => array(
+      'cid' => array(
+        'description' => 'Unique cache ID.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'namespace' => array(
+        'description' => "Tag namespace, such as 'node' or 'user.",
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'value' => array(
+        'description' => "The cache tag value. For example a Node or User ID.",
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+    ),
+    'primary key' => array('cid', 'value', 'namespace'),
+    'indexes' => array(
+      'value_namespace' => array('value', 'namespace'),
+    ),
+    'foreign_keys' => array(
+      'cache_item' => array(
+        'table' => 'cache',
+        'columns' => array('cid' => 'cid'),
+      ),
+    ),
+  );
+
   $schema['cache_bootstrap'] = $schema['cache'];
   $schema['cache_bootstrap']['description'] = 'Cache table for data required to bootstrap Drupal, may be routed to a shared memory cache.';
   $schema['cache_form'] = $schema['cache'];
   $schema['cache_form']['description'] = 'Cache table for the form system to store recently built forms and their storage data, to be used in subsequent page requests.';
   $schema['cache_page'] = $schema['cache'];
   $schema['cache_page']['description'] = 'Cache table used to store compressed pages for anonymous users, if page caching is enabled.';
+  $schema['cache_page_tags'] = $schema['cache_tags'];
+  $schema['cache_page_tags']['foreign_keys']['cache_item']['table'] = 'cache_page';
   $schema['cache_menu'] = $schema['cache'];
   $schema['cache_menu']['description'] = 'Cache table for the menu system to store router information as well as generated link trees for various menu/page/user combinations.';
   $schema['cache_path'] = $schema['cache'];
