Skip to content

缓存入门

导入依赖

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

开启缓存

java
@EnableCaching
@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

常用注解

常用注解:@CacheConfig@Cacheable@CacheEvict@CachePut@Caching

  • @CacheConfig: 常用于类级别的注解,用于统一配置类缓存的公共属性。
  • @Caching: 可以在一个类或方法同时使用多个缓存相关注解,灵活配置缓存策略。
  • @Cacheable: 用于标记一个类或方法,当被标记对象被访问,会先去缓存中查询相应的结果,如果存在则返回,不存在则指向方法。
  • @CacheEvict: 用于从缓存中移除数据。当使用@CacheEvict注解标记一个方法时,该方法执行后会触发缓存的清除操作。
  • @CachePut: 用于将方法的返回值存储到缓存中。与@Cacheable注解不同的是,@CachePut注解每次都会触发方法的执行,并将结果存储到缓存中。

缓存示例

java
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import top.haijunit.work.core.doamin.PageQuery;
import top.haijunit.work.core.doamin.ResultPage;

import java.util.Collection;
import java.util.List;

/**
 * `@CacheConfig` 标记类 全局配,置避免重复配置`cacheNames`、`keyGenerator`等属性
 * `Cacheable`  标记方法,当被访问时,会先去缓存中查询相应的结果,如果存在则返回,不存在则指向方法
 * `CacheEvict` 标记方法,删除对应的缓存中的数据
 * `CachePut`   标记方法,方法执行后会触发缓存的更新操作
 */
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@CacheConfig(cacheNames = {"cache:test"})
public class CacheTestService {

    private final List<String> dataList = CollUtil.newArrayList("a", "b", "c");

    /**
     * 查询分页列表。
     * 当查询条件为空或参数长度为0时,缓存查询结果。
     */
    @Cacheable(key = "#pageQuery.getPage() + '_' + #pageQuery.getSize()", condition = "#info == null || #info.isEmpty()")
    public ResultPage<String> selectPageList(PageQuery pageQuery, String info) {
        log.info("分页查询,条件:{}, 分页: {}", info, pageQuery);
        Collection<String> list = selectList(info).stream().skip(pageQuery.offset()).limit(pageQuery.getSize()).toList();
        return ResultPage.of(list, pageQuery.getPage(), pageQuery.getSize(), list.size());
    }

    /**
     * 查询所有或根据参数过滤的数据列表。
     * 使用@Cacheable注解来缓存查询结果。
     */
    @Cacheable(key = "'list'", condition = "#dto ==null || #dto.length() == 0")
    public Collection<String> selectList(String dto) {
        log.info("查询列表,条件:{}", dto);
        if (CharSequenceUtil.isBlank(dto)) {
            return dataList;
        }
        return dataList.stream().filter(item -> CharSequenceUtil.containsAnyIgnoreCase(item, dto)).toList();
    }

    /**
     * 根据code获取详细信息。
     * 使用@Cacheable注解来缓存单个对象的详细信息。
     */
    @Cacheable(key = "#code", unless = "#result == null")
    public String selectDetail(String code) {
        log.info("查询详情,code:{}", code);
        if (CharSequenceUtil.isBlank(code)) {
            return null;
        }
        return dataList.stream().filter(item -> CharSequenceUtil.containsAnyIgnoreCase(item, code)).findFirst().orElse(null);
    }

    /**
     * 插入新数据。
     * 使用@CachePut注解来更新缓存中的值。
     */
    @CacheEvict(allEntries = true)
    public String insert(String code) {
        log.info("插入数据,code:{}", code);
        if (CharSequenceUtil.isBlank(code)) {
            return null;
        }
        if (dataList.contains(code)) {
            log.warn("插入失败,数据已存在: {}", code);
            return null;
        }
        dataList.add(code);
        return code;
    }

    /**
     * 删除数据。
     * 使用@CacheEvict注解来从缓存中移除项目。
     */
    @CacheEvict(allEntries = true)
    public String delete(String code) {
        log.info("删除数据,code:{}", code);
        if (CharSequenceUtil.isBlank(code)) {
            return null;
        }
        if (!dataList.contains(code)) {
            log.warn("删除失败,数据不存在: {}", code);
            return null;
        }
        dataList.remove(code);
        return code;
    }
}

缓存进阶

目标

  • 1、实现两种CacheManager,本地(Caffeine)+ 远程(Redisson)。
  • 2、可以通过配置spring.cache.type来切换本地缓存实现,可以关闭全部缓存实现。
  • 3、一级缓存(本地缓存)+ 二级缓存(Redis),通过多级缓存,提高缓存的利用率。(非必须,没有想到对应的业务场景)
  • 3、本地缓存和远程缓存的同步,实现数据一致性。(非必须,没有想到对应的业务场景)

方案实现

  1. 配置spring.cache.type,默认使用本地缓存。

本地缓存Caffeine

POM依赖

xml
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

应用实践

java
@Slf4j
class CaffeineCacheTest {
    Cache<Object, Object> cache;
    @TestBefore
    void init() {
        cache = Caffeine.newBuilder()
          //初始数量
          .initialCapacity(10)
          //最大条数
          .maximumSize(10)
          //expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准
          //最后一次写操作后经过指定时间过期
          .expireAfterWrite(1, TimeUnit.SECONDS)
          //最后一次读或写操作后经过指定时间过期
          .expireAfterAccess(1, TimeUnit.SECONDS)
          //监听缓存被移除
          .removalListener((key, val, removalCause) -> { })
          //记录命中
          .recordStats()
          .build();
    }
    @Test
    void test() {
        cache.put("1", "张三");
        // 取值
        log.info("{}", cache.getIfPresent("1"));
        // 默认值
        log.info("{}", cache.get("2", o -> "默认值"));
    }
}

SpringBoot中运用

java
@Component
public class AgentCache {

    /**
     * 直播间代理员基本信息(批量)
     */
    private LoadingCache<Long, List<Agent>> agentBaseInfoCache = null;

    /**
     * 直播间代理员信息(单个)
     */
    private LoadingCache<Long, AgentModel> agentModelCache = null;

    @Resource
    private IZbSiteAttributeService siteAttributeService;

    @Autowired
    private IAgentService agentService;

    @PostConstruct
    private void init() {
        agentBaseInfoCache = Caffeine.newBuilder()
                //设置本地缓存容器的初始容量
                .initialCapacity(10)
                //设置本地缓存的最大容量
                .maximumSize(1000)
                //设置缓存后多少秒过期
                .expireAfterWrite(3600, TimeUnit.SECONDS).build(new CacheLoader<Long, List<Agent>>() {
                    @Override
                    public @Nullable List<Agent> load(Long zbId) {
                        return agentService.lambdaQuery()
                                .select(Agent::getUserId, Agent::getId, Agent::getParentCode, Agent::getLevel, Agent::getWeComAccount, Agent::getState, Agent::getStoreId)
                                .eq(Agent::getZbId, zbId)
                                .in(Agent::getState, HealthAgentConstants.AgentState.NORMA, HealthAgentConstants.AgentState.DISABLE)
                                .list();
                    }
                });

        agentModelCache = Caffeine.newBuilder()
                //设置本地缓存容器的初始容量
                .initialCapacity(10)
                //设置本地缓存的最大容量
                .maximumSize(1000)
                //设置缓存后多少秒过期
                .expireAfterWrite(3600, TimeUnit.SECONDS).build(new CacheLoader<Long, AgentModel>() {
                    @Override
                    public AgentModel load(Long zbId) {
                        String attributeVal = siteAttributeService.getAttributeVal(zbId, SiteAttributeEnum.AGENT_SETTING_MODEL.getStatus());
                        return StringUtils.isNotBlank(attributeVal) ? JsonUtil.fromJson(attributeVal, AgentModel.class) : new AgentModel();
                    }
                });
    }

    /**
     * 获取正常代理的用户ID
     */
    public List<Long> get(Long zbId) {
      return agentBaseInfoCache.get(zbId)
                .stream()
                .filter(a -> HealthAgentConstants.AgentState.NORMA == a.getState())
                .map(Agent::getUserId)
                .collect(Collectors.toList());
    }


    /**
     * 获取所有代理员用户ID 包含了正常 & 禁用
     */
    public List<Long> getAllAgentUserId(Long zbId) {
        return agentBaseInfoCache.get(zbId)
                .stream()
                .map(Agent::getUserId)
                .collect(Collectors.toList());
    }

    /**
     * 获取正常代理的用户代理集合
     */
    public List<Agent> getAgentBaseInfoList(Long zbId) {
        return agentBaseInfoCache.get(zbId)
                .stream()
                .filter(a -> HealthAgentConstants.AgentState.NORMA == a.getState())
                .collect(Collectors.toList());
    }

    /**
     * 获取代理的用户代理集合-所有
     */
    public List<Agent> getAllAgentBaseInfoList(Long zbId) {
        return agentBaseInfoCache.get(zbId).stream().collect(Collectors.toList());
    }

    public Agent getAgentBaseInfo(Long zbId, Long userId) {
        return agentBaseInfoCache.get(zbId).stream().filter(w -> w.getUserId().equals(userId)).findFirst().orElse(null);
    }

    public void invalidate(Long zbId) {
        agentBaseInfoCache.invalidate(zbId);
//        agentBaseInfoCache.refresh(zbId);
    }

    /**
     * 获取AgentModel缓存
     */
    public AgentModel getAgentModel(Long zbId) {
        AgentModel model;
        try {
            model = agentModelCache.get(zbId);
        } catch (Exception e) {
            String attributeVal = siteAttributeService.getAttributeVal(zbId, SiteAttributeEnum.AGENT_SETTING_MODEL.getStatus());
            return StringUtils.isNotBlank(attributeVal) ? JsonUtil.fromJson(attributeVal, AgentModel.class) : new AgentModel();
        }
        if (model == null) {
            model = new AgentModel();
        }
        return model;
    }

    /**清除AgentModel本地缓存*/
    public void refreshAgentModel(Long key) {
        try {
            agentModelCache.refresh(key);
        } catch (Exception ignored) {
        }
    }
}