Caffeine的使用

项目结构图

 运行反向代理服务器也就是负责反向代理到三个nginx的nginx,该nignx也负责前端页面的跳转。

该nginx的conf为下:

突出位置就是该nginx需要反向代理的其他nginx的IP和端口。

在资源比较有限的时候我们通常不适用上述的机构,而是用使用Caffeine进行二级缓存,在Cffeine没有查找到数据,我们才会去redis中查询数据。

Caffeine是什么?

Caffeine和redis都是内存级别的缓存,为什么要使用在这两缓存作为二级缓存,它们两有什么区别呢?

虽然它们都是内存级别的缓存,redis是需要单独部署的,其需要一个单独的进程,在tomcat访问redis时需要网络通信的开销,而Caffeine跟我们项目代码是写在一起的,它是JVM级别的缓存,用的就是Java中的堆内存,无需网络的通信的开销,在Caffeine找不到数据后才会去redis中查找。

Caffeine的使用

导入依赖

<!--jvm进程缓存-->
<dependency>
		<groupId>com.github.ben-manes.caffeine</groupId>
		<artifactId>caffeine</artifactId>
</dependency>

进行测试

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class test {

    @Test
    public void test1() {
        Cache<Object, Object> cache = Caffeine.newBuilder()
                .initialCapacity(100) //设置缓存的初始化容量
                .maximumSize(1000) //设置最大的容量
                .build();
        //向缓存中插入数据
        cache.put("key1", 123);
        //从缓存中取出数据
        Object value1 = cache.get("key1", key -> 456);
        System.out.println(value1);
        //获取没有的数据
        Object value2 = cache.get("key2", key -> 789);
        System.out.println(value2);
    }
}

驱逐策略(面试点: 使用Caffeine为了防止内存溢出,怎么做?)

为了防止一直往内存里装数值导致占用内存,所以Caffeine给我们提供了驱逐策略。

1.基于容量(设置缓存的上限)

 @Test
    public void test2() {
        Cache<Object, Object> cache = Caffeine.newBuilder()
                .initialCapacity(100) //设置缓存的初始化容量
                .maximumSize(1000) //设置最大的容量
                .build();
    }

通过设置最大的容量来控制内存,当内存达到最大时,会将最早存入的数据删除,当缓存超出这个容量的时候,会使用Window TinyLfu策略来删除缓存。

2.基于时间(设置有效期)

 @Test
    public void test3() {
        Cache<Object, Object> cache = Caffeine.newBuilder()
                .initialCapacity(100)
                .expireAfterWrite(Duration.ofSeconds(10)) //设置缓存的有效期,此时就是设置为10s
                .build();
    }

3.基于引用:设置数据的强引用和弱引用,在内存不足的时候jvm会进行垃圾回收,会将弱引用的数据进行回收,性能差,不建议使用。

设置一级缓存 

Caffeine配置(配置到ioc中,后续提供依赖注入进行使用)

import com.github.benmanes.caffeine.cache.Caffeine;
import com.sl.transport.info.domain.TransportInfoDTO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Caffeine缓存配置
 */
@Configuration
public class CaffeineConfig {

    //初始化的容量大小
    @Value("${caffeine.init}")
    private Integer init;
    //最大的容量大小
    @Value("${caffeine.max}")
    private Integer max;

    @Bean
    public Cache<String, TransportInfoDTO> transportInfoCache() {
        return Caffeine.newBuilder()
                .initialCapacity(init)
                .maximumSize(max).build();
    }

}

在Controller层中设置一级缓存

@Resource
    private TransportInfoService transportInfoService;
    @Resource
    private Cache<String, TransportInfoDTO> transportInfoCache;

    /**
     * 根据运单id查询运单信息
     *
     * @param transportOrderId 运单号
     * @return 运单信息
     */
    @ApiImplicitParams({
            @ApiImplicitParam(name = "transportOrderId", value = "运单id")
    })
    @ApiOperation(value = "查询", notes = "根据运单id查询物流信息")
    @GetMapping("{transportOrderId}")
    public TransportInfoDTO queryByTransportOrderId(@PathVariable("transportOrderId") String transportOrderId) {
        //提供Caffeine先获取一级缓存,如果没有缓存就去Mongodb中查数据
        TransportInfoDTO transportInfoDTO = transportInfoCache.get(transportOrderId, id -> {
            TransportInfoEntity transportInfoEntity = transportInfoService.queryByTransportOrderId(transportOrderId);
            return BeanUtil.toBean(transportInfoEntity, TransportInfoDTO.class);
        });

        if(ObjectUtil.isNotEmpty(transportInfoDTO)) {
            return transportInfoDTO;
        }
        throw new SLException(ExceptionEnum.NOT_FOUND);
    }

设置二级缓存(使用springCache进行二级缓存)

配置springCache的配置(redis的配置)

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * Redis相关的配置
 */
@Configuration
public class RedisConfig {

    /**
     * 存储的默认有效期时间,单位:小时
     */
    @Value("${redis.ttl:1}")
    private Integer redisTtl;

    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
        // 默认配置
        RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                // 设置key的序列化方式为字符串
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置value的序列化方式为json格式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues() // 不缓存null
                .entryTtl(Duration.ofHours(redisTtl));  // 默认缓存数据保存1小时

        // 构redis缓存管理器
        RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisTemplate.getConnectionFactory())
                .cacheDefaults(defaultCacheConfiguration)
                .transactionAware() // 只在事务成功提交后才会进行缓存的put/evict操作
                .build();
        return redisCacheManager;
    }
}

 在查询查找的Service上添加对应的注解

@Override
    //该注解的在作用就是查询到的数据缓存到redis中其key值就为: transport-info::transportOrderId
    //注解其中key的值表示key拼接的参数,这里就是第一个参数
    @Cacheable(value = "transport-info", key = "#p0")
    public TransportInfoEntity queryByTransportOrderId(String transportOrderId) {
        //通过orderId创建查询条件,查询物流信息
        return mongoTemplate.findOne(
                Query.query(Criteria.where("transportOrderId").is(transportOrderId)),
                TransportInfoEntity.class
        );
    }

添加此注解后,会先在redis的缓存中查找数据,如果有数据就直接返回数据,如果没有才会提供Mongodb查询。

当然为了保证在数据修改后还能保证缓存的准确性,这里我们需要在修改操作上添加springCache的注解@CachePut。(该注解的作用就是更新缓存的数据,所以可以在缓存的增删改时添加该注解

@Override
    @CachePut(value = "transport-info", key = "#p0")
    public TransportInfoEntity saveOrUpdate(String transportOrderId, TransportInfoDetail infoDetail) {
        //通过orderId创建查询条件,查询物流信息是否存在
        TransportInfoEntity updateTransportInfoEntity = mongoTemplate.findOne(
                Query.query(Criteria.where("transportOrderId").is(transportOrderId)),
                TransportInfoEntity.class
        );
        if(ObjectUtil.isNotEmpty(updateTransportInfoEntity)) {
            //如果存在就获取对应的信息,在infoList中添加对应的物流信息
            updateTransportInfoEntity.getInfoList().add(infoDetail);
        } else {
            //如果不存在就新建一个document
            updateTransportInfoEntity = new TransportInfoEntity();
            updateTransportInfoEntity.setTransportOrderId(transportOrderId);
            updateTransportInfoEntity.setInfoList(ListUtil.toList(infoDetail));
            updateTransportInfoEntity.setCreated(System.currentTimeMillis());
        }
        //修改物流信息的修改时间
        updateTransportInfoEntity.setUpdated(System.currentTimeMillis());
        //进行新增或修改操作 id为空时就进行新增,不为空时进行修改操作
        return mongoTemplate.save(updateTransportInfoEntity);
    }

一级缓存更新的问题

修改后,在一级缓存中的数据是不变的,所以为了保证数据的准确性,我们先是想到在进行增删改的时候用this.transportInfoCache.invalidate(transportOrderId);来清除缓存但是在微服务的情况小会出现数据不一致的情况。(因为一级缓存在微服务间不是共享的)

 @Override
    //value和key就是对缓存中key的拼接,这里的key就是transport-info::对应的第一个参数
    @CachePut(value = "transport-info", key = "#p0")
    public TransportInfoEntity saveOrUpdate(String transportOrderId, TransportInfoDetail infoDetail) {
        //通过orderId创建查询条件,查询物流信息是否存在
        TransportInfoEntity updateTransportInfoEntity = mongoTemplate.findOne(
                Query.query(Criteria.where("transportOrderId").is(transportOrderId)),
                TransportInfoEntity.class
        );
        if(ObjectUtil.isNotEmpty(updateTransportInfoEntity)) {
            //如果存在就获取对应的信息,在infoList中添加对应的物流信息
            updateTransportInfoEntity.getInfoList().add(infoDetail);
        } else {
            //如果不存在就新建一个document
            updateTransportInfoEntity = new TransportInfoEntity();
            updateTransportInfoEntity.setTransportOrderId(transportOrderId);
            updateTransportInfoEntity.setInfoList(ListUtil.toList(infoDetail));
            updateTransportInfoEntity.setCreated(System.currentTimeMillis());
        }
        //修改物流信息的修改时间
        updateTransportInfoEntity.setUpdated(System.currentTimeMillis());
        //清除缓存中的数据
        this.transportInfoCache.invalidate(transportOrderId);
        //进行新增或修改操作 id为空时就进行新增,不为空时进行修改操作
        return mongoTemplate.save(updateTransportInfoEntity);
    }

为了解决此问题,我们引入了redis中的发布与订阅的功能来解决此问题。

类似mq的机制,在发送对应的key也就是消息,然后订阅该消息的模块就会执行自定义的操作。

在配置中增加订阅的配置

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * Redis相关的配置
 */
@Configuration
public class RedisConfig {

    /**
     * 存储的默认有效期时间,单位:小时
     */
    @Value("${redis.ttl:1}")
    private Integer redisTtl;

    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
        // 默认配置
        RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                // 设置key的序列化方式为字符串
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置value的序列化方式为json格式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues() // 不缓存null
                .entryTtl(Duration.ofHours(redisTtl));  // 默认缓存数据保存1小时

        // 构redis缓存管理器
        RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisTemplate.getConnectionFactory())
                .cacheDefaults(defaultCacheConfiguration)
                .transactionAware() // 只在事务成功提交后才会进行缓存的put/evict操作
                .build();
        return redisCacheManager;
    }

    public static final String CHANNEL_TOPIC = "sl-express-ms-transport-info-caffeine";

    /**
     * 配置订阅,用于解决Caffeine一致性的问题
     *
     * @param connectionFactory 链接工厂
     * @param listenerAdapter 消息监听器
     * @return 消息监听容器
     */
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                                   MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, new ChannelTopic(CHANNEL_TOPIC));
        return container;
    }
}

 编写RedisMessageListener用于监听消息(监听消息后执行的自定义方法),删除caffeine中的数据。(可以理解成监听方法)

import cn.hutool.core.convert.Convert;
import com.github.benmanes.caffeine.cache.Cache;
import com.sl.transport.info.domain.TransportInfoDTO;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;


/**
 * redis消息监听,解决Caffeine一致性的问题
 */
@Component
public class RedisMessageListener extends MessageListenerAdapter {

    @Resource
    private Cache<String, TransportInfoDTO> transportInfoCache;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        //获取到消息中的运单id
        String transportOrderId = Convert.toStr(message);
        //将本jvm中的缓存删除掉
        this.transportInfoCache.invalidate(transportOrderId);
    }
}

在增删改的方法中向对应的频道发送消息。

@Override
    //value和key就是对缓存中key的拼接,这里的key就是transport-info::对应的第一个参数
    @CachePut(value = "transport-info", key = "#p0")
    public TransportInfoEntity saveOrUpdate(String transportOrderId, TransportInfoDetail infoDetail) {
        //通过orderId创建查询条件,查询物流信息是否存在
        TransportInfoEntity updateTransportInfoEntity = mongoTemplate.findOne(
                Query.query(Criteria.where("transportOrderId").is(transportOrderId)),
                TransportInfoEntity.class
        );
        if(ObjectUtil.isNotEmpty(updateTransportInfoEntity)) {
            //如果存在就获取对应的信息,在infoList中添加对应的物流信息
            updateTransportInfoEntity.getInfoList().add(infoDetail);
        } else {
            //如果不存在就新建一个document
            updateTransportInfoEntity = new TransportInfoEntity();
            updateTransportInfoEntity.setTransportOrderId(transportOrderId);
            updateTransportInfoEntity.setInfoList(ListUtil.toList(infoDetail));
            updateTransportInfoEntity.setCreated(System.currentTimeMillis());
        }
        //修改物流信息的修改时间
        updateTransportInfoEntity.setUpdated(System.currentTimeMillis());
        //清除缓存中的数据
        this.stringRedisTemplate.convertAndSend(RedisConfig.CHANNEL_TOPIC, transportOrderId);
        //进行新增或修改操作 id为空时就进行新增,不为空时进行修改操作
        return mongoTemplate.save(updateTransportInfoEntity);
    }

最终保证了一级缓存的准确性。

问: 那redis的这种机制也可以完成mq的一系列操作,为什么微服务中没有大量使用呢?

答:redis的发布订阅没有可靠性的处理,没有像mq那样的重试机制,所以我们微服务中没有大量使用。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值