diff --git a/trac/admin/tests/console-tests.txt b/trac/admin/tests/console-tests.txt
--- a/trac/admin/tests/console-tests.txt
+++ b/trac/admin/tests/console-tests.txt
@@ -11,6 +11,8 @@
 attachment export    Export an attachment from a resource to a file or stdout
 attachment list      List attachments of a resource
 attachment remove    Remove an attachment from a resource
+cache invalidate     Invalidate one or more caches
+cache list           List caches
 component add        Add a new component
 component chown      Change component ownership
 component list       Show available components
diff --git a/trac/cache.py b/trac/cache.py
new file mode 100644
--- /dev/null
+++ b/trac/cache.py
@@ -0,0 +1,269 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/.
+
+try:
+    import threading
+except ImportError:
+    import dummy_threading as threading
+
+from trac.admin import IAdminCommandProvider
+from trac.core import Component, implements
+from trac.util.compat import partial
+from trac.util.text import print_table
+from trac.util.translation import _
+
+__all__ = ["CacheManager", "cached", "cached_value"]
+
+
+class cached_value(object):
+    """Method decorator creating a cached attribute from a data retrieval
+    method.
+    
+    Accessing the cached attribute gives back the cached value. The data
+    retrieval method will be called as needed by the CacheManager.
+    Invalidating the cache for this value is done by `del`eting the attribute.
+    
+    The data retrieval method is called with a single argument `db` containing
+    a reference to a database connection. All data retrieval should be done
+    through this connection.
+    
+    Note that the cache validity is maintained using a table in the database.
+    Most notably, a cache invalidation will trigger a commit, so don't do this
+    while another database operation is in progress.
+    
+    If more control over the transaction is needed, see the `cached` decorator.
+    
+    This decorator can only be used within `Component` subclasses. See
+    CacheProxy for caching attributes of other objects.
+    """
+    def __init__(self, retriever):
+        self.retriever = retriever
+        self.__doc__ = retriever.__doc__
+        
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        key = owner.__module__ + '.' + owner.__name__ \
+              + '.' + self.retriever.__name__
+        return CacheManager(instance.env).get(key,
+                partial(self.retriever, instance))
+        
+    def __delete__(self, instance):
+        key = instance.__class__.__module__ \
+              + '.' + instance.__class__.__name__ \
+              + '.' + self.retriever.__name__
+        CacheManager(instance.env).invalidate(key)
+
+
+class cached(cached_value):
+    """Method decorator creating a cached attribute from a data retrieval
+    method.
+    
+    In contrast with cached attributes created by the `cached_value` decorator,
+    accessing a cached attribute created with `cached` will not directly give 
+    back the cached value. Instead, this will return a proxy object with `get`
+    and `invalidate` methods, both accepting a `db` connection. After calling
+    `invalidate(db)`, doing a `commit` is the responsibility of the caller.
+    
+    This decorator can only be used within `Component` subclasses. See
+    CacheProxy for caching attributes of other objects.
+    """
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        key = owner.__module__ + '.' + owner.__name__ \
+              + '.' + self.retriever.__name__
+        return CacheProxy(key, partial(self.retriever, instance),
+                          instance.env)
+
+
+class CacheProxy(object):
+    """Cached attribute proxy.
+    
+    This is the class of the object returned when accessing an attribute
+    cached with the `cached` decorator.
+    
+    It can also be instantiated explicitly to cache attributes of
+    non-`Component` objects. In this case, the cache identifier key must be
+    provided, and the data retrieval function is a normal callable (not an
+    unbound method).
+    """
+    __slots__ = ["key", "retriever", "env"]
+    
+    def __init__(self, key, retriever, env):
+        self.key = key
+        self.retriever = retriever
+        self.env = env
+    
+    def get(self, db=None):
+        return CacheManager(self.env).get(self.key, self.retriever, db)
+    
+    def invalidate(self, db=None):
+        CacheManager(self.env).invalidate(self.key, db)
+
+
+class CacheManager(Component):
+    """Cache manager component."""
+    
+    implements(IAdminCommandProvider)
+    
+    def __init__(self):
+        self._cache = {}
+        self._local = threading.local()
+        self._lock = threading.RLock()
+    
+    # Public interface
+    
+    def reset_metadata(self):
+        """Reset per-request cache metadata."""
+        try:
+            del self._local.meta
+            del self._local.cache
+        except AttributeError:
+            pass
+
+    def get(self, key, retriever, db=None):
+        """Get cached or fresh data for the given key."""
+        # Get cache metadata
+        try:
+            local_meta = self._local.meta
+            local_cache = self._local.cache
+        except AttributeError:
+            # First cache usage in this request, retrieve cache metadata
+            # from the database and make a thread-local copy of the cache
+            self.log.debug("Retrieving cache metadata")
+            if db is None:
+                db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT key, generation FROM cache")
+            self._local.meta = local_meta = dict(cursor)
+            self._local.cache = local_cache = self._cache.copy()
+        
+        db_generation = local_meta.get(key, -1)
+        
+        # Try the thread-local copy first
+        try:
+            (data, generation) = local_cache[key]
+            if generation == db_generation:
+                return data
+        except KeyError:
+            pass
+        
+        self._lock.acquire()
+        try:
+            # Get data from the process cache
+            try:
+                (data, generation) = local_cache[key] = self._cache[key]
+                if generation == db_generation:
+                    return data
+            except KeyError:
+                generation = None   # Force retrieval from the database
+            
+            # Check if the process cache has the newest version, as it may
+            # have been updated after the metadata retrieval
+            if db is None:
+                db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT generation FROM cache WHERE key=%s", (key,))
+            row = cursor.fetchone()
+            db_generation = row and row[0] or -1
+            if db_generation == generation:
+                return data
+            
+            # Retrieve data from the database
+            self.log.debug("Retrieving data for cache '%s'", key)
+            data = retriever(db)
+            local_cache[key] = self._cache[key] = (data, db_generation)
+            local_meta[key] = db_generation
+            return data
+        finally:
+            self._lock.release()
+        
+    def invalidate(self, key, db=None):
+        """Invalidate cached data for the given key."""
+        self.log.debug("Invalidating cache '%s'", key)
+        self._lock.acquire()
+        try:
+            # Invalidate in other processes
+            handle_ta = db is None
+            if handle_ta:
+                db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            
+            # The row corresponding to the cache may not exist in the table
+            # yet.
+            #  - If the row exists, the UPDATE increments the generation, the
+            #    SELECT returns a row and we're done.
+            #  - If the row doesn't exist, the UPDATE does nothing, but starts
+            #    a transaction. The SELECT then returns nothing, and we can
+            #    safely INSERT a new row.
+            cursor.execute("UPDATE cache SET generation=generation+1 "
+                           "WHERE key=%s", (key,))
+            cursor.execute("SELECT generation FROM cache WHERE key=%s", (key,))
+            if not cursor.fetchone():
+                cursor.execute("INSERT INTO cache VALUES (%s, %s)", (key, 0))
+            if handle_ta:
+                db.commit()
+            
+            # Invalidate in this process
+            self._cache.pop(key, None)
+            
+            # Invalidate in this thread
+            try:
+                del self._local.cache[key]
+            except (AttributeError, KeyError):
+                pass
+        finally:
+            self._lock.release()
+
+    # IAdminCommandProvider methods
+    
+    def get_admin_commands(self):
+        yield ('cache list', '',
+               'List caches',
+               None, self._do_list)
+        yield ('cache invalidate', '<key> [key] [...]',
+               'Invalidate one or more caches',
+               self._complete_invalidate, self._do_invalidate)
+    
+    def get_cache_keys(self, db=None):
+        if db is None:
+            db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT key FROM cache")
+        return [row[0] for row in cursor]
+        
+    def _complete_invalidate(self, args):
+        if len(args) == 1:
+            return self.get_cache_keys()
+    
+    def _do_list(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT key, generation FROM cache ORDER BY key")
+        print_table([(row[0], int(row[1])) for row in cursor],
+                    [_('Key'), _('Generation')])
+    
+    def _do_invalidate(self, *keys):
+        db = self.env.get_db_cnx()
+        all_keys = set(self.get_cache_keys(db))
+        inval_keys = []
+        for key in keys:
+            if key.endswith('*'):
+                inval_keys.extend(each for each in all_keys
+                                  if each.startswith(key[:-1]))
+            else:
+                inval_keys.append(key)
+        for key in inval_keys:
+            self.invalidate(key, db)
+        db.commit()
diff --git a/trac/db_default.py b/trac/db_default.py
--- a/trac/db_default.py
+++ b/trac/db_default.py
@@ -17,7 +17,7 @@
 from trac.db import Table, Column, Index
 
 # Database version identifier. Used for automatic upgrades.
-db_version = 21
+db_version = 22
 
 def __mkreports(reports):
     """Utility function used to create report data in same syntax as the
@@ -57,6 +57,9 @@
         Column('authenticated', type='int'),
         Column('name'),
         Column('value')],
+    Table('cache', key='key')[
+        Column('key'),
+        Column('generation')],
 
     # Attachments
     Table('attachment', key=('type', 'id', 'filename'))[
diff --git a/trac/env.py b/trac/env.py
--- a/trac/env.py
+++ b/trac/env.py
@@ -25,6 +25,7 @@
 
 from trac import db_default
 from trac.admin import AdminCommandError, IAdminCommandProvider
+from trac.cache import CacheManager
 from trac.config import *
 from trac.core import Component, ComponentManager, implements, Interface, \
                       ExtensionPoint, TracError
@@ -583,6 +584,8 @@
                 env = None
             if env is None:
                 env = env_cache.setdefault(env_path, open_environment(env_path))
+            else:
+                CacheManager(env).reset_metadata()
         finally:
             env_cache_lock.release()
     else:
diff --git a/trac/ticket/api.py b/trac/ticket/api.py
--- a/trac/ticket/api.py
+++ b/trac/ticket/api.py
@@ -16,13 +16,10 @@
 
 import re
 from datetime import datetime
-try:
-    import threading
-except ImportError:
-    import dummy_threading as threading
 
 from genshi.builder import tag
 
+from trac.cache import cached, cached_value
 from trac.config import *
 from trac.core import *
 from trac.perm import IPermissionRequestor, PermissionCache, PermissionSystem
@@ -157,13 +154,9 @@
         [TracTickets#Assign-toasDrop-DownList Assign-to as Drop-Down List]
         (''since 0.9'').""")
 
-    _fields = None
-    _custom_fields = None
-
     def __init__(self):
         self.log.debug('action controllers for ticket workflow: %r' % 
                 [c.__class__.__name__ for c in self.action_controllers])
-        self._fields_lock = threading.RLock()
 
     # Public API
 
@@ -192,29 +185,17 @@
 
     def get_ticket_fields(self):
         """Returns the list of fields available for tickets."""
-        # This is now cached - as it makes quite a number of things faster,
-        # e.g. #6436
-        if self._fields is None:
-            self._fields_lock.acquire()
-            try:
-                if self._fields is None: # double-check (race after 1st check)
-                    self._fields = self._get_ticket_fields()
-            finally:
-                self._fields_lock.release()
-        return [f.copy() for f in self._fields]
+        return [f.copy() for f in self.fields.get()]
 
-    def reset_ticket_fields(self):
-        self._fields_lock.acquire()
-        try:
-            self._fields = None
-            self.config.touch() # brute force approach for now
-        finally:
-            self._fields_lock.release()
+    def reset_ticket_fields(self, db=None):
+        """Invalidate ticket field cache."""
+        self.fields.invalidate(db)
 
-    def _get_ticket_fields(self):
+    @cached
+    def fields(self, db):
+        """Return the list of fields available for tickets."""
         from trac.ticket import model
 
-        db = self.env.get_db_cnx()
         fields = []
 
         # Basic text fields
@@ -290,16 +271,11 @@
                             'col', 'row', 'format', 'max', 'page', 'verbose']
 
     def get_custom_fields(self):
-        if self._custom_fields is None:
-            self._fields_lock.acquire()
-            try:
-                if self._custom_fields is None: # double-check
-                    self._custom_fields = self._get_custom_fields()
-            finally:
-                self._fields_lock.release()
-        return [f.copy() for f in self._custom_fields]
+        return [f.copy() for f in self.custom_fields]
 
-    def _get_custom_fields(self):
+    @cached_value
+    def custom_fields(self, db):
+        """Return the list of custom ticket fields available for tickets."""
         fields = []
         config = self.config['ticket-custom']
         for name in [option for option, value in config.options()
diff --git a/trac/ticket/model.py b/trac/ticket/model.py
--- a/trac/ticket/model.py
+++ b/trac/ticket/model.py
@@ -418,12 +418,12 @@
                     enum.update(db=db)
             except ValueError:
                 pass # Ignore cast error for this non-essential operation
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
         self.value = self._old_value = None
         self.name = self._old_name = None
-        TicketSystem(self.env).reset_ticket_fields()
 
     def insert(self, db=None):
         assert not self.exists, 'Cannot insert existing %s' % self.type
@@ -444,12 +444,12 @@
             self.value = int(float(cursor.fetchone()[0])) + 1
         cursor.execute("INSERT INTO enum (type,name,value) VALUES (%s,%s,%s)",
                        (self.type, self.name, self.value))
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
         self._old_name = self.name
         self._old_value = self.value
-        TicketSystem(self.env).reset_ticket_fields()
 
     def update(self, db=None):
         assert self.exists, 'Cannot update non-existent %s' % self.type
@@ -471,12 +471,12 @@
             cursor.execute("UPDATE ticket SET %s=%%s WHERE %s=%%s" %
                            (self.ticket_col, self.ticket_col),
                            (self.name, self._old_name))
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
         self._old_name = self.name
         self._old_value = self.value
-        TicketSystem(self.env).reset_ticket_fields()
 
     @classmethod
     def select(cls, env, db=None):
@@ -561,10 +561,10 @@
         cursor.execute("DELETE FROM component WHERE name=%s", (self.name,))
 
         self.name = self._old_name = None
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
-        TicketSystem(self.env).reset_ticket_fields()
 
     def insert(self, db=None):
         assert not self.exists, 'Cannot insert existing component'
@@ -581,10 +581,10 @@
         cursor.execute("INSERT INTO component (name,owner,description) "
                        "VALUES (%s,%s,%s)",
                        (self.name, self.owner, self.description))
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
-        TicketSystem(self.env).reset_ticket_fields()
 
     def update(self, db=None):
         assert self.exists, 'Cannot update non-existent component'
@@ -607,10 +607,10 @@
             cursor.execute("UPDATE ticket SET component=%s WHERE component=%s",
                            (self.name, self._old_name))
             self._old_name = self.name
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
-        TicketSystem(self.env).reset_ticket_fields()
 
     @classmethod
     def select(cls, env, db=None):
@@ -688,10 +688,10 @@
             ticket['milestone'] = retarget_to
             ticket.save_changes(author, 'Milestone %s deleted' % self.name,
                                 now, db=db)
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
-        TicketSystem(self.env).reset_ticket_fields()
 
     def insert(self, db=None):
         assert self.name, 'Cannot create milestone with no name'
@@ -708,10 +708,10 @@
                        "VALUES (%s,%s,%s,%s)",
                        (self.name, to_timestamp(self.due), to_timestamp(self.completed),
                         self.description))
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
-        TicketSystem(self.env).reset_ticket_fields()
 
     def update(self, db=None):
         assert self.name, 'Cannot update milestone with no name'
@@ -734,10 +734,10 @@
         cursor.execute("UPDATE ticket SET milestone=%s WHERE milestone=%s",
                        (self.name, self._old_name))
         self._old_name = self.name
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
-        TicketSystem(self.env).reset_ticket_fields()
 
     @classmethod
     def select(cls, env, include_completed=True, db=None):
@@ -814,10 +814,10 @@
         cursor.execute("DELETE FROM version WHERE name=%s", (self.name,))
 
         self.name = self._old_name = None
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
-        TicketSystem(self.env).reset_ticket_fields()
 
     def insert(self, db=None):
         assert not self.exists, 'Cannot insert existing version'
@@ -834,10 +834,10 @@
         cursor.execute("INSERT INTO version (name,time,description) "
                        "VALUES (%s,%s,%s)",
                        (self.name, to_timestamp(self.time), self.description))
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
-        TicketSystem(self.env).reset_ticket_fields()
 
     def update(self, db=None):
         assert self.exists, 'Cannot update non-existent version'
@@ -860,10 +860,10 @@
             cursor.execute("UPDATE ticket SET version=%s WHERE version=%s",
                            (self.name, self._old_name))
             self._old_name = self.name
+        TicketSystem(self.env).reset_ticket_fields(db)
 
         if handle_ta:
             db.commit()
-        TicketSystem(self.env).reset_ticket_fields()
 
     @classmethod
     def select(cls, env, db=None):
diff --git a/trac/upgrades/db22.py b/trac/upgrades/db22.py
new file mode 100644
--- /dev/null
+++ b/trac/upgrades/db22.py
@@ -0,0 +1,11 @@
+from trac.db import Table, Column, DatabaseManager
+
+def do_upgrade(env, ver, cursor):
+    """Add the cache table."""
+    table = Table('cache', key='key')[
+        Column('key'),
+        Column('generation')
+    ]
+    db_connector, _ = DatabaseManager(env)._get_connector()
+    for stmt in db_connector.to_sql(table):
+        cursor.execute(stmt)
diff --git a/trac/wiki/api.py b/trac/wiki/api.py
--- a/trac/wiki/api.py
+++ b/trac/wiki/api.py
@@ -16,17 +16,13 @@
 # Author: Jonas Borgström <jonas@edgewall.com>
 #         Christopher Lenz <cmlenz@gmx.de>
 
-try:
-    import threading
-except ImportError:
-    import dummy_threading as threading
-import time
 import urllib
 import re
 from StringIO import StringIO
 
 from genshi.builder import tag
 
+from trac.cache import cached_value
 from trac.config import BoolOption
 from trac.core import *
 from trac.resource import IResourceManager
@@ -164,8 +160,6 @@
     macro_providers = ExtensionPoint(IWikiMacroProvider)
     syntax_providers = ExtensionPoint(IWikiSyntaxProvider)
 
-    INDEX_UPDATE_INTERVAL = 5 # seconds
-
     ignore_missing_pages = BoolOption('wiki', 'ignore_missing_pages', 'false',
         """Enable/disable highlighting CamelCase links to missing pages
         (''since 0.9'').""")
@@ -182,26 +176,12 @@
         For public sites where anonymous users can edit the wiki it is
         recommended to leave this option disabled (which is the default).""")
 
-    def __init__(self):
-        self._index = None
-        self._last_index_update = 0
-        self._index_lock = threading.RLock()
-
-    def _update_index(self):
-        self._index_lock.acquire()
-        try:
-            now = time.time()
-            if now > self._last_index_update + WikiSystem.INDEX_UPDATE_INTERVAL:
-                self.log.debug('Updating wiki page index')
-                db = self.env.get_db_cnx()
-                cursor = db.cursor()
-                cursor.execute("SELECT DISTINCT name FROM wiki")
-                self._index = {}
-                for (name,) in cursor:
-                    self._index[name] = True
-                self._last_index_update = now
-        finally:
-            self._index_lock.release()
+    @cached_value
+    def pages(self, db):
+        """Return the names of all existing wiki pages."""
+        cursor = db.cursor()
+        cursor.execute("SELECT DISTINCT name FROM wiki")
+        return [name for (name,) in cursor]
 
     # Public API
 
@@ -211,32 +191,26 @@
         If the `prefix` parameter is given, only names that start with that
         prefix are included.
         """
-        self._update_index()
-        # Note: use of keys() is intentional since iterkeys() is prone to
-        # errors with concurrent modification
-        for page in self._index.keys():
+        for page in self.pages:
             if not prefix or page.startswith(prefix):
                 yield page
 
     def has_page(self, pagename):
         """Whether a page with the specified name exists."""
-        self._update_index()
-        return self._index.has_key(pagename.rstrip('/'))
+        return pagename.rstrip('/') in self.pages
 
     # IWikiChangeListener methods
 
     def wiki_page_added(self, page):
         if not self.has_page(page.name):
-            self.log.debug('Adding page %s to index' % page.name)
-            self._index[page.name] = True
+            del self.pages
 
     def wiki_page_changed(self, page, version, t, comment, author, ipnr):
         pass
 
     def wiki_page_deleted(self, page):
         if self.has_page(page.name):
-            self.log.debug('Removing page %s from index' % page.name)
-            del self._index[page.name]
+            del self.pages
 
     def wiki_page_version_deleted(self, page):
         pass
diff --git a/trac/wiki/interwiki.py b/trac/wiki/interwiki.py
--- a/trac/wiki/interwiki.py
+++ b/trac/wiki/interwiki.py
@@ -15,13 +15,10 @@
 # Author: Christian Boos <cboos@neuf.fr>
 
 import re
-try:
-    import threading
-except ImportError:
-    import dummy_threading as threading
 
 from genshi.builder import tag
 
+from trac.cache import cached_value
 from trac.core import *
 from trac.wiki.formatter import Formatter
 from trac.wiki.parser import WikiParser
@@ -37,18 +34,8 @@
     _interwiki_re = re.compile(r"(%s)[ \t]+([^ \t]+)(?:[ \t]+#(.*))?" %
                                WikiParser.LINK_SCHEME, re.UNICODE)
     _argspec_re = re.compile(r"\$\d")
-    _interwiki_map = None
 
-    def __init__(self):
-        self._interwiki_lock = threading.RLock()
-
-    def reset(self):
-        self._interwiki_map = None
-        self.config.touch()
-        # This dictionary maps upper-cased namespaces
-        # to (namespace, prefix, title) values;
-
-    # The component itself behaves as a map
+    # The component itself behaves as a read-only map
 
     def __contains__(self, ns):
         return ns.upper() in self.interwiki_map
@@ -56,9 +43,6 @@
     def __getitem__(self, ns):
         return self.interwiki_map[ns.upper()]
 
-    def __setitem__(self, ns, value):
-        self.interwiki_map[ns.upper()] = value
-
     def keys(self):
         return self.interwiki_map.keys()
 
@@ -102,42 +86,39 @@
 
     def wiki_page_changed(self, page, version, t, comment, author, ipnr):
         if page.name == InterWikiMap._page_name:
-            self.reset()
+            del self.interwiki_map
 
     def wiki_page_deleted(self, page):
         if page.name == InterWikiMap._page_name:
-            self.reset()
+            del self.interwiki_map
 
     def wiki_page_version_deleted(self, page):
         if page.name == InterWikiMap._page_name:
-            self.reset()
+            del self.interwiki_map
 
-    def _get_interwiki_map(self):
+    @cached_value
+    def interwiki_map(self, db):
+        """Map from upper-cased namespaces to (namespace, prefix, title) 
+        values.
+        """
         from trac.wiki.model import WikiPage
-        if self._interwiki_map is None:
-            self._interwiki_lock.acquire()
-            try:
-                if self._interwiki_map is None:
-                    self._interwiki_map = {}
-                    content = WikiPage(self.env, InterWikiMap._page_name).text
+        map = {}
+        content = WikiPage(self.env, InterWikiMap._page_name, db=db).text
+        in_map = False
+        for line in content.split('\n'):
+            if in_map:
+                if line.startswith('----'):
                     in_map = False
-                    for line in content.split('\n'):
-                        if in_map:
-                            if line.startswith('----'):
-                                in_map = False
-                            else:
-                                m = re.match(InterWikiMap._interwiki_re, line)
-                                if m:
-                                    prefix, url, title = m.groups()
-                                    url = url.strip()
-                                    title = title and title.strip() or prefix
-                                    self[prefix] = (prefix, url, title)
-                        elif line.startswith('----'):
-                            in_map = True
-            finally:
-                self._interwiki_lock.release()
-        return self._interwiki_map
-    interwiki_map = property(_get_interwiki_map)
+                else:
+                    m = re.match(InterWikiMap._interwiki_re, line)
+                    if m:
+                        prefix, url, title = m.groups()
+                        url = url.strip()
+                        title = title and title.strip() or prefix
+                        map[prefix.upper()] = (prefix, url, title)
+            elif line.startswith('----'):
+                in_map = True
+        return map
 
     # IWikiMacroProvider methods
 

