缓存常见问题

158 阅读4分钟

缓存作用

主要是解决读多写上的场景,降低系统的CPU和IO的使用,提高系统的性能。

在一些需要高并发的场景,也可以使用类似Redis解决,如:库存的管理。

经典问题

1、缓存穿透

2、缓存击穿

3、雪崩

针对上面的缓存问题代码实现如下:

package com.newretail.account.service;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

@Service
public class CacheService {

    @Autowired
    private RedisTemplate redisTemplate;

    private Cache<String, Object> dataCache = CacheBuilder.newBuilder()      // 使用 CacheBuilder 创建
            .expireAfterAccess(Duration.ofSeconds(30))  // 设置过期时间,在每次访问的半小时后过期,(再次访问则重新等待半小时)
            // .expireAfterWrite(Duration.ofHour(2))     在每次写入缓存2小时后过期
            .maximumSize(10240)     // 缓存最大数量
            .concurrencyLevel(4)    // 指定并发修改的数量,默认4
            .initialCapacity(2048)      // 设置初始化大小,避免经常扩容
            .build();       // 无参的 build 方法创建 Cache


    private Map<String, Object> locks = new ConcurrentHashMap<>();

    private AtomicBoolean casLock = new AtomicBoolean(false);

    public <T> List<T> getCache(String key, DBData<T> dbUtils, int expire) {
        List<T> data = getCacheData(key);
        if (data == null) {
            // 先获取指定key的锁,在获取前,先得加锁,保证每个key只能获取一个Key
            Object lock = null;
            //此处用cas保证只有一个线程进来,主要原因是获取key锁都是内存操作,时间短
            if (casLock.compareAndSet(false, true)) {
                lock = locks.get(key);
                if (lock == null) {
                    lock = new Object();
                    locks.put(key, lock);
                }
                casLock.set(false);
            }
            synchronized (lock) {
                //DCL检查,保证获取锁的线程直接读取Redis的数据,不要执行DB操作
                data = getCacheData(key);
                if (data == null) {
                    data = dbUtils.getDBData();
                    setCacheData(key, expire, data);
                }
            }
            // 清除内存,防止内存溢出,释放的时候,可能会有使一个key有多个锁,但是缓存中已经有了数据,不会有大面积请求请求到数据库
            locks.remove(key);
        }
        return data;
    }


    private <T> List<T> getCacheData(String key) {
        List<T> localCache = (List<T>) dataCache.getIfPresent(key);
        if (localCache != null) {
            return localCache;
        }
        List<T> redisCache = (List<T>) redisTemplate.opsForValue().get(key);
        if (redisCache != null) {
            dataCache.put(key, redisCache);
            return redisCache;
        }
        return null;
    }

    private void setCacheData(String key, int expire, List cacheData) {
        redisTemplate.opsForValue().set(key, cacheData, expire, TimeUnit.SECONDS);
        dataCache.put(key, cacheData);
    }
}


interface DBData<T> {
    List<T> getDBData();
}

4、数据一致性

5、拒绝bigkey(防止网卡流量、慢查询)

如果出现下面两种情况,我们会认为它是bigkey。

  • 字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey
  • 非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多

一般来说,string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。

反例:一个包含200万个元素的list。

非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞)

bigkey的危害:

  1. 导致redis阻塞
  2. 网络拥塞

bigkey也就意味着每次获取要产生的网络流量较大,假设一个bigkey为1MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例也造成影响,其后果不堪设想。

  1. 过期删除

有个bigkey,它安分守己(只执行简单的命令,例如hget、lpop、zscore等),但它设置了过期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性。

bigkey的产生:
一般来说,bigkey的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个例子:

  • 社交类:粉丝列表,如果某些明星或者大v不精心设计下,必是bigkey。
  • 统计类:例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是bigkey。
  • 缓存类:将数据从数据库load出来序列化放到Redis里,这个方式非常常用,但有两个地方需要注意,第一,是不是有必要把所有字段都缓存;第二,有没有相关关联的数据,有的同学为了图方便把相关数据都存一个key下,产生bigkey。

如何优化bigkey

big list: list1、list2、...listN
big hash:可以讲数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成200个key,每个key下面存放5000个用户数据

  1. 如果bigkey不可避免,也尽量不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理。

参考:

juejin.cn/post/720818…