多级缓存

1.依赖和配置

<dependency>
  <groupId>com.alicp.jetcache</groupId>
  <artifactId>jetcache-starter-redis</artifactId>
  <version>2.7.4</version>
</dependency>
jetcache:
  # 统计间隔,默认0:表示不统计
  statIntervalMinutes: 1
  metrics: true           # 必须显式开启
  # areaName是否作为缓存key前缀,默认True
  areaInCacheName: false
  local:
    default:
      # 已支持可选:linkedhashmap、caffeine
      type: linkedhashmap
      # key转换器的全局配置,当前只有:fastjson, @see com.alicp.jetcache.support.FastjsonKeyConvertor
      keyConvertor: fastjson
      # 每个缓存实例的最大元素的全局配置,仅local类型的缓存需要指定
      limit: 100
      # jetcache2.2以上,以毫秒为单位,指定多长时间没有访问,就让缓存失效,当前只有本地缓存支持。0表示不使用这个功能
      expireAfterAccessInMillis: 30000
  remote:
    default:
      # 已支持可选:redis、tair
      type: redis
      # 连接格式@see:https://github.com/lettuce-io/lettuce-core/wiki/Redis-URI-and-connection-details
      host: 127.0.0.1
      port: 6379
      password: JOTBOP1Qc4y1n1jn
      keyConvertor: fastjson2
      # 序列化器的全局配置。仅remote类型的缓存需要指定,可选java和kryo
      broadcastChannel: projectA
      valueEncoder: java
      valueDecoder: java
      poolConfig:
        minIdle: 5
        maxIdle: 20
        maxTotal: 50
      # 以毫秒为单位指定超时时间的全局配置
      expireAfterWriteInMillis: 5000

2.启动类

@SpringBootApplication
@MapperScan("com.mapper")
@EnableMethodCache(basePackages = "com")
public class Jetcache
{
    public static void main( String[] args )
    {
        SpringApplication.run(Jetcache.class,args);
    }
}

3.核心类


@Service
public class TBaseOrgunitDistService {

    @Autowired
    private TBaseOrgunitDistMapper dao;

    /**
     * area: 配置文件中关联的缓存域,比如上面的`default`,`longTime`,`shortTime`。默认`default`,继承缓存域的中的相关配置
     * cacheType: 枚举值。`BOTH`:本地和远程都缓存,`LOCAL`:本地缓存,`REMOTE`远程缓存
     * localLimit: 本地缓存条目限制,仅对本地缓存有效
     * key: 缓存的key,支持SpEL表达式
     * condition: 前置条件(方法执行前判断),结果为false则不添加缓存,支持SpEL表达式
     * postCondition: 后置条件(方法执行后判断),结果为false则不添加缓存,支持SpEL表达式(可以通过#result获取返回结果)。
     * @param id
     * @return
     */
    @Cached(name = "orgUnit:", key = "#id", cacheType = CacheType.BOTH)
    public TBaseOrgunitDist get(String id) {
        // 实际数据库查询逻辑
        return dao.selectById(id);
    }

    // 更新方法:保持一致性
    @CacheUpdate(name = "orgUnit:", key = "#entity.id", value = "#entity")
    public void update(TBaseOrgunitDist entity) {
//        dao.updateById(entity);
    }

    // 删除方法:双级失效
    @CacheInvalidate(name = "orgUnit:", key = "#id")
    @Transactional
    public void delete(String id) {
        dao.deleteById(id);
    }
}
  • 实体类
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_base_orgunit_dist")
public class TBaseOrgunitDist extends Model<TBaseOrgunitDist> implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;

    @TableField("name")
    private String name;

    @TableField("age")
    private Integer age;
}

4.测试类

@RestController
@RequestMapping("/test")
public class TestController{

    @Autowired
    private TBaseOrgunitDistService service;

    @GetMapping("/get")
    public TBaseOrgunitDist get() {
        return service.get("1");
    }

    @GetMapping("/update")
    public void update() {
        // 序列化匿名内部类 Jetcache 的 Redis 序列化配置使用 Java 原生序列化(配置中valueEncoder: java),
        // 而匿名内部类的类名会包含外部类信息(如TestController$1),导致反序列化时找不到对应类。
        TBaseOrgunitDist dist = new TBaseOrgunitDist();
        dist.setId("1");
        dist.setName("name");
        dist.setAge(18);
        service.update(dist);
    }

    @GetMapping("/delete")
    public void delete() {
         service.delete("1");
    }
}

5.说明

BOTH适用场景:只有qps很高且命中率很高的Cache实例,才需要设置为两级缓存(BOTH),在本地local cache内先查找就是为了给redis挡一下,降低一点redis的负担。如果qps很低,没必要设置BOTH(不要过早、过度优化);如果QPS很高而命中率很低,说明有很多不同key,这种情况下也不适合BOTH,因为local jvm根本就装不下那么多,localLimit多了会降低JVM的gc性能,少了local命中率上不去。在qps和命中率双高的情况下,cacheType设置为BOTH,这时有一个问题,一个JVM对缓存进行了更新操作(这个更新包括PUT和REMOVE),它自己的local JVM和redis里面的数据更新了,可是其它JVM的local cache没更新,所以就需要一个机制来让其它JVM中local cache失效。
原有的解决方案:只提供了一个localExpire的属性,让localExpire的超时时间短一点,超时后local get miss,然后去redis里面get,再刷新local。但这个办法有时候确实难以让人满意,才新增解决方案syncLocal。
所以正确的做法是:
1.一切走默认,就用cacheType=remote
2.觉得redis负担大了,根据80%/20%的原则,找到qps和命中率双高的几个地方,改成BOTH,别的地方都不用管
3.改成BOTH以后如果不能接受local cache没更新,设置localExpire短一点,让local cache尽快失效,保证最终一致
4.不能接受localExpire方案的,再开syncLocal

posted @ 2025-05-21 14:28  lwx_R  阅读(49)  评论(0)    收藏  举报