柚子快報邀請碼778899分享:緩存 Caffeine的使用
柚子快報邀請碼778899分享:緩存 Caffeine的使用
項目結構圖
?運行反向代理服務器也就是負責反向代理到三個nginx的nginx,該nignx也負責前端頁面的跳轉。
該nginx的conf為下:
突出位置就是該nginx需要反向代理的其他nginx的IP和端口。
在資源比較有限的時候我們通常不適用上述的機構,而是用使用Caffeine進行二級緩存,在Cffeine沒有查找到數(shù)據(jù),我們才會去redis中查詢數(shù)據(jù)。
Caffeine是什么?
Caffeine和redis都是內存級別的緩存,為什么要使用在這兩緩存作為二級緩存,它們兩有什么區(qū)別呢?
雖然它們都是內存級別的緩存,redis是需要單獨部署的,其需要一個單獨的進程,在tomcat訪問redis時需要網絡通信的開銷,而Caffeine跟我們項目代碼是寫在一起的,它是JVM級別的緩存,用的就是Java中的堆內存,無需網絡的通信的開銷,在Caffeine找不到數(shù)據(jù)后才會去redis中查找。
Caffeine的使用
導入依賴
進行測試
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
.initialCapacity(100) //設置緩存的初始化容量
.maximumSize(1000) //設置最大的容量
.build();
//向緩存中插入數(shù)據(jù)
cache.put("key1", 123);
//從緩存中取出數(shù)據(jù)
Object value1 = cache.get("key1", key -> 456);
System.out.println(value1);
//獲取沒有的數(shù)據(jù)
Object value2 = cache.get("key2", key -> 789);
System.out.println(value2);
}
}
驅逐策略(面試點: 使用Caffeine為了防止內存溢出,怎么做?)
為了防止一直往內存里裝數(shù)值導致占用內存,所以Caffeine給我們提供了驅逐策略。
1.基于容量(設置緩存的上限)
@Test
public void test2() {
Cache
.initialCapacity(100) //設置緩存的初始化容量
.maximumSize(1000) //設置最大的容量
.build();
}
通過設置最大的容量來控制內存,當內存達到最大時,會將最早存入的數(shù)據(jù)刪除,當緩存超出這個容量的時候,會使用Window TinyLfu策略來刪除緩存。
2.基于時間(設置有效期)
@Test
public void test3() {
Cache
.initialCapacity(100)
.expireAfterWrite(Duration.ofSeconds(10)) //設置緩存的有效期,此時就是設置為10s
.build();
}
3.基于引用:設置數(shù)據(jù)的強引用和弱引用,在內存不足的時候jvm會進行垃圾回收,會將弱引用的數(shù)據(jù)進行回收,性能差,不建議使用。
設置一級緩存?
Caffeine配置(配置到ioc中,后續(xù)提供依賴注入進行使用)
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
return Caffeine.newBuilder()
.initialCapacity(init)
.maximumSize(max).build();
}
}
在Controller層中設置一級緩存
@Resource
private TransportInfoService transportInfoService;
@Resource
private Cache
/**
* 根據(jù)運單id查詢運單信息
*
* @param transportOrderId 運單號
* @return 運單信息
*/
@ApiImplicitParams({
@ApiImplicitParam(name = "transportOrderId", value = "運單id")
})
@ApiOperation(value = "查詢", notes = "根據(jù)運單id查詢物流信息")
@GetMapping("{transportOrderId}")
public TransportInfoDTO queryByTransportOrderId(@PathVariable("transportOrderId") String transportOrderId) {
//提供Caffeine先獲取一級緩存,如果沒有緩存就去Mongodb中查數(shù)據(jù)
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)); // 默認緩存數(shù)據(jù)保存1小時
// 構redis緩存管理器
RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisTemplate.getConnectionFactory())
.cacheDefaults(defaultCacheConfiguration)
.transactionAware() // 只在事務成功提交后才會進行緩存的put/evict操作
.build();
return redisCacheManager;
}
}
?在查詢查找的Service上添加對應的注解
@Override
//該注解的在作用就是查詢到的數(shù)據(jù)緩存到redis中其key值就為: transport-info::transportOrderId
//注解其中key的值表示key拼接的參數(shù),這里就是第一個參數(shù)
@Cacheable(value = "transport-info", key = "#p0")
public TransportInfoEntity queryByTransportOrderId(String transportOrderId) {
//通過orderId創(chuàng)建查詢條件,查詢物流信息
return mongoTemplate.findOne(
Query.query(Criteria.where("transportOrderId").is(transportOrderId)),
TransportInfoEntity.class
);
}
添加此注解后,會先在redis的緩存中查找數(shù)據(jù),如果有數(shù)據(jù)就直接返回數(shù)據(jù),如果沒有才會提供Mongodb查詢。
當然為了保證在數(shù)據(jù)修改后還能保證緩存的準確性,這里我們需要在修改操作上添加springCache的注解@CachePut。(該注解的作用就是更新緩存的數(shù)據(jù),所以可以在緩存的增刪改時添加該注解)
@Override
@CachePut(value = "transport-info", key = "#p0")
public TransportInfoEntity saveOrUpdate(String transportOrderId, TransportInfoDetail infoDetail) {
//通過orderId創(chuàng)建查詢條件,查詢物流信息是否存在
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);
}
一級緩存更新的問題
修改后,在一級緩存中的數(shù)據(jù)是不變的,所以為了保證數(shù)據(jù)的準確性,我們先是想到在進行增刪改的時候用this.transportInfoCache.invalidate(transportOrderId);來清除緩存但是在微服務的情況小會出現(xiàn)數(shù)據(jù)不一致的情況。(因為一級緩存在微服務間不是共享的)
@Override
//value和key就是對緩存中key的拼接,這里的key就是transport-info::對應的第一個參數(shù)
@CachePut(value = "transport-info", key = "#p0")
public TransportInfoEntity saveOrUpdate(String transportOrderId, TransportInfoDetail infoDetail) {
//通過orderId創(chuàng)建查詢條件,查詢物流信息是否存在
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());
//清除緩存中的數(shù)據(jù)
this.transportInfoCache.invalidate(transportOrderId);
//進行新增或修改操作 id為空時就進行新增,不為空時進行修改操作
return mongoTemplate.save(updateTransportInfoEntity);
}
為了解決此問題,我們引入了redis中的發(fā)布與訂閱的功能來解決此問題。
類似mq的機制,在發(fā)送對應的key也就是消息,然后訂閱該消息的模塊就會執(zhí)行自定義的操作。
在配置中增加訂閱的配置
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)); // 默認緩存數(shù)據(jù)保存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 消息監(jiān)聽器
* @return 消息監(jiān)聽容器
*/
@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用于監(jiān)聽消息(監(jiān)聽消息后執(zhí)行的自定義方法),刪除caffeine中的數(shù)據(jù)。(可以理解成監(jiān)聽方法)
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消息監(jiān)聽,解決Caffeine一致性的問題
*/
@Component
public class RedisMessageListener extends MessageListenerAdapter {
@Resource
private Cache
@Override
public void onMessage(Message message, byte[] pattern) {
//獲取到消息中的運單id
String transportOrderId = Convert.toStr(message);
//將本jvm中的緩存刪除掉
this.transportInfoCache.invalidate(transportOrderId);
}
}
在增刪改的方法中向對應的頻道發(fā)送消息。
@Override
//value和key就是對緩存中key的拼接,這里的key就是transport-info::對應的第一個參數(shù)
@CachePut(value = "transport-info", key = "#p0")
public TransportInfoEntity saveOrUpdate(String transportOrderId, TransportInfoDetail infoDetail) {
//通過orderId創(chuàng)建查詢條件,查詢物流信息是否存在
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());
//清除緩存中的數(shù)據(jù)
this.stringRedisTemplate.convertAndSend(RedisConfig.CHANNEL_TOPIC, transportOrderId);
//進行新增或修改操作 id為空時就進行新增,不為空時進行修改操作
return mongoTemplate.save(updateTransportInfoEntity);
}
最終保證了一級緩存的準確性。
問: 那redis的這種機制也可以完成mq的一系列操作,為什么微服務中沒有大量使用呢?
答:redis的發(fā)布訂閱沒有可靠性的處理,沒有像mq那樣的重試機制,所以我們微服務中沒有大量使用。
柚子快報邀請碼778899分享:緩存 Caffeine的使用
相關閱讀
本文內容根據(jù)網絡資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉載請注明,如有侵權,聯(lián)系刪除。