前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、JVM 缓存

1、简单本地缓存

@Slf4j
@Service
public class BypassTaskCacheImpl implements BypassTaskCacheService {

    /**
     * key 为版本号,value为旁路配置
     */
    private Map<String, BypassTaskConfigDto> bypassTaskMap;

    /**
     * 实时旁路 realtimeBypass
     */
    private List<BypassTaskConfigDto> realtimeBypassList;

    /**
     * 历史旁路 historyBypass
     */
    private List<BypassTaskConfigDto> historyBypassList;

    /**
     * bypassTaskMapper
     */
    @Resource
    private BypassTaskMapper bypassTaskMapper;

    /**
     * umpAlarmService
     */
    @Resource
    private UmpAlarmService umpAlarmService;

    /**
     * 启动就加载
     */
    @PostConstruct
    public void init() {
        try {
            log.info("[BypassTaskSwitchCache#init]:start");
            BypassTaskQuery taskQuery = new BypassTaskQuery();
            taskQuery.setType(BypassEnum.BypassTypeEnum.REALTIME.getCode());
            taskQuery.setTaskStatus(BypassEnum.BypassTaskStatusEnum.EXECUTING.getCode());
            taskQuery.setStatus(StatusEnum.VALID.getCode());
            Map<String, BypassTaskConfigDto> newMap = new HashMap<>(8);
            List<BypassTask> dbRealtimeBypassTasks = bypassTaskMapper.selectByExample(taskQuery);
            List<BypassTaskConfigDto> newRealtimeBypassList = new ArrayList<>();
            if (!CollectionUtils.isEmpty(dbRealtimeBypassTasks)) {
                for (BypassTask bypassTask : dbRealtimeBypassTasks) {
                    BypassTaskConfigDto bypassTaskConfigDto = BeanUtils.bypassTaskToConfigDto(bypassTask);
                    newRealtimeBypassList.add(bypassTaskConfigDto);
                    newMap.put(bypassTask.getVersion(), bypassTaskConfigDto);
                }
            }

            taskQuery = new BypassTaskQuery();
            taskQuery.setType(BypassEnum.BypassTypeEnum.HISTORY.getCode());
            taskQuery.setTaskStatusList(Arrays.asList(BypassEnum.BypassTaskStatusEnum.CREATED.getCode(), BypassEnum.BypassTaskStatusEnum.EXECUTING.getCode()));
            taskQuery.setStatus(StatusEnum.VALID.getCode());
            List<BypassTask> dbHistoryBypassTasks = bypassTaskMapper.selectByExample(taskQuery);
            List<BypassTaskConfigDto> newHistoryBypassList = new ArrayList<>();
            if (!CollectionUtils.isEmpty(dbHistoryBypassTasks)) {
                for (BypassTask bypassTask : dbHistoryBypassTasks) {
                    BypassTaskConfigDto bypassTaskConfigDto = BeanUtils.bypassTaskToConfigDto(bypassTask);
                    newHistoryBypassList.add(bypassTaskConfigDto);
                    newMap.put(bypassTask.getVersion(), bypassTaskConfigDto);
                }
            }

            bypassTaskMap = newMap;
            realtimeBypassList = newRealtimeBypassList;
            historyBypassList = newHistoryBypassList;
            log.info("[BypassTaskSwitchCache#init]:end");

        } catch (Exception e) {
            log.error("[BypassTaskSwitchCache#init] error", e);
            umpAlarmService.businessAlarm("旁路[BypassTaskSwitchCache] 失败,请关注");
        }
    }

    /**
     * 任务执行( 1分钟执行 1次)
     */
    @Scheduled(cron = "0 0/1 * * * ?")
    public void refresh() {
        log.info("[BypassTaskSwitchCache#refresh]:start");
        init();
        log.info("[BypassTaskSwitchCache#refresh]:end");
    }


    /**
     * getBypassTaskMap
     *
     * @return bypassTaskMap
     */
    @Override
    public Map<String, BypassTaskConfigDto> getBypassTaskMap() {
        return bypassTaskMap;
    }

    /**
     * getRealtimeBypassList
     *
     * @return realtimeBypassList
     */
    @Override
    public List<BypassTaskConfigDto> getRealtimeBypassList() {
        return realtimeBypassList;
    }

    /**
     * getHistoryBypassList
     *
     * @return historyBypassList
     */
    @Override
    public List<BypassTaskConfigDto> getHistoryBypassList() {
        return historyBypassList;
    }

    /**
     * getBypassTaskByVersion
     *
     * @param version version
     * @return BypassTaskConfigDto
     */
    @Override
    public BypassTaskConfigDto getBypassTaskByVersion(String version) {
        Map<String, BypassTaskConfigDto> bypassTaskMap = getBypassTaskMap();
        if (CollectionUtils.isEmpty(bypassTaskMap)) {
            return null;
        }
        return bypassTaskMap.get(version);
    }

    /**
     * 险种获取开关状态
     * 1、判断开关状态
     * 2、判断险种是否存在
     *
     * @param version       旁路版本
     * @param insuranceId   险种Id
     * @param insuranceType 子险种Id
     * @return
     */
    @Override
    public boolean getSwitch(String version, String insuranceId, String insuranceType) {
        BypassTaskConfigDto bypassTaskByVersion = getBypassTaskByVersion(version);
        if (Objects.isNull(bypassTaskByVersion)) {
            return false;
        }
        List<BypassInsuranceDto> insurances = bypassTaskByVersion.getInsurances();
        return insurances.stream().anyMatch(item -> {
            if (StringUtils.isNotBlank(item.getInsuranceType())){
                return item.getInsuranceId().equals(insuranceId) && item.getInsuranceType().equals(insuranceType);
            }
            return item.getInsuranceId().equals(insuranceId);
        });
    }


    /**
     * 获取旁路数据
     * @param version  版本号
     * @param insuranceId 险种Id
     * @param insuranceType 子险种Id
     * @return
     */
    @Override
    public MerchantBypassDto getMerchantBypassDto(String version, String insuranceId, String insuranceType) {
        BypassTaskConfigDto bypassTaskByVersion = getBypassTaskByVersion(version);
        if (Objects.isNull(bypassTaskByVersion)) {
            return null;
        }
        List<BypassInsuranceDto> insurances = bypassTaskByVersion.getInsurances();
        boolean switchFlag = insurances.stream().anyMatch(item -> {
            if (StringUtils.isNotBlank(item.getInsuranceType())) {
                return item.getInsuranceId().equals(insuranceId) && item.getInsuranceType().equals(insuranceType);
            }
            return item.getInsuranceId().equals(insuranceId);
        });
        if (Boolean.FALSE.equals(switchFlag)) {
            return null;
        }
        MerchantBypassDto merchantBypassDto = new MerchantBypassDto();
        merchantBypassDto.setVersion(bypassTaskByVersion.getVersion());
        merchantBypassDto.setBypassTaskBusinessConfigDto(bypassTaskByVersion.getBypassTaskBusinessConfig());
        return merchantBypassDto;
    }

}

1、HashMap

1)LRUCache


import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * LRUCache
 * 优点:简单粗暴,不需要引入第三方包,比较适合一些比较简单的场景。
 * 缺点:没有缓存淘汰策略,定制化开发成本高。
 *
 * @author zhangyujin
 * @date 2023/1/3  13:54.
 */

public class LRUCache<K, V> extends LinkedHashMap<K, V> {

    /**
     * 可重入读写锁,保证并发读写安全性
     */
    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    /**
     * readLock
     */
    private final Lock readLock = readWriteLock.readLock();
    /**
     * writeLock
     */
    private final Lock writeLock = readWriteLock.writeLock();

    /**
     * 缓存大小限制
     */
    private final int maxSize;

    public LRUCache(int maxSize) {
        super(maxSize + 1, 1.0f, true);
        this.maxSize = maxSize;
    }

    /**
     * @param key key
     * @return Object
     */
    @Override
    public V get(Object key) {
        readLock.lock();
        try {
            return super.get(key);
        } finally {
            readLock.unlock();
        }
    }

    /**
     * put
     *
     * @param key   key
     * @param value value
     * @return Object
     */
    @Override
    public V put(K key, V value) {
        writeLock.lock();
        try {
            return super.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * removeEldestEntry
     *
     * @param eldest eldest
     * @return boolean
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return this.size() > maxSize;
    }
}

二、GuavaCache

1、GuavaCache-定时刷新

本方式适用于,缓存不经常变化,切已知要缓存的数据的情况(缓存不过期,通过定时进行刷新)

1)、AbstractLocalCacheManager

@Slf4j
@EnableScheduling
public abstract class AbstractLocalCacheManager<K, V> implements LocalCacheManage<K, V> {


    /**
     * IGNORE_kEY
     */
    private Set<K> ignoreKey;

    /**
     * AbstractLocalCacheManager
     */
    public AbstractLocalCacheManager() {
    }

    /**
     * AbstractLocalCacheManager
     *
     * @param ignoreKey ignoreKey
     */
    public AbstractLocalCacheManager(Set<K> ignoreKey) {
        this.ignoreKey = ignoreKey;
    }


    /**
     * 缓存容器
     */
    private final Cache<String, V> localCache = CacheBuilder.newBuilder()
            .maximumSize(200000)
            .recordStats()
            .build();


    /**
     * 初始化数据
     */
    @PostConstruct
    public void init() {
        refreshAll();
    }


    /**
     * 获取全部缓存信息
     *
     * @return Map<String, V>
     */
    @Override
    public Map<String, V> getAllLocalCache() {
        ConcurrentMap<String, V> concurrentMap = localCache.asMap();
        if (CollectionUtils.isEmpty(concurrentMap)) {
            refreshAll();
        }
        return localCache.asMap();
    }


    /**
     * 获取多个Keys结果
     *
     * @param keys keys
     * @return Map<K, V>
     */
    @Override
    public Map<K, V> getLocalCache(Set<K> keys) {
        if (CollectionUtils.isEmpty(keys)) {
            return Collections.emptyMap();
        }
        if (!CollectionUtils.isEmpty(ignoreKey)) {
            keys.removeAll(ignoreKey);
        }
        if (CollectionUtils.isEmpty(keys)) {
            return Collections.emptyMap();
        }

        // 转成本地key
        Map<String, K> localKeyMap = keys.stream().collect(Collectors.toMap(this::toLocalKey, k -> k));
        // 先获取本地结果
        Map<String, V> localResultMap = localCache.getAllPresent(localKeyMap.keySet());
        // 获取除本地缓存之外的其余信息
        Map<K, V> loadResultMap = Collections.emptyMap();
        if (localKeyMap.size() > localResultMap.size()) {
            Set<String> absentLocalKeys = Sets.difference(localKeyMap.keySet(), localResultMap.keySet());
            Set<K> absentKeys = absentLocalKeys.stream().map(localKeyMap::get).collect(Collectors.toSet());
            // 存在失效的key
            if (!absentKeys.isEmpty()) {
                loadResultMap = refresh(absentKeys);
            }
        }
        // 合并结果
        Map<K, V> retMap = new HashMap<>(loadResultMap);
        localResultMap.forEach((localKey, v) -> {
            K k = localKeyMap.get(localKey);
            retMap.put(k, v);
        });
        return retMap;
    }

    /**
     * 获取单个Key
     *
     * @param k k
     * @return V
     */
    public V getLocalCache(K k) {
        if (k == null) {
            return null;
        }
        if (!CollectionUtils.isEmpty(ignoreKey) && ignoreKey.contains(k)) {
            return null;
        }
        String localKey = toLocalKey(k);
        V ret = localCache.getIfPresent(localKey);
        if (ret == null) {
            return refresh(k);
        }
        return ret;
    }

    /**
     * 重新加载Key,会对历史缓存进行清理
     *
     * @param key key
     * @return @return
     */
    public V refresh(K key) {
        try {
            V v = load(key);
            localCache.invalidate(key);
            if (v != null) {
                writeCache(key, v);
            }
            return v;
        } catch (Exception ex) {
            String errorMsg = MessageFormatter.format("加载:{}.loadKey({})到内存-异常:", new Object[]{this.getClass().getSimpleName(), key}).getMessage();
            log.error(errorMsg, ex);
            throw new RuntimeException(errorMsg);
        }
    }

    /**
     * 重新加载多个Key,会对历史缓存进行清理
     *
     * @param keys keys
     * @return Map<K, V>
     */
    public Map<K, V> refresh(Set<K> keys) {
        try {
            Map<K, V> map = load(keys);
            localCache.invalidateAll(keys);
            if (!CollectionUtils.isEmpty(map)) {
                map.forEach(this::writeCache);
            }
            return map;
        } catch (Exception ex) {
            String errorMsg = MessageFormatter.format("加载:{}.loadKeys({})到内存-异常:", new Object[]{this.getClass().getSimpleName(), JsonUtils.toJsonString(keys)}).getMessage();
            log.error(errorMsg, ex);
            throw new RuntimeException(errorMsg);
        }

    }

    /**
     * 刷新所有Key,会对历史缓存进行清理
     */
    @Override
    public void refreshAll() {
        try {
            long t1 = System.currentTimeMillis();
            Map<K, V> map = loadAll();
            localCache.invalidateAll();
            if (!CollectionUtils.isEmpty(map)) {
                map.forEach(this::writeCache);
            }
            log.info("加载:{}.loadAll() success, stats:{},cost:{}ms", this.getClass().getSimpleName(), localCache.stats(), System.currentTimeMillis() - t1);
        } catch (Exception ex) {
            String errorMsg = MessageFormatter.format("加载:{}.loadAll()到内存-异常:", new Object[]{this.getClass().getSimpleName()}).getMessage();
            log.error(errorMsg, ex);
            throw new RuntimeException(errorMsg);
        }
    }


    /**
     * 子类实现,缓存数据加载,单Key
     *
     * @param keys
     * @return
     */
    protected abstract Map<K, V> load(Set<K> keys);

    /**
     * 子类实现,缓存数据加载,多Key
     *
     * @param key
     * @return
     */
    protected abstract V load(K key);

    /**
     * 子类实现,缓存数据加载,全部Key
     *
     * @return Map<K, V>
     */
    protected abstract Map<K, V> loadAll();

    /**
     * 缓存Key转换
     *
     * @param k k
     * @return String
     */
    private String toLocalKey(K k) {
        return k.toString();
    }

    /**
     * 写入缓存
     *
     * @param k k
     * @param v v
     */
    private void writeCache(K k, V v) {
        String localKey = toLocalKey(k);
        localCache.put(localKey, v);
    }

    /**
     * 删除缓存key
     *
     * @param k key
     */
    public void remove(K k) {
        localCache.invalidate(k);
    }
}

2)StringLocalCache


import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 *
 * StringLocalCache
 * @author zhangyujin
 * @date 2023/1/3  13:30.
 */
@Slf4j
@Component
public class StringLocalCache extends AbstractLocalCacheManager<String, List<String>> {

    /**
     * @param s s
     * @return load
     */
    @Override
    protected List<String> load(String s) {
        return null;
    }

    /**
     * load
     *
     * @param keys keys
     * @return Map<String, String>
     */
    @Override
    protected Map<String, List<String>> load(Set<String> keys) {
        return null;
    }


    /**
     * loadAll
     *
     * @return Map<String, String>
     */
    @Override
    protected Map<String, List<String>> loadAll() {
        return null;
    }


    /**
     * 每5分钟更新一次缓存
     */
    @Scheduled(fixedRate = 5 * 60 * 1000)
    @Override
    public void refreshAll() {
        super.refreshAll();
    }

}

三、Caffeine

Guava Cache 是由 Google 开源的基于 LRU 替换算法的缓存技术。但 Guava Cache由于被下面即将介绍的 Caffeine全面超越而被取代,其和 Caffeine 方法基本一致

⬤ 通过对 guava cachecaffeine 从性能到算法及使用的对比中,可以发现 Caffeine 基本是在 Guava 的基础上进行优化而来的,提供的功能基本一致,但是通过对算法和部分逻辑的优化,完成了对性能极大的提升,而且我们可以发现,两者切换几乎没有成本,毕竟 caffeine 就是以替换 guava cache 为目的而来的。

⬤ 那么为什么这么好的东西需要被淘汰呢,如果对于本地 Cache 有过深入研究的人应该知道 LRU 算法基本可以满足大部分的场景,但是很多人为了精益求精,基于 LRU的算法,又在此基础上提出了一系列更好的,更有效果的淘汰策略。比如有 ARCLIRSW-TinyLFU 等都提供了接近最理想的命中率,他们这些算法进一步提高了本地缓存的效率 (Caffeine 采用了一种结合 LRULFU 优点的算法:W-TinyLFU,其特点是:高命中率、低内存占用。)

注意:

⬤ 需要注意的是,在使用 Guavaget() 方法时,当缓存的 load() 方法返回 null 时,会抛出 ExecutionException。切换到Caffeine 后,get() 方法不会抛出异常,允许返回为 null

1、Caffeine — 定时刷新

1)AbstractLocalCacheManager

@Slf4j
@EnableScheduling
public abstract class AbstractLocalCacheManager<K, V> implements LocalCacheManage<K, V> {

    /**
     * IGNORE_kEY
     */
    private Set<K> ignoreKey;

    /**
     * AbstractLocalCacheManager
     */
    public AbstractLocalCacheManager() {
    }

    /**
     * AbstractLocalCacheManager
     *
     * @param ignoreKey ignoreKey
     */
    public AbstractLocalCacheManager(Set<K> ignoreKey) {
        this.ignoreKey = ignoreKey;
    }


    /**
     * 缓存容器
     */
    private final Cache<String, V> localCache = Caffeine.newBuilder()
            .maximumSize(200000)
            .recordStats()
            .build();

    /**
     * 初始化数据
     */
    @PostConstruct
    public void init() {
        refreshAll();
    }


    /**
     * 获取全部缓存信息
     *
     * @return Map<String, V>
     */
    @Override
    public Map<String, V> getAllLocalCache() {
        ConcurrentMap<String, V> concurrentMap = localCache.asMap();
        if (CollectionUtils.isEmpty(concurrentMap)) {
            refreshAll();
        }
        return localCache.asMap();
    }


    /**
     * 获取多个Keys结果
     *
     * @param keys keys
     * @return Map<K, V>
     */
    @Override
    public Map<K, V> getLocalCache(Set<K> keys) {
        if (CollectionUtils.isEmpty(keys)) {
            return Collections.emptyMap();
        }
        if (!CollectionUtils.isEmpty(ignoreKey)) {
            keys.removeAll(ignoreKey);
        }
        if (CollectionUtils.isEmpty(keys)) {
            return Collections.emptyMap();
        }

        // 转成本地key
        Map<String, K> localKeyMap = keys.stream().collect(Collectors.toMap(this::toLocalKey, k -> k));
        // 先获取本地结果
        Map<String, V> localResultMap = localCache.getAllPresent(localKeyMap.keySet());
        // 获取除本地缓存之外的其余信息
        Map<K, V> loadResultMap = Collections.emptyMap();
        if (localKeyMap.size() > localResultMap.size()) {
            Set<String> absentLocalKeys = Sets.difference(localKeyMap.keySet(), localResultMap.keySet());
            Set<K> absentKeys = absentLocalKeys.stream().map(localKeyMap::get).collect(Collectors.toSet());
            // 存在失效的key
            if (!absentKeys.isEmpty()) {
                loadResultMap = refresh(absentKeys);
            }
        }
        // 合并结果
        Map<K, V> retMap = new HashMap<>(loadResultMap);
        localResultMap.forEach((localKey, v) -> {
            K k = localKeyMap.get(localKey);
            retMap.put(k, v);
        });
        return retMap;
    }

    /**
     * 获取单个Key
     *
     * @param k k
     * @return V
     */
    public V getLocalCache(K k) {
        if (k == null) {
            return null;
        }
        if (!CollectionUtils.isEmpty(ignoreKey) && ignoreKey.contains(k)) {
            return null;
        }
        String localKey = toLocalKey(k);
        V ret = localCache.getIfPresent(localKey);
        if (ret == null) {
            return refresh(k);
        }
        return ret;
    }

    /**
     * 重新加载Key,会对历史缓存进行清理
     *
     * @param key key
     * @return @return
     */
    public V refresh(K key) {
        try {
            V v = load(key);
            localCache.invalidate(key);
            if (v != null) {
                writeCache(key, v);
            }
            return v;
        } catch (Exception ex) {
            String errorMsg = MessageFormatter.format("加载:{}.loadKey({})到内存-异常:", new Object[]{this.getClass().getSimpleName(), key}).getMessage();
            log.error(errorMsg, ex);
            throw new RuntimeException(errorMsg);
        }
    }

    /**
     * 重新加载多个Key,会对历史缓存进行清理
     *
     * @param keys keys
     * @return Map<K, V>
     */
    public Map<K, V> refresh(Set<K> keys) {
        try {
            Map<K, V> map = load(keys);
            localCache.invalidateAll(keys);
            if (!CollectionUtils.isEmpty(map)) {
                map.forEach(this::writeCache);
            }
            return map;
        } catch (Exception ex) {
            String errorMsg = MessageFormatter.format("加载:{}.loadKeys({})到内存-异常:", new Object[]{this.getClass().getSimpleName(), JsonUtils.toJsonString(keys)}).getMessage();
            log.error(errorMsg, ex);
            throw new RuntimeException(errorMsg);
        }

    }

    /**
     * 刷新所有Key,会对历史缓存进行清理
     */
    @Override
    public void refreshAll() {
        try {
            long t1 = System.currentTimeMillis();
            Map<K, V> map = loadAll();
            localCache.invalidateAll();
            if (!CollectionUtils.isEmpty(map)) {
                map.forEach(this::writeCache);
            }
            log.info("加载:{}.loadAll() success, stats:{},cost:{}ms", this.getClass().getSimpleName(), localCache.stats(), System.currentTimeMillis() - t1);
        } catch (Exception ex) {
            String errorMsg = MessageFormatter.format("加载:{}.loadAll()到内存-异常:", new Object[]{this.getClass().getSimpleName()}).getMessage();
            log.error(errorMsg, ex);
            throw new RuntimeException(errorMsg);
        }
    }


    /**
     * 子类实现,缓存数据加载,单Key
     *
     * @param keys
     * @return
     */
    protected abstract Map<K, V> load(Set<K> keys);

    /**
     * 子类实现,缓存数据加载,多Key
     *
     * @param key
     * @return
     */
    protected abstract V load(K key);

    /**
     * 子类实现,缓存数据加载,全部Key
     *
     * @return Map<K, V>
     */
    protected abstract Map<K, V> loadAll();

    /**
     * 缓存Key转换
     *
     * @param k k
     * @return String
     */
    private String toLocalKey(K k) {
        return k.toString();
    }

    /**
     * 写入缓存
     *
     * @param k k
     * @param v v
     */
    private void writeCache(K k, V v) {
        String localKey = toLocalKey(k);
        localCache.put(localKey, v);
    }

    /**
     * 删除缓存key
     *
     * @param k key
     */
    public void remove(K k) {
        localCache.invalidate(k);
    }

}

2)StringLocalCache


import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 *
 * StringLocalCache
 * @author zhangyujin
 * @date 2023/1/3  13:30.
 */
@Slf4j
@Component
public class StringLocalCache extends AbstractLocalCacheManager<String, List<String>> {

    /**
     * @param s s
     * @return load
     */
    @Override
    protected List<String> load(String s) {
        return null;
    }

    /**
     * load
     *
     * @param keys keys
     * @return Map<String, String>
     */
    @Override
    protected Map<String, List<String>> load(Set<String> keys) {
        return null;
    }


    /**
     * loadAll
     *
     * @return Map<String, String>
     */
    @Override
    protected Map<String, List<String>> loadAll() {
        return null;
    }


    /**
     * 每5分钟更新一次缓存
     */
    @Scheduled(fixedRate = 1 * 60 * 1000)
    @Override
    public void refreshAll() {
        super.refreshAll();
    }

}

三、多级缓存

⬤ 缓存,就是让数据更接近使用者,让访问速度加快,从而提升系统性能。工作机制大概是先从缓存中加载数据,如果没有,再从慢速设备(eg:数据库)中加载数据并同步到缓存中。

⬤ 多级缓存,是指在整个系统架构的不同系统层面进行数据缓存,以提升访问速度。主要分为三层缓存:网关 nginx缓存、分布式缓存、本地缓存。这里的多级缓存就是用 redis 分布式缓存 + caffeine 本地缓存整合而来。

平时我们在开发过程中,一般都是使用 redis 实现分布式缓存、caffeine 操作本地缓存,但是发现只使用 redis 或者是 caffeine实现缓存都有一些问题:

⬤ 一级缓存:Caffeine 是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。优点数据就在应用内存所以速度快。缺点受应用内存的限制,所以容量有限;没有持久化,重启服务后缓存数据会丢失;在分布式环境下缓存数据数据无法同步;

⬤ 二级缓存:redis 是一高性能、高可用的 key - value 数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。优点支持多种数据类型,扩容方便;有持久化,重启应用服务器缓存数据不会丢失;他是一个集中式缓存,不存在在应用服务器之间同步数据的问题。缺点每次都需要访问 redis 存在 IO 浪费的情况。

综上所述,我们可以通过整合 rediscaffeine 实现多级缓存,解决上面单一缓存的痛点,从而做到相互补足。

1、rediscaffeine 多级缓存

1)技术方案

image-20231109174523409

四、缓存对象占用观察

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>8.11.2</version>
</dependency>

RamUsageEstimator就是根据java对象在堆内存中的存储格式,通过计算Java对象头、实例数据、引用等的大小,相加而得,如果有引用,还能递归计算引用对象的大小。RamUsageEstimator的源码并不多,几百行,清晰可读。这里不进行一一解读了。它在初始化的时候会根据当前JVM运行环境、CPU架构、运行参数、是否开启指针压缩、JDK版本等综合计算对象头的大小,而实例数据部分则按照java基础数据类型的标准大小进行计


使用该第三方工具比较简单直接,主要依靠JVM本身环境、参数及CPU架构计算头信息,再依据数据类型的标准计算实例字段大小,计算速度很快,另外使用较方便。如果非要说这种方式有什么缺点的话,那就是这种方式计算所得的对象头大小是基于JVM声明规范的,并不是通过运行时内存地址计算而得,存在与实际大小不符的这种可能性。

public static void main(String[] args) {
    Map<String, String> map = new HashMap<>();
    System.out.println("map init value is " +  RamUsageEstimator.sizeOfObject(map));
    for (int i = 1000000; i < 6000000; i++) {
        map.put(String.valueOf(i), String.valueOf(i+1000000));
    }

    System.out.println("map size: 1  is " + RamUsageEstimator.sizeOfObject(map.get("1000000"))+ " byte");
    System.out.println("map size: " +map.size() +"  is " + RamUsageEstimator.sizeOfMap(map)+ " byte");
    System.out.println("map size: " +map.size() +"  is " + RamUsageEstimator.sizeOfObject(map)+ " byte");

    // JDK自带
    System.out.println("map size: " +map.size() +" is " + ObjectSizeCalculator.getObjectSize(map) + " byte");
}



map init value is 48
map size: 1  is 56 byte
map size: 5000000  is 720000048 byte
map size: 5000000  is 720000048 byte
map size: 5000000 is 753554512 byte

ContactAuthor