项目结构图
运行反向代理服务器也就是负责反向代理到三个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那样的重试机制,所以我们微服务中没有大量使用。