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

突出位置就是该nginx需要反向代理的其他nginx的IP和端口。
在资源比较有限的时候我们通常不适用上述的机构,而是用使用Caffeine进行二级缓存,在Cffeine没有查找到数据,我们才会去redis中查询数据。
Caffeine和redis都是内存级别的缓存,为什么要使用在这两缓存作为二级缓存,它们两有什么区别呢?
虽然它们都是内存级别的缓存,redis是需要单独部署的,其需要一个单独的进程,在tomcat访问redis时需要网络通信的开销,而Caffeine跟我们项目代码是写在一起的,它是JVM级别的缓存,用的就是Java中的堆内存,无需网络的通信的开销,在Caffeine找不到数据后才会去redis中查找。
导入依赖
- <!--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
- .initialCapacity(100) //设置缓存的初始化容量
- .maximumSize(1000) //设置最大的容量
- .build();
- }
通过设置最大的容量来控制内存,当内存达到最大时,会将最早存入的数据删除,当缓存超出这个容量的时候,会使用Window TinyLfu策略来删除缓存。
2.基于时间(设置有效期)
- @Test
- public void test3() {
- Cache
- .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
transportInfoCache() { - return Caffeine.newBuilder()
- .initialCapacity(init)
- .maximumSize(max).build();
- }
-
- }
在Controller层中设置一级缓存
- @Resource
- private TransportInfoService transportInfoService;
- @Resource
- private Cache
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
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那样的重试机制,所以我们微服务中没有大量使用。