柚子快報邀請碼778899分享:緩存 Caffeine的使用
柚子快報邀請碼778899分享:緩存 Caffeine的使用
項目結(jié)構(gòu)圖
?運行反向代理服務(wù)器也就是負(fù)責(zé)反向代理到三個nginx的nginx,該nignx也負(fù)責(zé)前端頁面的跳轉(zhuǎn)。
該nginx的conf為下:
突出位置就是該nginx需要反向代理的其他nginx的IP和端口。
在資源比較有限的時候我們通常不適用上述的機構(gòu),而是用使用Caffeine進行二級緩存,在Cffeine沒有查找到數(shù)據(jù),我們才會去redis中查詢數(shù)據(jù)。
Caffeine是什么?
Caffeine和redis都是內(nèi)存級別的緩存,為什么要使用在這兩緩存作為二級緩存,它們兩有什么區(qū)別呢?
雖然它們都是內(nèi)存級別的緩存,redis是需要單獨部署的,其需要一個單獨的進程,在tomcat訪問redis時需要網(wǎng)絡(luò)通信的開銷,而Caffeine跟我們項目代碼是寫在一起的,它是JVM級別的緩存,用的就是Java中的堆內(nèi)存,無需網(wǎng)絡(luò)的通信的開銷,在Caffeine找不到數(shù)據(jù)后才會去redis中查找。
Caffeine的使用
導(dǎo)入依賴
進行測試
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) //設(shè)置緩存的初始化容量
.maximumSize(1000) //設(shè)置最大的容量
.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);
}
}
驅(qū)逐策略(面試點: 使用Caffeine為了防止內(nèi)存溢出,怎么做?)
為了防止一直往內(nèi)存里裝數(shù)值導(dǎo)致占用內(nèi)存,所以Caffeine給我們提供了驅(qū)逐策略。
1.基于容量(設(shè)置緩存的上限)
@Test
public void test2() {
Cache
.initialCapacity(100) //設(shè)置緩存的初始化容量
.maximumSize(1000) //設(shè)置最大的容量
.build();
}
通過設(shè)置最大的容量來控制內(nèi)存,當(dāng)內(nèi)存達到最大時,會將最早存入的數(shù)據(jù)刪除,當(dāng)緩存超出這個容量的時候,會使用Window TinyLfu策略來刪除緩存。
2.基于時間(設(shè)置有效期)
@Test
public void test3() {
Cache
.initialCapacity(100)
.expireAfterWrite(Duration.ofSeconds(10)) //設(shè)置緩存的有效期,此時就是設(shè)置為10s
.build();
}
3.基于引用:設(shè)置數(shù)據(jù)的強引用和弱引用,在內(nèi)存不足的時候jvm會進行垃圾回收,會將弱引用的數(shù)據(jù)進行回收,性能差,不建議使用。
設(shè)置一級緩存?
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層中設(shè)置一級緩存
@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);
}
設(shè)置二級緩存(使用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相關(guān)的配置
*/
@Configuration
public class RedisConfig {
/**
* 存儲的默認(rèn)有效期時間,單位:小時
*/
@Value("${redis.ttl:1}")
private Integer redisTtl;
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
// 默認(rèn)配置
RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
// 設(shè)置key的序列化方式為字符串
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 設(shè)置value的序列化方式為json格式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues() // 不緩存null
.entryTtl(Duration.ofHours(redisTtl)); // 默認(rèn)緩存數(shù)據(jù)保存1小時
// 構(gòu)redis緩存管理器
RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisTemplate.getConnectionFactory())
.cacheDefaults(defaultCacheConfiguration)
.transactionAware() // 只在事務(wù)成功提交后才會進行緩存的put/evict操作
.build();
return redisCacheManager;
}
}
?在查詢查找的Service上添加對應(yīng)的注解
@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查詢。
當(dāng)然為了保證在數(shù)據(jù)修改后還能保證緩存的準(zhǔn)確性,這里我們需要在修改操作上添加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)) {
//如果存在就獲取對應(yīng)的信息,在infoList中添加對應(yīng)的物流信息
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ù)的準(zhǔn)確性,我們先是想到在進行增刪改的時候用this.transportInfoCache.invalidate(transportOrderId);來清除緩存但是在微服務(wù)的情況小會出現(xiàn)數(shù)據(jù)不一致的情況。(因為一級緩存在微服務(wù)間不是共享的)
@Override
//value和key就是對緩存中key的拼接,這里的key就是transport-info::對應(yīng)的第一個參數(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)) {
//如果存在就獲取對應(yīng)的信息,在infoList中添加對應(yīng)的物流信息
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ā)送對應(yīng)的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相關(guān)的配置
*/
@Configuration
public class RedisConfig {
/**
* 存儲的默認(rèn)有效期時間,單位:小時
*/
@Value("${redis.ttl:1}")
private Integer redisTtl;
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
// 默認(rèn)配置
RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
// 設(shè)置key的序列化方式為字符串
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 設(shè)置value的序列化方式為json格式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues() // 不緩存null
.entryTtl(Duration.ofHours(redisTtl)); // 默認(rèn)緩存數(shù)據(jù)保存1小時
// 構(gòu)redis緩存管理器
RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisTemplate.getConnectionFactory())
.cacheDefaults(defaultCacheConfiguration)
.transactionAware() // 只在事務(wù)成功提交后才會進行緩存的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);
}
}
在增刪改的方法中向?qū)?yīng)的頻道發(fā)送消息。
@Override
//value和key就是對緩存中key的拼接,這里的key就是transport-info::對應(yīng)的第一個參數(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)) {
//如果存在就獲取對應(yīng)的信息,在infoList中添加對應(yīng)的物流信息
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);
}
最終保證了一級緩存的準(zhǔn)確性。
問: 那redis的這種機制也可以完成mq的一系列操作,為什么微服務(wù)中沒有大量使用呢?
答:redis的發(fā)布訂閱沒有可靠性的處理,沒有像mq那樣的重試機制,所以我們微服務(wù)中沒有大量使用。
柚子快報邀請碼778899分享:緩存 Caffeine的使用
相關(guān)閱讀
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。