Skip to content

缓存方案

在高并发场景下,缓存技术是优化系统性能、降低数据库压力的重要手段。

关键策略

  1. 数据缓存

    • 静态内容缓存:对于不经常变化的静态资源如图片、CSS、JavaScript等,可以通过CDN(内容分发网络)或服务器本地缓存来减少对服务器的请求。
    • 数据库查询结果缓存:使用Redis、Memcached或其他内存数据库作为缓存中间件,将热点查询的结果存储起来,当接收到相同的请求时,直接从缓存获取结果,避免频繁访问数据库。
  2. 对象缓存

    • 在处理用户会话、商品详情等数据时,可以将这些对象序列化后放入缓存中,后续请求可以直接读取缓存中的对象,提升响应速度。
  3. LRU(Least Recently Used)算法

    • 当缓存容量有限时,采用LRU等淘汰策略,确保最近最常使用的数据被保留在缓存中。
  4. 分层缓存

    • 可以设置多级缓存结构,例如先检查本地缓存(如Tomcat的session缓存),未命中则查询分布式缓存(如Redis),再未命中才去数据库查询。这样可以有效减少数据库负载。
  5. 失效策略与缓存更新

    • 设计合理的缓存失效策略,如设定过期时间或结合数据库事件进行缓存刷新(如MySQL的binlog监听实现同步更新缓存)。
    • 对于写操作较多的场景,可采用“写后清除”或者“写后更新”策略,在更新数据库的同时更新或删除缓存。
  6. 预加载与异步加载

    • 对于预期访问量较大的数据,可以在后台提前加载到缓存中。
    • 对于耗时较长但非即时返回的数据,可以先返回缓存中的旧数据并异步更新缓存。
  7. 缓存雪崩和穿透的防护

    • 缓存雪崩:通过合理的失效时间分散策略以及备用集群防止所有缓存同时失效导致数据库崩溃。
    • 缓存穿透:对不存在的数据也设置一个默认值,避免每次请求都穿透到数据库;也可以设置布隆过滤器预先拦截无效的请求。
  8. 分布式缓存一致性

    • 对于分布式环境下的缓存,需要考虑如何保证缓存的一致性,比如使用分布式锁机制保证写入和失效操作的原子性。

缓存特征

特征描述
命中率缓存命中数与总访问次数之比,用于衡量缓存效率的指标。
最大元素缓存可以存储的最大元素数量,即缓存空间的大小。
清空策略一种用于管理缓存中元素的策略,常见的包括 FIFO、LFU、LRU、过期时间和随机等。

命中率:计算公式为命中数 / (命中数 + 未命中数),用来衡量缓存对于请求的命中情况,命中率越高表示缓存效率越好。

最大元素:表示缓存所能存储的最大元素数量,超过这个数量后需要根据清空策略来进行淘汰。

清空策略:用于管理缓存中的元素,常见的策略有:

  • FIFO(先进先出):按照元素进入缓存的顺序进行清除,最早进入的元素最先被清除。
  • LFU(最少使用):清除使用频率最低的元素。
  • LRU(最近最少使用):清除最近最少被使用的元素,即最长时间没有被访问的元素。
  • 过期时间:设置每个缓存元素的过期时间,在超过过期时间后自动清除。
  • 随机:随机选择要清除的元素。

这些清空策略可以根据具体的需求和应用场景进行选择和配置,以达到最佳的缓存效果。

缓存命中率影响因素

  1. 缓存策略:缓存策略的选择直接影响缓存命中率。常见的缓存策略包括 FIFO、LFU、LRU、过期时间和随机等,不同的策略适用于不同的场景,选择合适的缓存策略可以提高命中率。

  2. 缓存空间大小:缓存空间大小决定了可以缓存的元素数量,空间大小不足会导致缓存淘汰频繁,影响命中率。合理设置缓存空间大小可以提高命中率。

  3. 缓存键设计:缓存键的设计直接影响命中率。良好的缓存键设计可以提高缓存的命中率,减少不必要的缓存淘汰和访问开销。

  4. 数据访问模式:数据访问模式是指数据的访问频率和访问顺序。如果数据访问模式规律性强,缓存命中率可能会较高;反之,如果数据访问模式随机或呈现周期性变化,缓存命中率可能会较低。

  5. 缓存预热:缓存预热是指在系统启动或服务升级后,提前将热点数据加载到缓存中,避免冷启动时的缓存未命中现象,从而提高缓存命中率。

  6. 缓存失效策略:缓存失效策略是指缓存中数据失效的规则,不合理的失效策略会导致缓存中的数据过早失效,影响命中率。合理设置失效策略可以提高缓存命中率。

  7. 缓存穿透和击穿:缓存穿透是指查询不存在于缓存和数据库中的数据,缓存击穿是指缓存中的热点数据突然失效,导致大量请求直接打到数据库。缓存穿透和击穿会导致缓存命中率下降,影响系统性能。

常用的缓存方案

  1. 内存缓存:将数据缓存在应用程序的内存中,例如使用 HashMap、ConcurrentHashMap 等数据结构来实现简单的内存缓存。内存缓存具有读写速度快的优点,适用于数据量不大且对数据实时性要求高的场景。

  2. 本地缓存:将数据缓存在本地文件系统或数据库中,例如使用 SQLite、H2、LevelDB 等本地数据库或者将数据序列化到文件中。本地缓存适用于单机应用或者需要长期保存数据的场景。

  3. 分布式缓存:将数据缓存在多台服务器上,以实现数据的分布式存储和访问。常见的分布式缓存方案包括 Redis、Memcached、Hazelcast 等。分布式缓存适用于大规模应用或者需要横向扩展的场景,可以提高系统的性能和可扩展性。

  4. 反向代理缓存:通过反向代理服务器(如 Nginx、Varnish 等)将静态资源缓存起来,减轻后端服务器的压力。反向代理缓存适用于静态资源频繁访问的场景,可以提高网站的访问速度和稳定性。

  5. CDN 缓存:通过内容分发网络(CDN)将静态资源缓存在全球各地的节点上,加速用户对静态资源的访问。CDN 缓存适用于全球用户分布广泛、静态资源访问频繁的场景,可以提高网站的访问速度和稳定性。

GuavaCache

引入依赖

xml
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

使用方法-LoadingCache

java
public class GuavaCacheExample1 {

    public static void main(String[] args) {

        LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
                .maximumSize(10) // 最多存放10个数据
                .expireAfterWrite(10, TimeUnit.SECONDS) // 缓存10秒
                .recordStats() // 开启记录状态数据功能
                .build(new CacheLoader<String, Integer>() {
                    @Override
                    public Integer load(String key) throws Exception {
                        return -1;
                    }
                });

        log.info("{}", cache.getIfPresent("key1")); // null
        cache.put("key1", 1);
        log.info("{}", cache.getIfPresent("key1")); // 1
        cache.invalidate("key1");
        log.info("{}", cache.getIfPresent("key1")); // null

        try {
            log.info("{}", cache.get("key2")); // -1
            cache.put("key2", 2);
            log.info("{}", cache.get("key2")); // 2

            log.info("{}", cache.size()); // 1

            for (int i = 3; i < 13; i++) {
                cache.put("key" + i, i);
            }
            log.info("{}", cache.size()); // 10

            log.info("{}", cache.getIfPresent("key2")); // null,因为maximumSize=10,之前的数据被清除

            Thread.sleep(11000); // 10秒过期

            log.info("{}", cache.get("key5")); // -1

            log.info("{},{}", cache.stats().hitCount(), cache.stats().missCount());//命中率: 2,5

            log.info("{},{}", cache.stats().hitRate(), cache.stats().missRate());// 未命中率: 0.2857142857142857,0.7142857142857143
        } catch (Exception e) {
            log.error("cache exception", e);
        }
    }
}

输出

xml
21:14:15.721 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - null
21:14:15.731 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - 1
21:14:15.734 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - null
21:14:15.745 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - -1
21:14:15.745 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - 2
21:14:15.745 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - 1
21:14:15.748 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - 10
21:14:15.748 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - null
21:14:26.757 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - -1
21:14:26.757 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - 2,5
21:14:26.757 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample1 - 0.2857142857142857,0.7142857142857143

使用方法-Cache

java
public class GuavaCacheExample2 {

    public static void main(String[] args) {

        Cache<String, Integer> cache = CacheBuilder.newBuilder()
                .maximumSize(10) // 最多存放10个数据
                .expireAfterWrite(10, TimeUnit.SECONDS) // 缓存10秒
                .recordStats() // 开启记录状态数据功能
                .build();

        log.info("{}", cache.getIfPresent("key1")); // null
        cache.put("key1", 1);
        log.info("{}", cache.getIfPresent("key1")); // 1
        cache.invalidate("key1");
        log.info("{}", cache.getIfPresent("key1")); // null

        try {
            log.info("{}", cache.get("key2", new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    return -1;
                }
            })); // -1
            cache.put("key2", 2);
            log.info("{}", cache.get("key2", new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    return -1;
                }
            })); // 2

            log.info("{}", cache.size()); // 1

            for (int i = 3; i < 13; i++) {
                cache.put("key" + i, i);
            }
            log.info("{}", cache.size()); // 10

            log.info("{}", cache.getIfPresent("key2")); // null

            Thread.sleep(11000);

            log.info("{}", cache.get("key5", new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    return -1;
                }
            })); // -1

            log.info("{},{}", cache.stats().hitCount(), cache.stats().missCount());//命中率: 2,5

            log.info("{},{}", cache.stats().hitRate(), cache.stats().missRate());// 未命中率: 0.2857142857142857,0.7142857142857143
        } catch (Exception e) {
            log.error("cache exception", e);
        }
    }
}

输出

21:33:12.868 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - null
21:33:12.875 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - 1
21:33:12.877 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - null
21:33:12.886 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - -1
21:33:12.886 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - 2
21:33:12.887 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - 1
21:33:12.887 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - 10
21:33:12.887 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - null

21:33:23.897 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - -1
21:33:23.897 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - 2,5
21:33:23.897 [main] INFO cn.diyai.mul_thread.cache.GuavaCacheExample2 - 0.2857142857142857,0.7142857142857143

Redis

官网

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。

它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。

Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

Spring Boot集成Redis

添加依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置参数

yml
spring:
	redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    password: 
    timeout: 2000 #连接超时时间(毫秒)
    jedis:
      pool:
        max-active: 8 #连接池最大连接数(使用负值表示没有限制)
        max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-idle: 8  # 连接池中的最大空闲连接
        min-idle: 0 # 连接池中的最小空闲连接

配置类RedisConfig

java
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Autowired
    RedisTemplate redisTemplate;

    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }


    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

引用

java
@Autowired
RedisTemplate redisTemplate;

操作列表

java
// 左侧出队
redisTemplate.opsForList().leftPop(key);

// 右侧入队
redisTemplate.opsForList().rightPush(key,expiredKey)

操作String

java
// 赋值
redisTemplate.opsForValue().set(state,value,expire, TimeUnit.SECONDS);

// 获取值
redisTemplate.opsForValue().get(state);

高并发场景下缓存的常见问题

缓存一致性

缓存中的数据与数据源(如数据库)中的数据保持一致。

当缓存中的数据与数据源中的数据发生变化时,需要及时更新缓存,以确保数据的一致性。

常见的解决方案包括缓存更新、缓存失效、定时刷新等。

缓存并发问题

在高并发情况下,多个线程同时访问缓存可能导致的问题,如缓存雪崩、缓存穿透等。

为了解决缓存并发问题,可以采用加锁、使用分布式锁、限流等手段来控制并发访问。

缓存穿透问题

恶意用户或者恶意攻击利用缓存失效的特性,大量请求一个不存在的 key,导致所有请求都直接访问数据库,从而造成数据库压力过大。

常见的解决方案包括布隆过滤器、空值缓存、缓存预热等。

缓存的雪崩现象

缓存中的大量数据同时失效或者因某种原因导致缓存不可用,从而造成大量请求直接访问数据库,导致数据库压力过大。

为了防止缓存雪崩,可以采用缓存失效时间随机化、使用分布式锁、多级缓存策略等方法。