MST

星途 面试题库

面试题:缓存设计之并发控制基础

在缓存设计中,当多个并发请求同时尝试读取和修改缓存数据时,可能会出现什么问题?请简述两种常见的并发控制方法及其原理。
13.1万 热度难度
后端开发缓存设计

知识考点

AI 面试

面试题答案

一键面试

可能出现的问题

  1. 缓存击穿:指一个存在于缓存中的热点数据,在某个时刻,其缓存过期,此时大量并发请求同时尝试读取该数据,发现缓存中没有,就会同时去数据库查询,给数据库带来巨大压力,甚至可能导致数据库崩溃。
  2. 缓存雪崩:大量缓存数据在同一时间过期失效,大量并发请求同时查询数据库,导致数据库压力骤增,甚至可能使数据库和应用系统崩溃。这通常是由于缓存服务器重启、缓存数据批量过期等原因引起。
  3. 数据不一致:多个并发请求同时读取缓存数据,然后其中一些请求对数据进行修改并写回缓存,由于并发操作的无序性,可能导致最终缓存中的数据不是最新的、正确的数据,出现数据不一致的情况。

常见的并发控制方法及其原理

  1. 互斥锁(Mutex)
    • 原理:在多个并发请求尝试读取和修改缓存数据时,使用互斥锁来保证同一时间只有一个请求能够对缓存进行操作。当一个请求获取到互斥锁后,其他请求需要等待该请求释放互斥锁才能进行操作。在代码实现中,可以使用操作系统提供的线程同步机制(如POSIX互斥锁、Windows临界区等)或者编程语言自带的锁机制(如Java的synchronized关键字、Python的threading.Lock)。例如在Java中:
public class Cache {
    private static final Object mutex = new Object();
    private static Map<String, Object> cache = new HashMap<>();

    public static Object get(String key) {
        synchronized (mutex) {
            return cache.get(key);
        }
    }

    public static void put(String key, Object value) {
        synchronized (mutex) {
            cache.put(key, value);
        }
    }
}
- **优点**:实现简单,能够有效避免并发操作导致的数据不一致问题。
- **缺点**:性能较低,因为同一时间只有一个请求能操作缓存,在高并发场景下,大量请求会被阻塞等待锁的释放,可能成为系统的性能瓶颈。

2. 乐观锁 - 原理:乐观锁假设在大多数情况下,并发操作不会发生冲突。在读取数据时,不进行加锁操作,而是在更新数据时,检查数据在读取后是否被其他线程修改。通常的做法是在数据中添加一个版本号(Version)或者时间戳(Timestamp)。当一个请求读取数据时,同时获取数据的版本号。在更新数据时,将当前版本号与数据库中最新的版本号进行比较,如果版本号相同,则说明数据在读取后没有被其他线程修改,可以进行更新操作,并将版本号加1;如果版本号不同,则说明数据已被其他线程修改,放弃本次更新操作,重新读取数据并再次尝试更新。例如在数据库层面,如果使用SQL语句实现乐观锁更新:

-- 假设表结构为 cache_table (id, value, version)
-- 读取数据及版本号
SELECT value, version FROM cache_table WHERE id =?;
-- 更新数据,同时检查版本号
UPDATE cache_table SET value =?, version = version + 1 WHERE id =? AND version =?;

在代码层面(以Java为例):

public class Cache {
    private static Map<String, CacheEntry> cache = new HashMap<>();

    public static class CacheEntry {
        Object value;
        int version;

        public CacheEntry(Object value, int version) {
            this.value = value;
            this.version = version;
        }
    }

    public static CacheEntry get(String key) {
        return cache.get(key);
    }

    public static boolean put(String key, Object value, int expectedVersion) {
        CacheEntry entry = cache.get(key);
        if (entry == null || entry.version != expectedVersion) {
            return false;
        }
        entry.value = value;
        entry.version++;
        cache.put(key, entry);
        return true;
    }
}
- **优点**:性能较高,因为读取操作不加锁,并发性能好,适用于读多写少的场景。
- **缺点**:如果并发冲突频繁发生,会导致大量更新操作失败,需要重试,增加系统开销。同时,实现相对复杂,需要额外维护版本号或时间戳。