diff --git a/typo3/sysext/core/Classes/Cache/Backend/AbstractBackend.php b/typo3/sysext/core/Classes/Cache/Backend/AbstractBackend.php
index 6d80002a2c03ad98bb450a0d05802aa9a014b321..ee3f64181c4c9eb370da2fe3c223ea7dca5e073d 100644
--- a/typo3/sysext/core/Classes/Cache/Backend/AbstractBackend.php
+++ b/typo3/sysext/core/Classes/Cache/Backend/AbstractBackend.php
@@ -106,6 +106,22 @@ abstract class AbstractBackend implements \TYPO3\CMS\Core\Cache\Backend\BackendI
         $this->defaultLifetime = $defaultLifetime;
     }
 
+    /**
+     * Backwards compatibility safeguard since re-introducing flushByTags as API.
+     * See https://review.typo3.org/#/c/50537/ comments for patch set 14.
+     *
+     * The method is here even though it is only required for TaggableBackendInterface.
+     * We add it here to ensure third party cache backends do not fail but instead
+     * delegate to a less efficient linear flushing behavior.
+     *
+     * @param string[] $tags
+     * @api
+     */
+    public function flushByTags(array $tags)
+    {
+        array_walk($tags, [$this, 'flushByTag']);
+    }
+
     /**
      * Calculates the expiry time by the given lifetime. If no lifetime is
      * specified, the default lifetime is used.
diff --git a/typo3/sysext/core/Classes/Cache/Backend/TaggableBackendInterface.php b/typo3/sysext/core/Classes/Cache/Backend/TaggableBackendInterface.php
index d9f2949f692812ccbed75e74bf0113e7748178ff..8dd7db8a0c207eb0274f4a972bb4836a9faf9bb7 100644
--- a/typo3/sysext/core/Classes/Cache/Backend/TaggableBackendInterface.php
+++ b/typo3/sysext/core/Classes/Cache/Backend/TaggableBackendInterface.php
@@ -26,6 +26,15 @@ interface TaggableBackendInterface extends \TYPO3\CMS\Core\Cache\Backend\Backend
      */
     public function flushByTag($tag);
 
+    /**
+     * Removes all cache entries of this cache which are tagged by any of the specified tags.
+     *
+     * @param string[] $tag List of tags
+     * @return void
+     * @api
+     */
+    public function flushByTags(array $tags);
+
     /**
      * Finds and returns all cache entry identifiers which are tagged by the
      * specified tag
diff --git a/typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php b/typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php
index d7f59f30a1508db18f24ca98b39bd85d04d819f3..817461ec14da8e5c54e4bd5935d5c8135e4df1ac 100644
--- a/typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php
+++ b/typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php
@@ -261,6 +261,69 @@ class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInt
             ->truncate($this->tagsTable);
     }
 
+    /**
+     * Removes all entries tagged by any of the specified tags. Performs the SQL
+     * operation as a bulk query for better performance.
+     *
+     * @param string[] $tags
+     */
+    public function flushByTags(array $tags)
+    {
+        $this->throwExceptionIfFrontendDoesNotExist();
+
+        if (empty($tags)) {
+            return;
+        }
+
+        /** @var Connection $connection */
+        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
+
+        // A large set of tags was detected. Process it in chunks to guard against exceeding
+        // maximum SQL query limits.
+        if (count($tags) > 100) {
+            array_walk(array_chunk($tags, 100), [$this, 'flushByTags']);
+            return;
+        }
+        // VERY simple quoting of tags is sufficient here for performance. Tags are already
+        // validated to not contain any bad characters, e.g. they are automatically generated
+        // inside this class and suffixed with a pure integer enforced by DB.
+        $quotedTagList = array_map(function ($value) {
+            return '\'' . $value . '\'';
+        }, $tags);
+
+        if ($this->isConnectionMysql($connection)) {
+            // Use a optimized query on mysql ... don't use on your own
+            // * ansi sql does not know about multi table delete
+            // * doctrine query builder does not support join on delete()
+            $connection->executeQuery(
+                'DELETE tags2, cache1'
+                . ' FROM ' . $this->tagsTable . ' AS tags1'
+                . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
+                . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
+                . ' WHERE tags1.tag IN (' . implode(',', $quotedTagList) . ')'
+            );
+        } else {
+            $queryBuilder = $connection->createQueryBuilder();
+            $result = $queryBuilder->select('identifier')
+                ->from($this->tagsTable)
+                ->where('tag IN (' . implode(',', $quotedTagList) . ')')
+                // group by is like DISTINCT and used here to suppress possible duplicate identifiers
+                ->groupBy('identifier')
+                ->execute();
+            $cacheEntryIdentifiers = [];
+            while ($row = $result->fetch()) {
+                $cacheEntryIdentifiers[] = $row['identifier'];
+            }
+            $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
+            $queryBuilder->delete($this->cacheTable)
+                ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
+                ->execute();
+            $queryBuilder->delete($this->tagsTable)
+                ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
+                ->execute();
+        }
+    }
+
     /**
      * Removes all cache entries of this cache which are tagged by the specified tag.
      *
@@ -271,7 +334,15 @@ class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInt
     {
         $this->throwExceptionIfFrontendDoesNotExist();
 
+        if (empty($tag)) {
+            return;
+        }
+
+        /** @var Connection $connection */
         $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
+
+        $quotedTag = '\'' . $tag . '\'';
+
         if ($this->isConnectionMysql($connection)) {
             // Use a optimized query on mysql ... don't use on your own
             // * ansi sql does not know about multi table delete
@@ -281,14 +352,13 @@ class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInt
                 . ' FROM ' . $this->tagsTable . ' AS tags1'
                 . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
                 . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
-                . ' WHERE tags1.tag = ?',
-                [$tag]
+                . ' WHERE tags1.tag = ' . $quotedTag
             );
         } else {
             $queryBuilder = $connection->createQueryBuilder();
             $result = $queryBuilder->select('identifier')
                 ->from($this->tagsTable)
-                ->where($queryBuilder->expr()->eq('tag', $queryBuilder->createNamedParameter($tag, \PDO::PARAM_STR)))
+                ->where('tag = ' . $quotedTag)
                 // group by is like DISTINCT and used here to suppress possible duplicate identifiers
                 ->groupBy('identifier')
                 ->execute();
diff --git a/typo3/sysext/core/Classes/Cache/CacheManager.php b/typo3/sysext/core/Classes/Cache/CacheManager.php
index c4b56e7a7ad7b7a48e53ad536691c7108442f501..bb6321e36dfc96c557dd155d46225feaf1c0d0fc 100644
--- a/typo3/sysext/core/Classes/Cache/CacheManager.php
+++ b/typo3/sysext/core/Classes/Cache/CacheManager.php
@@ -163,15 +163,14 @@ class CacheManager implements SingletonInterface
     public function flushCachesInGroup($groupIdentifier)
     {
         $this->createAllCaches();
-        if (isset($this->cacheGroups[$groupIdentifier])) {
-            foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
-                if (isset($this->caches[$cacheIdentifier])) {
-                    $this->caches[$cacheIdentifier]->flush();
-                }
-            }
-        } else {
+        if (!isset($this->cacheGroups[$groupIdentifier])) {
             throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390334120);
         }
+        foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
+            if (isset($this->caches[$cacheIdentifier])) {
+                $this->caches[$cacheIdentifier]->flush();
+            }
+        }
     }
 
     /**
@@ -179,23 +178,51 @@ class CacheManager implements SingletonInterface
      * caches of a specific group.
      *
      * @param string $groupIdentifier
-     * @param string $tag Tag to search for
+     * @param string|array $tag Tag to search for
      * @return void
      * @throws NoSuchCacheGroupException
      * @api
      */
     public function flushCachesInGroupByTag($groupIdentifier, $tag)
     {
+        if (empty($tag)) {
+            return;
+        }
         $this->createAllCaches();
-        if (isset($this->cacheGroups[$groupIdentifier])) {
-            foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
-                if (isset($this->caches[$cacheIdentifier])) {
-                    $this->caches[$cacheIdentifier]->flushByTag($tag);
-                }
-            }
-        } else {
+        if (!isset($this->cacheGroups[$groupIdentifier])) {
             throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390337129);
         }
+        foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
+            if (isset($this->caches[$cacheIdentifier])) {
+                $this->caches[$cacheIdentifier]->flushByTag($tag);
+            }
+        }
+    }
+
+    /**
+     * Flushes entries tagged by any of the specified tags in all registered
+     * caches of a specific group.
+     *
+     * @param string $groupIdentifier
+     * @param string[] $tag Tags to search for
+     * @return void
+     * @throws NoSuchCacheGroupException
+     * @api
+     */
+    public function flushCachesInGroupByTags($groupIdentifier, array $tags)
+    {
+        if (empty($tag)) {
+            return;
+        }
+        $this->createAllCaches();
+        if (!isset($this->cacheGroups[$groupIdentifier])) {
+            throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390337130);
+        }
+        foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
+            if (isset($this->caches[$cacheIdentifier])) {
+                $this->caches[$cacheIdentifier]->flushByTags($tags);
+            }
+        }
     }
 
     /**
@@ -214,6 +241,21 @@ class CacheManager implements SingletonInterface
         }
     }
 
+    /**
+     * Flushes entries tagged by any of the specified tags in all registered caches.
+     *
+     * @param string[] $tag Tags to search for
+     * @return void
+     * @api
+     */
+    public function flushCachesByTags(array $tags)
+    {
+        $this->createAllCaches();
+        foreach ($this->caches as $cache) {
+            $cache->flushByTags($tags);
+        }
+    }
+
     /**
      * Instantiates all registered caches.
      *
diff --git a/typo3/sysext/core/Classes/Cache/Frontend/AbstractFrontend.php b/typo3/sysext/core/Classes/Cache/Frontend/AbstractFrontend.php
index cdddad469f8ed2e02d379358815794ba4edae507..b32fc92d1233bad53b136d31f6da67135c0000b9 100644
--- a/typo3/sysext/core/Classes/Cache/Frontend/AbstractFrontend.php
+++ b/typo3/sysext/core/Classes/Cache/Frontend/AbstractFrontend.php
@@ -121,6 +121,25 @@ abstract class AbstractFrontend implements FrontendInterface
         $this->backend->flush();
     }
 
+    /**
+     * Removes all cache entries of this cache which are tagged by any of the specified tags.
+     *
+     * @param string[] $tags
+     * @return void
+     * @throws \InvalidArgumentException
+     */
+    public function flushByTags(array $tags)
+    {
+        foreach ($tags as $tag) {
+            if (!$this->isValidTag($tag)) {
+                throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057360);
+            }
+        }
+        if ($this->backend instanceof TaggableBackendInterface) {
+            $this->backend->flushByTags($tags);
+        }
+    }
+
     /**
      * Removes all cache entries of this cache which are tagged by the specified tag.
      *
@@ -171,12 +190,20 @@ abstract class AbstractFrontend implements FrontendInterface
     /**
      * Checks the validity of a tag. Returns TRUE if it's valid.
      *
-     * @param string $tag An identifier to be checked for validity
+     * @param string|array $tag An identifier to be checked for validity
      * @return bool
      * @api
      */
     public function isValidTag($tag)
     {
-        return preg_match(self::PATTERN_TAG, $tag) === 1;
+        if (!is_array($tag)) {
+            return preg_match(self::PATTERN_TAG, $tag) === 1;
+        }
+        foreach ($tag as $tagValue) {
+            if (!$this->isValidTag($tagValue)) {
+                return false;
+            }
+        }
+        return true;
     }
 }
diff --git a/typo3/sysext/core/Classes/Cache/Frontend/FrontendInterface.php b/typo3/sysext/core/Classes/Cache/Frontend/FrontendInterface.php
index 43868c38f2073bf2cdfba9d79d3f93ce8c12bfbd..5863c619f22c0df5a127ff44ef6e02fef7699133 100644
--- a/typo3/sysext/core/Classes/Cache/Frontend/FrontendInterface.php
+++ b/typo3/sysext/core/Classes/Cache/Frontend/FrontendInterface.php
@@ -114,6 +114,15 @@ interface FrontendInterface
      */
     public function flushByTag($tag);
 
+    /**
+     * Removes all cache entries of this cache which are tagged by any of the specified tags.
+     *
+     * @param string[] $tag List of tags
+     * @return void
+     * @api
+     */
+    public function flushByTags(array $tags);
+
     /**
      * Does garbage collection
      *
diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index c5437d90fe22f57e25657cbd0c2c2b689067c703..24629cf2e0fa98e86c779a595471feac2bbb0dad 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -8113,9 +8113,7 @@ class DataHandler
 
         /** @var CacheManager $cacheManager */
         $cacheManager = $this->getCacheManager();
-        foreach ($tagsToClear as $tag => $_) {
-            $cacheManager->flushCachesInGroupByTag('pages', $tag);
-        }
+        $cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear));
 
         // Execute collected clear cache commands from page TSConfig
         foreach ($clearCacheCommands as $command) {
@@ -8360,9 +8358,7 @@ class DataHandler
         }
         // process caching framwork operations
         if (!empty($tagsToFlush)) {
-            foreach (array_unique($tagsToFlush) as $tag) {
-                $this->getCacheManager()->flushCachesInGroupByTag('pages', $tag);
-            }
+            $this->getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);
         }
 
         // Call post processing function for clear-cache:
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcBackendTest.php
index cc3639352158261c78bd8da0410d6b641e03624a..24f63dbdaeee7896252367f17229fb4024321d42 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcBackendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcBackendTest.php
@@ -207,6 +207,22 @@ class ApcBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $this->assertTrue($backend->has('BackendAPCTest3'), 'BackendAPCTest3');
     }
 
+    /**
+     * @test
+     */
+    public function flushByTagsRemovesCacheEntriesWithSpecifiedTags()
+    {
+        $backend = $this->setUpBackend();
+        $data = 'some data' . microtime();
+        $backend->set('BackendAPCTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']);
+        $backend->set('BackendAPCTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']);
+        $backend->set('BackendAPCTest3', $data, ['UnitTestTag%test']);
+        $backend->flushByTags(['UnitTestTag%special', 'UnitTestTag%boring']);
+        $this->assertFalse($backend->has('BackendAPCTest1'), 'BackendAPCTest1');
+        $this->assertFalse($backend->has('BackendAPCTest2'), 'BackendAPCTest2');
+        $this->assertTrue($backend->has('BackendAPCTest3'), 'BackendAPCTest3');
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcuBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcuBackendTest.php
index 865ba7e436327b5849f339b8bccbeb88b64f18d7..1620d3c769f506ca3f01d7ba57998c7d76a91de4 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcuBackendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcuBackendTest.php
@@ -206,6 +206,22 @@ class ApcuBackendTest extends UnitTestCase
         $this->assertTrue($backend->has('BackendAPCUTest3'));
     }
 
+    /**
+     * @test
+     */
+    public function flushByTagsRemovesCacheEntriesWithSpecifiedTags()
+    {
+        $backend = $this->setUpBackend();
+        $data = 'some data' . microtime();
+        $backend->set('BackendAPCUTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']);
+        $backend->set('BackendAPCUTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']);
+        $backend->set('BackendAPCUTest3', $data, ['UnitTestTag%test']);
+        $backend->flushByTags(['UnitTestTag%special', 'UnitTestTag%boring']);
+        $this->assertFalse($backend->has('BackendAPCUTest1'), 'BackendAPCTest1');
+        $this->assertFalse($backend->has('BackendAPCUTest2'), 'BackendAPCTest2');
+        $this->assertTrue($backend->has('BackendAPCUTest3'), 'BackendAPCTest3');
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php
index 61d1f2e87ac78536b65f5550de6341fc57d1d0dd..2509c93ee4ce129e92eed288af3f3e9444100aa3 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php
@@ -192,6 +192,22 @@ class MemcachedBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $this->assertTrue($backend->has('BackendMemcacheTest3'), 'BackendMemcacheTest3');
     }
 
+    /**
+     * @test
+     */
+    public function flushByTagsRemovesCacheEntriesWithSpecifiedTags()
+    {
+        $backend = $this->setUpBackend();
+        $data = 'some data' . microtime();
+        $backend->set('BackendMemcacheTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']);
+        $backend->set('BackendMemcacheTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']);
+        $backend->set('BackendMemcacheTest3', $data, ['UnitTestTag%test']);
+        $backend->flushByTags(['UnitTestTag%special', 'UnitTestTag%boring']);
+        $this->assertFalse($backend->has('BackendMemcacheTest1'), 'BackendMemcacheTest1');
+        $this->assertFalse($backend->has('BackendMemcacheTest2'), 'BackendMemcacheTest2');
+        $this->assertTrue($backend->has('BackendMemcacheTest3'), 'BackendMemcacheTest3');
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/PdoBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/PdoBackendTest.php
index 95151caf643960511c0080379fdcf114afe2e838..57c0c6e71c16fa8635ed8705478bd0b692396e2a 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Backend/PdoBackendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/PdoBackendTest.php
@@ -176,6 +176,22 @@ class PdoBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $this->assertTrue($backend->has('PdoBackendTest3'), 'PdoBackendTest3');
     }
 
+    /**
+     * @test
+     */
+    public function flushByTagsRemovesCacheEntriesWithSpecifiedTags()
+    {
+        $backend = $this->setUpBackend();
+        $data = 'some data' . microtime();
+        $backend->set('PdoBackendTest1', $data, ['UnitTestTag%test', 'UnitTestTags%boring']);
+        $backend->set('PdoBackendTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']);
+        $backend->set('PdoBackendTest3', $data, ['UnitTestTag%test']);
+        $backend->flushByTags(['UnitTestTag%special', 'UnitTestTags%boring']);
+        $this->assertFalse($backend->has('PdoBackendTest1'), 'PdoBackendTest1');
+        $this->assertFalse($backend->has('PdoBackendTest2'), 'PdoBackendTest2');
+        $this->assertTrue($backend->has('PdoBackendTest3'), 'PdoBackendTest3');
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/RedisBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/RedisBackendTest.php
index c44f0b01d456c84567ddc83a6eab704101203a7d..40039f96c4d77719ebc9bc9c3e433b6c9acc64f3 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Backend/RedisBackendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/RedisBackendTest.php
@@ -762,6 +762,28 @@ class RedisBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $this->assertSame($expectedResult, $actualResult);
     }
 
+    /**
+     * @test Functional
+     */
+    public function flushByTagsRemovesEntriesTaggedWithSpecifiedTags()
+    {
+        $this->setUpBackend();
+        $identifier = $this->getUniqueId('identifier');
+        $this->backend->set($identifier . 'A', 'data', ['tag1']);
+        $this->backend->set($identifier . 'B', 'data', ['tag2']);
+        $this->backend->set($identifier . 'C', 'data', ['tag1', 'tag2']);
+        $this->backend->set($identifier . 'D', 'data', ['tag3']);
+        $this->backend->flushByTags(['tag1', 'tag2']);
+        $expectedResult = [false, false, false, true];
+        $actualResult = [
+            $this->backend->has($identifier . 'A'),
+            $this->backend->has($identifier . 'B'),
+            $this->backend->has($identifier . 'C'),
+            $this->backend->has($identifier . 'D')
+        ];
+        $this->assertSame($expectedResult, $actualResult);
+    }
+
     /**
      * @test Implementation
      */
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/TransientMemoryBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/TransientMemoryBackendTest.php
index 26b61b61bec21d10b64007021711fd6333525b39..0e2ee1e4ef71d262fe3f2cdb78c14313ab5179f9 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Backend/TransientMemoryBackendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/TransientMemoryBackendTest.php
@@ -159,6 +159,24 @@ class TransientMemoryBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $this->assertTrue($backend->has('TransientMemoryBackendTest3'), 'TransientMemoryBackendTest3');
     }
 
+    /**
+     * @test
+     */
+    public function flushByTagsRemovesCacheEntriesWithSpecifiedTags()
+    {
+        $cache = $this->createMock(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface::class);
+        $backend = new \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend('Testing');
+        $backend->setCache($cache);
+        $data = 'some data' . microtime();
+        $backend->set('TransientMemoryBackendTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']);
+        $backend->set('TransientMemoryBackendTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']);
+        $backend->set('TransientMemoryBackendTest3', $data, ['UnitTestTag%test']);
+        $backend->flushByTags(['UnitTestTag%special', 'UnitTestTag%boring']);
+        $this->assertFalse($backend->has('TransientMemoryBackendTest1'), 'TransientMemoryBackendTest1');
+        $this->assertFalse($backend->has('TransientMemoryBackendTest2'), 'TransientMemoryBackendTest2');
+        $this->assertTrue($backend->has('TransientMemoryBackendTest3'), 'TransientMemoryBackendTest3');
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php
index a15798d911411cc112405e2f6bc85835ebc7b6e3..474c9ef978270904298286530e7e79a5fe9dcea8 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php
@@ -176,6 +176,50 @@ class Typo3DatabaseBackendTest extends UnitTestCase
         $subject->flush();
     }
 
+    public function flushByTagCallsDeleteOnConnection()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_test');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $connectionProphet = $this->prophesize(Connection::class);
+        $connectionProphet->delete('cf_cache_test')->shouldBeCalled()->willReturn(0);
+        $connectionProphet->delete('cf_cache_test_tags')->shouldBeCalled()->willReturn(0);
+
+        $connectionPoolProphet = $this->prophesize(ConnectionPool::class);
+        $connectionPoolProphet->getConnectionForTable(Argument::cetera())->willReturn($connectionProphet->reveal());
+
+        // Two instances are required as there are different tables being cleared
+        GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
+        GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
+
+        $subject->flushByTag('Tag');
+    }
+
+    public function flushByTagsCallsDeleteOnConnection()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_test');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $connectionProphet = $this->prophesize(Connection::class);
+        $connectionProphet->delete('cf_cache_test')->shouldBeCalled()->willReturn(0);
+        $connectionProphet->delete('cf_cache_test_tags')->shouldBeCalled()->willReturn(0);
+
+        $connectionPoolProphet = $this->prophesize(ConnectionPool::class);
+        $connectionPoolProphet->getConnectionForTable(Argument::cetera())->willReturn($connectionProphet->reveal());
+
+        // Two instances are required as there are different tables being cleared
+        GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
+        GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
+
+        $subject->flushByTag(['Tag1', 'Tag2']);
+    }
+
     /**
      * @test
      */
@@ -184,6 +228,16 @@ class Typo3DatabaseBackendTest extends UnitTestCase
         $subject = new Typo3DatabaseBackend('Testing');
         $this->expectException(Exception::class);
         $this->expectExceptionCode(1236518288);
-        $subject->flushByTag([]);
+        $subject->flushByTag('Tag');
+    }
+    /**
+     * @test
+     */
+    public function flushByTagsThrowsExceptionIfFrontendWasNotSet()
+    {
+        $subject = new Typo3DatabaseBackend('Testing');
+        $this->expectException(Exception::class);
+        $this->expectExceptionCode(1236518288);
+        $subject->flushByTags([]);
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/WincacheBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/WincacheBackendTest.php
index f31fbe198bd647467e8bc58fed686a5dda55133f..c6e0868bd814f464eb03b51c3796d7a4cbcb389d 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Backend/WincacheBackendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/WincacheBackendTest.php
@@ -169,6 +169,22 @@ class WincacheBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $this->assertTrue($backend->has('BackendWincacheTest3'), 'BackendWincacheTest3');
     }
 
+    /**
+     * @test
+     */
+    public function flushByTagsRemovesCacheEntriesWithSpecifiedTags()
+    {
+        $backend = $this->setUpBackend();
+        $data = 'some data' . microtime();
+        $backend->set('BackendWincacheTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']);
+        $backend->set('BackendWincacheTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']);
+        $backend->set('BackendWincacheTest3', $data, ['UnitTestTag%test']);
+        $backend->flushByTag('UnitTestTag%special', 'UnitTestTag%boring');
+        $this->assertTrue($backend->has('BackendWincacheTest1'), 'BackendWincacheTest1');
+        $this->assertFalse($backend->has('BackendWincacheTest2'), 'BackendWincacheTest2');
+        $this->assertTrue($backend->has('BackendWincacheTest3'), 'BackendWincacheTest3');
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/core/Tests/Unit/Cache/CacheManagerTest.php b/typo3/sysext/core/Tests/Unit/Cache/CacheManagerTest.php
index c8410122c9f2ec04e2a60619e51ef4924ac93086..a03fe9b4908d7ad3e67752f2fa0d8e3f64c1c007 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/CacheManagerTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/CacheManagerTest.php
@@ -138,6 +138,28 @@ class CacheManagerTest extends UnitTestCase
         $manager->flushCachesByTag('theTag');
     }
 
+    /**
+     * @test
+     */
+    public function flushCachesByTagsCallsTheFlushByTagsMethodOfAllRegisteredCaches()
+    {
+        $manager = new CacheManager();
+        $cache1 = $this->getMockBuilder(AbstractFrontend::class)
+            ->setMethods(['getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTags'])
+            ->disableOriginalConstructor()
+            ->getMock();
+        $cache1->expects($this->atLeastOnce())->method('getIdentifier')->will($this->returnValue('cache1'));
+        $cache1->expects($this->once())->method('flushByTags')->with($this->equalTo(['theTag']));
+        $manager->registerCache($cache1);
+        $cache2 = $this->getMockBuilder(AbstractFrontend::class)
+            ->setMethods(['getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTags'])
+            ->disableOriginalConstructor()
+            ->getMock();
+        $cache2->expects($this->once())->method('flushByTags')->with($this->equalTo(['theTag']));
+        $manager->registerCache($cache2);
+        $manager->flushCachesByTags(['theTag']);
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Fixtures/FrontendFixture.php b/typo3/sysext/core/Tests/Unit/Cache/Fixtures/FrontendFixture.php
index 6a861c9f5b8bf4de267b38ac7f16bee9e868aa63..4f6cf54b0a98b8bcaf68fad4eab797154053a7c2 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Fixtures/FrontendFixture.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Fixtures/FrontendFixture.php
@@ -65,6 +65,10 @@ class FrontendFixture implements FrontendInterface
     {
     }
 
+    public function flushByTags(array $tags)
+    {
+    }
+
     public function collectGarbage()
     {
     }
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Frontend/AbstractFrontendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Frontend/AbstractFrontendTest.php
index 227abfe6a7b117112971371a068fd9278b512f43..a4fd5bea268211ae302ceea4bf1c3e6aa15743dd 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Frontend/AbstractFrontendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Frontend/AbstractFrontendTest.php
@@ -103,7 +103,7 @@ class AbstractFrontendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $tag = 'sometag';
         $identifier = 'someCacheIdentifier';
         $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface::class)
-            ->setMethods(['setCache', 'get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'collectGarbage'])
+            ->setMethods(['setCache', 'get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'flushByTags', 'collectGarbage'])
             ->disableOriginalConstructor()
             ->getMock();
         $backend->expects($this->once())->method('flushByTag')->with($tag);
@@ -114,6 +114,25 @@ class AbstractFrontendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $cache->flushByTag($tag);
     }
 
+    /**
+     * @test
+     */
+    public function flushByTagsCallsBackendIfItIsATaggableBackend()
+    {
+        $tag = 'sometag';
+        $identifier = 'someCacheIdentifier';
+        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface::class)
+            ->setMethods(['setCache', 'get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'flushByTags', 'collectGarbage'])
+            ->disableOriginalConstructor()
+            ->getMock();
+        $backend->expects($this->once())->method('flushByTags')->with([$tag]);
+        $cache = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Frontend\StringFrontend::class)
+            ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag'])
+            ->setConstructorArgs([$identifier, $backend])
+            ->getMock();
+        $cache->flushByTags([$tag]);
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
index 209b72c6beb701c52207667cd8595edc54ce73a0..e264eb38aadba9bd64a9a9a76cf00e0d8602e7fa 100644
--- a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
+++ b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\DataHandler;
 
 use Prophecy\Argument;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
 use TYPO3\CMS\Core\DataHandling\DataHandler;
 use TYPO3\CMS\Core\Tests\AccessibleObjectInterface;
@@ -349,7 +350,18 @@ class DataHandlerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
 
         /** @var $subject DataHandler|\PHPUnit_Framework_MockObject_MockObject */
         $subject = $this->getMockBuilder(DataHandler::class)
-            ->setMethods(['newlog', 'checkModifyAccessList', 'tableReadOnly', 'checkRecordUpdateAccess', 'recordInfo'])
+            ->setMethods([
+                'newlog',
+                'checkModifyAccessList',
+                'tableReadOnly',
+                'checkRecordUpdateAccess',
+                'recordInfo',
+                'getCacheManager',
+                'registerElementsToBeDeleted',
+                'unsetElementsToBeDeleted',
+                'resetElementsToBeDeleted'
+            ])
+            ->disableOriginalConstructor()
             ->getMock();
 
         $subject->bypassWorkspaceRestrictions = false;
@@ -360,10 +372,18 @@ class DataHandlerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
                 ]
             ]
         ];
+
+        $cacheManagerMock = $this->getMockBuilder(CacheManager::class)
+            ->setMethods(['flushCachesInGroupByTags'])
+            ->getMock();
+        $cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTags')->with('pages', []);
+
+        $subject->expects($this->once())->method('getCacheManager')->willReturn($cacheManagerMock);
         $subject->expects($this->once())->method('recordInfo')->will($this->returnValue(null));
         $subject->expects($this->once())->method('checkModifyAccessList')->with('pages')->will($this->returnValue(true));
         $subject->expects($this->once())->method('tableReadOnly')->with('pages')->will($this->returnValue(false));
         $subject->expects($this->once())->method('checkRecordUpdateAccess')->will($this->returnValue(true));
+        $subject->expects($this->once())->method('unsetElementsToBeDeleted')->willReturnArgument(0);
 
         /** @var BackendUserAuthentication|\PHPUnit_Framework_MockObject_MockObject $backEndUser */
         $backEndUser = $this->createMock(BackendUserAuthentication::class);
diff --git a/typo3/sysext/extbase/Classes/Service/CacheService.php b/typo3/sysext/extbase/Classes/Service/CacheService.php
index 04cb590779791911fc1ceec90135e34bec35cc0a..b1c2911ba05fe43d99228a264088bc3b69b201f1 100644
--- a/typo3/sysext/extbase/Classes/Service/CacheService.php
+++ b/typo3/sysext/extbase/Classes/Service/CacheService.php
@@ -67,9 +67,10 @@ class CacheService implements \TYPO3\CMS\Core\SingletonInterface
             if (!is_array($pageIdsToClear)) {
                 $pageIdsToClear = [(int)$pageIdsToClear];
             }
-            foreach ($pageIdsToClear as $pageId) {
-                $this->cacheManager->flushCachesInGroupByTag('pages', 'pageId_' . $pageId);
-            }
+            $tags = array_map(function ($item) {
+                return 'pageId_' . $item;
+            }, $pageIdsToClear);
+            $this->cacheManager->flushCachesInGroupByTags('pages', $tags);
         }
     }
 
diff --git a/typo3/sysext/extbase/Tests/Unit/Service/CacheServiceTest.php b/typo3/sysext/extbase/Tests/Unit/Service/CacheServiceTest.php
index 305788a73c4ca9b0cb465c33f7b05d95e0e35a0b..c9cc1e69c4251343a36ea0b9d50f6657d29fcc5b 100644
--- a/typo3/sysext/extbase/Tests/Unit/Service/CacheServiceTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Service/CacheServiceTest.php
@@ -41,7 +41,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function clearPageCacheConvertsPageIdsToArray()
     {
-        $this->cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTag')->with('pages', 'pageId_123');
+        $this->cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTags')->with('pages', ['pageId_123']);
         $this->cacheService->clearPageCache(123);
     }
 
@@ -50,7 +50,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function clearPageCacheConvertsPageIdsToNumericArray()
     {
-        $this->cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTag')->with('pages', 'pageId_0');
+        $this->cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTags')->with('pages', ['pageId_0']);
         $this->cacheService->clearPageCache('Foo');
     }
 
@@ -68,9 +68,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function clearPageCacheUsesCacheManagerToFlushCacheOfSpecifiedPages()
     {
-        $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTag')->with('pages', 'pageId_1');
-        $this->cacheManagerMock->expects($this->at(1))->method('flushCachesInGroupByTag')->with('pages', 'pageId_2');
-        $this->cacheManagerMock->expects($this->at(2))->method('flushCachesInGroupByTag')->with('pages', 'pageId_3');
+        $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTags')->with('pages', ['pageId_1', 'pageId_2', 'pageId_3']);
         $this->cacheService->clearPageCache([1, 2, 3]);
     }
 
@@ -79,9 +77,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function clearsCachesOfRegisteredPageIds()
     {
-        $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTag')->with('pages', 'pageId_2');
-        $this->cacheManagerMock->expects($this->at(1))->method('flushCachesInGroupByTag')->with('pages', 'pageId_15');
-        $this->cacheManagerMock->expects($this->at(2))->method('flushCachesInGroupByTag')->with('pages', 'pageId_8');
+        $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTags')->with('pages', ['pageId_2', 'pageId_15', 'pageId_8']);
 
         $this->cacheService->getPageIdStack()->push(8);
         $this->cacheService->getPageIdStack()->push(15);
@@ -95,10 +91,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function clearsCachesOfDuplicateRegisteredPageIdsOnlyOnce()
     {
-        $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTag')->with('pages', 'pageId_2');
-        $this->cacheManagerMock->expects($this->at(1))->method('flushCachesInGroupByTag')->with('pages', 'pageId_15');
-        $this->cacheManagerMock->expects($this->at(2))->method('flushCachesInGroupByTag')->with('pages', 'pageId_8');
-        $this->cacheManagerMock->expects($this->exactly(3))->method('flushCachesInGroupByTag');
+        $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTags')->with('pages', ['pageId_2', 'pageId_15', 'pageId_8']);
 
         $this->cacheService->getPageIdStack()->push(8);
         $this->cacheService->getPageIdStack()->push(15);