避免缓存穿透和击穿:保护数据库的小秘密

在这里插入图片描述

数据库缓存一致性是指将数据库中的数据复制到缓存中时,要保证缓存数据与数据库数据的一致性。以下是一些常见的数据库缓存一致性问题:

1. 缓存穿透

当一个请求查询数据库中不存在的数据时,由于缓存中没有该数据,请求会穿透到数据库。为了防止缓存穿透,可以在缓存中设置一个空值来表示数据库中不存在该数据。

当一个请求查询数据库中不存在的数据时,会发生缓存穿透。以下是一个缓存穿透的代码案例,使用Java语言和Redis作为缓存实现:

import redis.clients.jedis.Jedis;

public class CacheExample {
    private Jedis jedis; // Redis客户端
    
    public CacheExample() {
        // 初始化Redis连接
        jedis = new Jedis("localhost", 6379);
    }
    
    public String getDataFromCache(String key) {
        // 先尝试从缓存中获取数据
        String data = jedis.get(key);
        
        if (data == null) {
            // 缓存中不存在该数据,需要查询数据库
            data = fetchDataFromDatabase(key);
            
            // 将查询结果存入缓存,设置过期时间
            if (data != null) {
                jedis.setex(key, 3600, data); // 设置缓存数据并设置过期时间为1小时
            } else {
                // 数据库中不存在该数据,设置一个空值到缓存,避免缓存穿透
                jedis.setex(key, 60, ""); // 设置空值并设置过期时间为1分钟
            }
        }
        
        return data;
    }
    
    private String fetchDataFromDatabase(String key) {
        // 模拟从数据库中查询数据
        // 这里省略具体的数据库查询逻辑,直接返回null表示数据不存在
        
        return null;
    }
    
    public static void main(String[] args) {
        CacheExample example = new CacheExample();
        
        // 请求查询一个不存在的数据
        String result = example.getDataFromCache("nonexistent_key");
        System.out.println("查询结果:" + result);
    }
}

在上述代码中,先尝试从缓存中获取数据,如果缓存中不存在该数据,则查询数据库。如果数据库中也不存在该数据,将设置一个空值到缓存,并设置短暂的过期时间,以防止缓存穿透。这样,在接下来的一段时间内,对于相同的查询请求,都会直接从缓存中获取一个空值,避免了对数据库的频繁查询。

2. 缓存击穿

当某个热点数据在缓存中过期或被删除时,大量的请求同时涌入,导致所有的请求都穿透到数据库,给数据库造成压力。解决缓存击穿的一种方法是使用互斥锁或分布式锁,在缓存失效的情况下只允许一个请求去查询数据库并更新缓存,其他请求等待获取缓存数据。

当某个热点数据过期或被删除时,会发生缓存击穿。以下是一个缓存击穿的代码案例,使用Java语言和Redis作为缓存实现:

import redis.clients.jedis.Jedis;

public class CacheExample {
    private Jedis jedis; // Redis客户端
    
    public CacheExample() {
        // 初始化Redis连接
        jedis = new Jedis("localhost", 6379);
    }
    
    public String getDataFromCache(String key) {
        // 先尝试从缓存中获取数据
        String data = jedis.get(key);
        
        if (data == null) {
            // 缓存中不存在该数据,需要查询数据库
            data = fetchDataFromDatabase(key);
            
            // 将查询结果存入缓存,设置过期时间
            if (data != null) {
                jedis.setex(key, 3600, data); // 设置缓存数据并设置过期时间为1小时
            }
        }
        
        return data;
    }
    
    private String fetchDataFromDatabase(String key) {
        // 模拟从数据库中查询数据
        // 这里省略具体的数据库查询逻辑,直接返回null表示数据不存在
        
        return null;
    }
    
    public static void main(String[] args) {
        CacheExample example = new CacheExample();
        
        // 请求查询一个热点数据
        String result = example.getDataFromCache("hot_data");
        System.out.println("查询结果:" + result);
    }
}

在上述代码中,先尝试从缓存中获取数据,如果缓存中不存在该数据,则查询数据库。如果数据库中存在该数据,将查询结果存入缓存,并设置适当的过期时间。这样,在接下来的一段时间内,相同的查询请求都可以直接从缓存中获取数据,避免了对数据库的频繁查询。

然而,如果在查询某个热点数据时,该数据恰好过期并被删除,此时会出现缓存击穿的情况。为了解决这个问题,可以使用互斥锁(或分布式锁)来控制对数据库的并发访问。

以下是修改后的代码示例,加入了互斥锁保护:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

public class CacheExample {
    private Jedis jedis; // Redis客户端
    private final String LOCK_KEY = "lock"; // 锁的键名
    private final int LOCK_EXPIRE_TIME = 60; // 锁的过期时间(秒)
    private final int RETRY_INTERVAL = 100; // 获取锁失败后的重试间隔时间(毫秒)
    
    public CacheExample() {
        // 初始化Redis连接
        jedis = new Jedis("localhost", 6379);
    }
    
    public String getDataFromCache(String key) {
        // 先尝试从缓存中获取数据
        String data = jedis.get(key);
        
        if (data == null) {
            // 缓存中不存在该数据,需要查询数据库
            data = fetchDataFromDatabase(key);
            
            // 将查询结果存入缓存,设置过期时间
            if (data != null) {
                jedis.setex(key, 3600, data); // 设置缓存数据并设置过期时间为1小时
            }
        }
        
        return data;
    }
    
    private String fetchDataFromDatabase(String key) {
        // 模拟从数据库中查询数据
        // 这里省略具体的数据库查询逻辑,直接返回null表示数据不存在
        
        return null;
    }
    
    private boolean acquireLock(String lockKey) {
        // 使用SET命令获取锁
        String result = jedis.set(lockKey, "", SetParams.setParams().nx().ex(LOCK_EXPIRE_TIME));
        return "OK".equals(result);
    }
    
    private void releaseLock(String lockKey) {
        // 使用DEL命令释放锁
        jedis.del(lockKey);
    }
    
    public String getDataFromCacheWithLock(String key) {
        String data = jedis.get(key);
        
        if (data == null) {
            // 获取互斥锁
            boolean locked = false;
            while (!locked) {
                locked = acquireLock(LOCK_KEY);
                if (!locked) {
                    try {
                        Thread.sleep(RETRY_INTERVAL);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            
            try {
                // 再次尝试从缓存中获取数据
                data = jedis.get(key);
                
                if (data == null) {
                    // 缓存中仍然不存在该数据,则查询数据库
                    data = fetchDataFromDatabase(key);
                    
                    // 将查询结果存入缓存,设置过期时间
                    if (data != null) {
                        jedis.setex(key, 3600, data); // 设置缓存数据并设置过期时间为1小时
                    }
                }
            } finally {
                // 释放互斥锁
                releaseLock(LOCK_KEY);
            }
        }
        
        return data;
    }
    
    public static void main(String[] args) {
        CacheExample example = new CacheExample();
        
        // 请求查询一个热点数据
        String result = example.getDataFromCacheWithLock("hot_data");
        System.out.println("查询结果:" + result);
    }
}

在上述修改后的代码中,加入了acquireLock()releaseLock()方法来获取和释放互斥锁。使用互斥锁时,首先尝试获取锁,如果获取失败,则通过循环重试的方式等待锁的释放。获取到锁后,再次尝试从缓存中获取数据,以防在等待锁的过程中其他线程已经完成了数据库查询并将数据存入缓存。最后,在使用完数据后记得释放互斥锁,以便其他线程可以获取到锁并进行操作。这样可以避免缓存击穿问题的发生。

3. 缓存雪崩

当缓存中的大量数据在同一时间失效,而数据库又无法承受所有请求的压力时,就会出现缓存雪崩。为了防止缓存雪崩,可以设置缓存数据的过期时间随机化,使得不同的数据在不同的时间过期,减少缓存同时失效的概率。

缓存雪崩是指在某个时间点,大部分或全部缓存同时失效,导致大量请求直接访问数据库,从而压垮数据库。以下是一个可能导致缓存雪崩的代码示例:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

public class CacheExample {
    private Jedis jedis; // Redis客户端
    
    public CacheExample() {
        // 初始化Redis连接
        jedis = new Jedis("localhost", 6379);
    }
    
    public String getDataFromCache(String key) {
        // 先尝试从缓存中获取数据
        String data = jedis.get(key);
        
        if (data == null) {
            // 缓存中不存在该数据,需要查询数据库
            data = fetchDataFromDatabase(key);
            
            // 将查询结果存入缓存,设置过期时间
            if (data != null) {
                jedis.setex(key, 3600, data); // 设置缓存数据并设置过期时间为1小时
            }
        }
        
        return data;
    }
    
    private String fetchDataFromDatabase(String key) {
        // 模拟从数据库中查询数据
        // 这里省略具体的数据库查询逻辑,直接返回null表示数据不存在
        
        return null;
    }
    
    public static void main(String[] args) {
        CacheExample example = new CacheExample();
        
        // 请求查询一个热点数据
        String result = example.getDataFromCache("hot_data");
        System.out.println("查询结果:" + result);
    }
}

在上述代码中,当缓存中不存在需要查询的数据时,会直接查询数据库,并将结果存入缓存。但是,如果在某个时间点,大量数据的缓存同时过期,此时大量请求会同时访问数据库,导致数据库压力过大,甚至可能导致数据库崩溃。

为了解决缓存雪崩问题,可以采取以下措施:

  1. 设置合适的缓存过期时间:将缓存的过期时间设置为随机值,避免大量缓存同时失效。
  2. 针对热点数据进行热身加载:在缓存失效前,提前加载热点数据到缓存中,以保证热点数据始终可用。
  3. 使用互斥锁(或分布式锁):在获取缓存数据时,使用互斥锁保护,避免多个线程同时去查询数据库。
  4. 多级缓存架构:引入多级缓存架构,将热点数据缓存在多个不同层次的缓存中,以提升系统的稳定性和并发能力。

修改后的代码示例如下,增加了热身加载和互斥锁保护:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

public class CacheExample {
    private Jedis jedis; // Redis客户端
    
    public CacheExample() {
        // 初始化Redis连接
        jedis = new Jedis("localhost", 6379);
    }
    
    public String getDataFromCache(String key) {
        // 先尝试从缓存中获取数据
        String data = jedis.get(key);
        
        if (data == null) {
            // 获取互斥锁
            boolean locked = acquireLock(key);
            if (locked) {
                try {
                    // 再次尝试从缓存中获取数据
                    data = jedis.get(key);
                    
                    if (data == null) {
                        // 缓存中仍然不存在该数据,则查询数据库
                        data = fetchDataFromDatabase(key);
                        
                        // 将查询结果存入缓存,设置过期时间
                        if (data != null) {
                            jedis.setex(key, 3600, data); // 设置缓存数据并设置过期时间为1小时
                        }
                    }
                } finally {
                    // 释放互斥锁
                    releaseLock(key);
                }
            } else {
                // 等待一段时间后重试
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                // 递归调用获取数据的方法
                data = getDataFromCache(key);
            }
        }
        
        return data;
    }
    
    private String fetchDataFromDatabase(String key) {
        // 模拟从数据库中查询数据
        // 这里省略具体的数据库查询逻辑,直接返回null表示数据不存在
        
        return null;
    }
    
    private boolean acquireLock(String lockKey) {
        // 使用SET命令获取锁
        String result = jedis.set(lockKey, "", SetParams.setParams().nx().ex(60));
        return "OK".equals(result);
    }
    
    private void releaseLock(String lockKey) {
        // 使用DEL命令释放锁
        jedis.del(lockKey);
    }
    
    public static void main(String[] args) {
        CacheExample example = new CacheExample();
        
        // 请求查询一个热点数据
        String result = example.getDataFromCache("hot_data");
        System.out.println("查询结果:" + result);
    }
}

在上述修改后的代码中,增加了acquireLock()releaseLock()方法来获取和释放互斥锁。当缓存中不存在需要查询的数据时,先尝试获取互斥锁。如果获取到锁,则再次尝试从缓存中获取数据,如果仍然不存在则查询数据库,并将结果存入缓存。获取数据完成后,记得释放互斥锁。如果获取锁失败,则等待一段时间后再次尝试获取数据,以避免大量线程同时去查询数据库。这样可以有效地解决缓存雪崩问题。

4. 更新缓存的一致性

当数据库中的数据发生更新或删除时,需要及时更新或删除缓存中对应的数据,以保证缓存数据与数据库数据的一致性。可以使用缓存的读写策略,在数据库更新后主动更新或删除缓存数据。

更新缓存的一致性是指在更新数据库数据后,及时更新缓存数据,以保证缓存中的数据与数据库的数据一致。以下是一个示例代码:

import redis.clients.jedis.Jedis;

public class CacheConsistencyExample {
    private Jedis jedis; // Redis客户端
    
    public CacheConsistencyExample() {
        // 初始化Redis连接
        jedis = new Jedis("localhost", 6379);
    }
    
    public void updateData(String key, String newData) {
        // 更新数据库中的数据
        
        // 省略具体的更新数据库逻辑
        
        // 更新缓存中的数据
        jedis.set(key, newData);
    }
    
    public String getData(String key) {
        // 先从缓存中获取数据
        String data = jedis.get(key);
        
        if (data == null) {
            // 缓存中不存在该数据,则从数据库中获取数据
            data = fetchDataFromDatabase(key);
            
            // 将查询结果存入缓存
            if (data != null) {
                jedis.set(key, data);
            }
        }
        
        return data;
    }
    
    private String fetchDataFromDatabase(String key) {
        // 模拟从数据库中查询数据
        // 这里省略具体的数据库查询逻辑,直接返回null表示数据不存在
        
        return null;
    }
    
    public static void main(String[] args) {
        CacheConsistencyExample example = new CacheConsistencyExample();
        
        // 查询一个数据
        String key = "data_key";
        String result = example.getData(key);
        System.out.println("查询结果:" + result);
        
        // 更新数据
        String newData = "new_data";
        example.updateData(key, newData);
        
        // 再次查询数据,此时应该从缓存中获取数据,而不是去查询数据库
        result = example.getData(key);
        System.out.println("更新后的结果:" + result);
    }
}

在上述代码中,updateData()方法用于更新数据库中的数据,并在更新后将最新数据存入缓存中。getData()方法用于查询数据,先尝试从缓存中获取数据,如果缓存中不存在,则从数据库中查询,并将结果存入缓存。

在示例代码的main()方法中,首先查询一个数据,如果数据存在于缓存中,则直接返回缓存中的数据;否则,从数据库中查询,并将结果存入缓存。然后,通过调用updateData()方法来更新数据库中的数据,并且更新缓存中的数据。最后,再次查询同样的数据,这时应该直接从缓存中获取数据,而不需要再查询数据库。

通过及时更新缓存数据,可以保证缓存中的数据与数据库的数据一致性,提高系统的性能和响应速度。

为了确保数据库缓存的一致性,需要结合业务场景和具体的技术选型,采取合适的缓存策略和高可用方案。常用的缓存技术包括Redis、Memcached等,而在分布式系统中,可以使用一致性哈希算法或一致性哈希环来进行数据分片和负载均衡,从而提高缓存的可靠性和可扩展性。此外,定期监控缓存和数据库的状态,并根据需求进行调优和扩容,也是确保数据库缓存一致性的重要措施之一。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值