Redis+Caffeine實(shí)現(xiàn)兩級(jí)緩存的教程
Redis+Caffeine 實(shí)現(xiàn)兩級(jí)緩存
背景
? 事情的開始是這樣的,前段時(shí)間接了個(gè)需求,給公司的商城官網(wǎng)提供一個(gè)查詢預(yù)計(jì)送達(dá)時(shí)間的接口。接口很簡單,根據(jù)請(qǐng)求傳的城市+倉庫+發(fā)貨時(shí)間查詢快遞的預(yù)計(jì)送達(dá)時(shí)間。因?yàn)樯坛窍聠尉蜁?huì)調(diào)用這個(gè)接口,所以對(duì)接口的性能要求還是挺高的,據(jù)老員工的說法是特別是大促的時(shí)候,訪問量還是比較大的。
? 因?yàn)閿?shù)據(jù)量不是很大,每天會(huì)全量推今天和明天的預(yù)計(jì)送達(dá)時(shí)間到MySQL,總數(shù)據(jù)量大約7k+。每次推完數(shù)據(jù)后會(huì)把數(shù)據(jù)全量寫入到redis中,做一個(gè)緩存預(yù)熱,然后設(shè)置過期時(shí)間為1天。
? 鑒于之前Redis集群出現(xiàn)過壓力過大查詢緩慢的情況,進(jìn)一步保證接口的高性能和高可用,防止redis出現(xiàn)壓力大,查詢慢,緩存雪崩,緩存穿透等問題,我們最終采用了Reids + Caffeine兩級(jí)緩存的策略。
本地緩存優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 本地緩存,基于本地內(nèi)存,查詢速度是很快的。適用于:實(shí)時(shí)性要求不高,更新頻率不高等場(chǎng)景。(我們的數(shù)據(jù)每天凌晨更新一次,總量7k左右)
- 查詢本地緩存與查詢遠(yuǎn)程緩存相比可以減少網(wǎng)絡(luò)的I/O,降低網(wǎng)絡(luò)上的一些消耗。(我們的redis之前出現(xiàn)過查詢緩慢的情況)
缺點(diǎn):
- Caffeine既然是本地緩存,在分布式環(huán)境的情況下就要考慮各個(gè)節(jié)點(diǎn)之間緩存的一致性問題,一個(gè)節(jié)點(diǎn)的本地緩存更新了,怎么可以同步到其他的節(jié)點(diǎn)。
- Caffeine不支持持久化的存儲(chǔ)。
- Caffeine使用本地內(nèi)存,需要合理設(shè)置大小,避免內(nèi)存溢出。
流程圖

代碼實(shí)現(xiàn)
MySQL表
CREATE TABLE `t_estimated_arrival_date` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵id', `warehouse_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '貨倉id', `warehouse` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '發(fā)貨倉', `city` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '簽收城市', `delivery_date` date NULL DEFAULT NULL COMMENT '發(fā)貨時(shí)間', `estimated_arrival_date` date NULL DEFAULT NULL COMMENT '預(yù)計(jì)到貨日期', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_warehouse_id_city_delivery_date`(`warehouse_id`, `city`, `delivery_date`) USING BTREE ) ENGINE = InnoDB COMMENT = '預(yù)計(jì)到貨時(shí)間表(具體到day:T, T+1,近90天到貨時(shí)間眾數(shù))' ROW_FORMAT = Dynamic; INSERT INTO `t_estimated_arrival_date` VALUES (9, '6', '湖熟正常倉', '蘭州市', '2024-07-08', '2024-07-10'); INSERT INTO `t_estimated_arrival_date` VALUES (10, '6', '湖熟正常倉', '蘭州市', '2024-07-09', '2024-07-11'); INSERT INTO `t_estimated_arrival_date` VALUES (11, '6', '湖熟正常倉', '興安盟', '2024-07-08', '2024-07-11'); INSERT INTO `t_estimated_arrival_date` VALUES (12, '6', '湖熟正常倉', '興安盟', '2024-07-09', '2024-07-12'); INSERT INTO `t_estimated_arrival_date` VALUES (13, '6', '湖熟正常倉', '其他', '2024-07-08', '2024-07-19'); INSERT INTO `t_estimated_arrival_date` VALUES (14, '6', '湖熟正常倉', '其他', '2024-07-09', '2024-07-20'); INSERT INTO `t_estimated_arrival_date` VALUES (15, '6', '湖熟正常倉', '內(nèi)江市', '2024-07-08', '2024-07-10'); INSERT INTO `t_estimated_arrival_date` VALUES (16, '6', '湖熟正常倉', '內(nèi)江市', '2024-07-09', '2024-07-11'); INSERT INTO `t_estimated_arrival_date` VALUES (17, '6', '湖熟正常倉', '涼山彝族自治州', '2024-07-08', '2024-07-11'); INSERT INTO `t_estimated_arrival_date` VALUES (18, '6', '湖熟正常倉', '涼山彝族自治州', '2024-07-09', '2024-07-12'); INSERT INTO `t_estimated_arrival_date` VALUES (19, '6', '湖熟正常倉', '包頭市', '2024-07-08', '2024-07-11'); INSERT INTO `t_estimated_arrival_date` VALUES (20, '6', '湖熟正常倉', '包頭市', '2024-07-09', '2024-07-12'); INSERT INTO `t_estimated_arrival_date` VALUES (21, '6', '湖熟正常倉', '北京城區(qū)', '2024-07-08', '2024-07-10'); INSERT INTO `t_estimated_arrival_date` VALUES (22, '6', '湖熟正常倉', '北京城區(qū)', '2024-07-09', '2024-07-11');
pom.xm
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redis連接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>application.yml
server:
port: 9001
spring:
application:
name: springboot-redis
datasource:
name: demo
url: jdbc:mysql://localhost:3306/test?userUnicode=true&&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username:
password:
# mybatis相關(guān)配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
cache-enabled: true
use-generated-keys: true
default-executor-type: REUSE
use-actual-param-name: true
# 打印日志
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
redis:
host: 192.168.117.73
port: 6379
password: root
# redis:
# lettuce:
# cluster:
# refresh:
# adaptive: true
# period: 10S
# pool:
# max-idle: 50
# min-idle: 8
# max-active: 100
# max-wait: -1
# timeout: 100000
# cluster:
# nodes:
# - 192.168.117.73:6379
logging:
level:
com.itender.redis.mapper: debug
配置類
- RedisConfig
/**
* @author yuanhewei
* @date 2024/5/31 16:18
* @description
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(mapper);
// 如果不序列化在key value 使用redis客戶端工具 直連redis服務(wù)器 查看數(shù)據(jù)時(shí) 前面會(huì)有一個(gè) \xac\xed\x00\x05t\x00\x05 字符串
// StringRedisSerializer 來序列化和反序列化 String 類型 redis 的 key value
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
// StringRedisSerializer 來序列化和反序列化 hash 類型 redis 的 key value
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}- CaffeineConfig
/**
* @author yuanhewei
* @date 2024/7/9 14:16
* @description
*/
@Configuration
public class CaffeineConfig {
/**
* Caffeine 配置類
* initialCapacity:初始緩存空間大小
* maximumSize:緩存的最大數(shù)量,設(shè)置這個(gè)值避免內(nèi)存溢出
* expireAfterWrite:指定緩存的過期時(shí)間,是最后一次寫操作的一個(gè)時(shí)間
* 容量的大小要根據(jù)自己的實(shí)際應(yīng)用場(chǎng)景設(shè)置
*
* @return
*/
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
// 初始大小
.initialCapacity(128)
//最大數(shù)量
.maximumSize(1024)
//過期時(shí)間
.expireAfterWrite(60, TimeUnit.SECONDS)
.build();
}
@Bean
public CacheManager cacheManager(){
CaffeineCacheManager cacheManager=new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(128)
.maximumSize(1024)
.expireAfterWrite(60, TimeUnit.SECONDS));
return cacheManager;
}
}Mapper
這里采用了Mybatis Plus
/**
* @author yuanhewei
* @date 2024/7/9 18:11
* @description
*/
@Mapper
public interface EstimatedArrivalDateMapper extends BaseMapper<EstimatedArrivalDateEntity> {
} Service
/**
* @author yuanhewei
* @date 2024/7/9 14:25
* @description
*/
public interface DoubleCacheService {
/**
* 查詢一級(jí)送達(dá)時(shí)間-常規(guī)方式
*
* @param request
* @return
*/
EstimatedArrivalDateEntity getEstimatedArrivalDateCommon(EstimatedArrivalDateEntity request);
/**
* 查詢一級(jí)送達(dá)時(shí)間-注解方式
*
* @param request
* @return
*/
EstimatedArrivalDateEntity getEstimatedArrivalDate(EstimatedArrivalDateEntity request);
}實(shí)現(xiàn)類
/**
* @author yuanhewei
* @date 2024/7/9 14:26
* @description
*/
@Slf4j
@Service
public class DoubleCacheServiceImpl implements DoubleCacheService {
@Resource
private Cache<String, Object> caffeineCache;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private EstimatedArrivalDateMapper estimatedArrivalDateMapper;
@Override
public EstimatedArrivalDateEntity getEstimatedArrivalDateCommon(EstimatedArrivalDateEntity request) {
String key = request.getDeliveryDate() + RedisConstants.COLON + request.getWarehouseId() + RedisConstants.COLON + request.getCity();
log.info("Cache key: {}", key);
Object value = caffeineCache.getIfPresent(key);
if (Objects.nonNull(value)) {
log.info("get from caffeine");
return EstimatedArrivalDateEntity.builder().estimatedArrivalDate(value.toString()).build();
}
value = redisTemplate.opsForValue().get(key);
if (Objects.nonNull(value)) {
log.info("get from redis");
caffeineCache.put(key, value);
return EstimatedArrivalDateEntity.builder().estimatedArrivalDate(value.toString()).build();
}
log.info("get from mysql");
DateTime deliveryDate = DateUtil.parse(request.getDeliveryDate(), "yyyy-MM-dd");
EstimatedArrivalDateEntity estimatedArrivalDateEntity = estimatedArrivalDateMapper.selectOne(new QueryWrapper<EstimatedArrivalDateEntity>()
.eq("delivery_date", deliveryDate)
.eq("warehouse_id", request.getWarehouseId())
.eq("city", request.getCity())
);
redisTemplate.opsForValue().set(key, estimatedArrivalDateEntity.getEstimatedArrivalDate(), 120, TimeUnit.SECONDS);
caffeineCache.put(key, estimatedArrivalDateEntity.getEstimatedArrivalDate());
return EstimatedArrivalDateEntity.builder().estimatedArrivalDate(estimatedArrivalDateEntity.getEstimatedArrivalDate()).build();
}
@DoubleCache(cacheName = "estimatedArrivalDate", key = {"#request.deliveryDate", "#request.warehouseId", "#request.city"},
type = DoubleCache.CacheType.FULL)
@Override
public EstimatedArrivalDateEntity getEstimatedArrivalDate(EstimatedArrivalDateEntity request) {
DateTime deliveryDate = DateUtil.parse(request.getDeliveryDate(), "yyyy-MM-dd");
EstimatedArrivalDateEntity estimatedArrivalDateEntity = estimatedArrivalDateMapper.selectOne(new QueryWrapper<EstimatedArrivalDateEntity>()
.eq("delivery_date", deliveryDate)
.eq("warehouse_id", request.getWarehouseId())
.eq("city", request.getCity())
);
return EstimatedArrivalDateEntity.builder().estimatedArrivalDate(estimatedArrivalDateEntity.getEstimatedArrivalDate()).build();
}
}
這里的代碼本來是采用了常規(guī)的寫法,沒有采用自定義注解的方式,注解的方式是參考了后面那位大佬的文章,加以修改實(shí)現(xiàn)的。因?yàn)槲业腃acheKey可能存在多個(gè)屬性值的組合。
Annotitions
/**
* @author yuanhewei
* @date 2024/7/9 14:51
* @description
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {
/**
* 緩存名稱
*
* @return
*/
String cacheName();
/**
* 緩存的key,支持springEL表達(dá)式
*
* @return
*/
String[] key();
/**
* 過期時(shí)間,單位:秒
*
* @return
*/
long expireTime() default 120;
/**
* 緩存類型
*
* @return
*/
CacheType type() default CacheType.FULL;
enum CacheType {
/**
* 存取
*/
FULL,
/**
* 只存
*/
PUT,
/**
* 刪除
*/
DELETE
}
}
Aspect
/**
* @author yuanhewei
* @date 2024/7/9 14:51
* @description
*/
@Slf4j
@Component
@Aspect
public class DoubleCacheAspect {
@Resource
private Cache<String, Object> caffeineCache;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Pointcut("@annotation(com.itender.redis.annotation.DoubleCache)")
public void doubleCachePointcut() {
}
@Around("doubleCachePointcut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 拼接解析springEl表達(dá)式的map
String[] paramNames = signature.getParameterNames();
Object[] args = point.getArgs();
TreeMap<String, Object> treeMap = new TreeMap<>();
for (int i = 0; i < paramNames.length; i++) {
treeMap.put(paramNames[i], args[i]);
}
DoubleCache annotation = method.getAnnotation(DoubleCache.class);
String elResult = DoubleCacheUtil.arrayParse(Lists.newArrayList(annotation.key()), treeMap);
String realKey = annotation.cacheName() + RedisConstants.COLON + elResult;
// 強(qiáng)制更新
if (annotation.type() == DoubleCache.CacheType.PUT) {
Object object = point.proceed();
redisTemplate.opsForValue().set(realKey, object, annotation.expireTime(), TimeUnit.SECONDS);
caffeineCache.put(realKey, object);
return object;
}
// 刪除
else if (annotation.type() == DoubleCache.CacheType.DELETE) {
redisTemplate.delete(realKey);
caffeineCache.invalidate(realKey);
return point.proceed();
}
// 讀寫,查詢Caffeine
Object caffeineCacheObj = caffeineCache.getIfPresent(realKey);
if (Objects.nonNull(caffeineCacheObj)) {
log.info("get data from caffeine");
return caffeineCacheObj;
}
// 查詢Redis
Object redisCache = redisTemplate.opsForValue().get(realKey);
if (Objects.nonNull(redisCache)) {
log.info("get data from redis");
caffeineCache.put(realKey, redisCache);
return redisCache;
}
log.info("get data from database");
Object object = point.proceed();
if (Objects.nonNull(object)) {
// 寫入Redis
log.info("get data from database write to cache: {}", object);
redisTemplate.opsForValue().set(realKey, object, annotation.expireTime(), TimeUnit.SECONDS);
// 寫入Caffeine
caffeineCache.put(realKey, object);
}
return object;
}
}
因?yàn)樽⒔馍系呐渲靡С諷pring的EL表達(dá)式。
public static String parse(String elString, SortedMap<String, Object> map) {
elString = String.format("#{%s}", elString);
// 創(chuàng)建表達(dá)式解析器
ExpressionParser parser = new SpelExpressionParser();
// 通過evaluationContext.setVariable可以在上下文中設(shè)定變量。
EvaluationContext context = new StandardEvaluationContext();
map.forEach(context::setVariable);
// 解析表達(dá)式
Expression expression = parser.parseExpression(elString, new TemplateParserContext());
// 使用Expression.getValue()獲取表達(dá)式的值,這里傳入了Evaluation上下文
return expression.getValue(context, String.class);
}
public static String arrayParse(List<String> elStrings, SortedMap<String, Object> map) {
List<String> result = Lists.newArrayList();
elStrings.forEach(elString -> {
elString = String.format("#{%s}", elString);
// 創(chuàng)建表達(dá)式解析器
ExpressionParser parser = new SpelExpressionParser();
// 通過evaluationContext.setVariable可以在上下文中設(shè)定變量。
EvaluationContext context = new StandardEvaluationContext();
map.forEach(context::setVariable);
// 解析表達(dá)式
Expression expression = parser.parseExpression(elString, new TemplateParserContext());
// 使用Expression.getValue()獲取表達(dá)式的值,這里傳入了Evaluation上下文
result.add(expression.getValue(context, String.class));
});
return String.join(RedisConstants.COLON, result);
}Controller
/**
* @author yuanhewei
* @date 2024/7/9 14:14
* @description
*/
@RestController
@RequestMapping("/doubleCache")
public class DoubleCacheController {
@Resource
private DoubleCacheService doubleCacheService;
@PostMapping("/common")
public EstimatedArrivalDateEntity getEstimatedArrivalDateCommon(@RequestBody EstimatedArrivalDateEntity estimatedArrivalDate) {
return doubleCacheService.getEstimatedArrivalDateCommon(estimatedArrivalDate);
}
@PostMapping("/annotation")
public EstimatedArrivalDateEntity getEstimatedArrivalDate(@RequestBody EstimatedArrivalDateEntity estimatedArrivalDate) {
return doubleCacheService.getEstimatedArrivalDate(estimatedArrivalDate);
}
}代碼中演示了Redis + Caffeine實(shí)現(xiàn)兩級(jí)緩存的方式,一種是傳統(tǒng)常規(guī)的方式,另一種是基于注解的方式實(shí)現(xiàn)的。具體實(shí)現(xiàn)可以根據(jù)自己項(xiàng)目中的實(shí)際場(chǎng)景。
最后的測(cè)試結(jié)果也是兩種方式都可以實(shí)現(xiàn)查詢先走一級(jí)緩存;一級(jí)緩存不存在查詢二級(jí)緩存,然后寫入一級(jí)緩存;二級(jí)緩存不存在,查詢MySQL然后寫入二級(jí)緩存,再寫入一級(jí)緩存的目的。測(cè)試結(jié)果就不貼出來了
總結(jié)
本文介紹Redis+Caffeine實(shí)現(xiàn)兩級(jí)緩存的方式。一種是常規(guī)的方式,一種的基于注解的方式。具體的實(shí)現(xiàn)可根據(jù)自己項(xiàng)目中的業(yè)務(wù)場(chǎng)景。
至于為什么要用Redis+Caffeine的方式,文章也提到了,目前我們Redis集群壓力還算挺大的,而且接口對(duì)RT的要求也是比較高的。有一點(diǎn)好的就是我們的數(shù)據(jù)是每天全量推一邊,總量也不大,實(shí)時(shí)性要求也不強(qiáng)。所以就很適合本地緩存的方式。
使用本地緩存也要注意設(shè)置容量的大小和過期時(shí)間,否則容易出現(xiàn)內(nèi)存溢出。
其實(shí)現(xiàn)實(shí)中很多的場(chǎng)景直接使用Redis就可以搞定的,沒必要硬要使用Caffeine。這里也只是簡單的介紹了最簡單基礎(chǔ)的實(shí)現(xiàn)方式。對(duì)于其他一些復(fù)雜的場(chǎng)景還要根據(jù)自己具體的業(yè)務(wù)進(jìn)行設(shè)計(jì)。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
解決mybatis generator MySQL自增ID出現(xiàn)重復(fù)問題MySQLIntegrityC
在MySQL中使用MyBatis時(shí),可能會(huì)遇到由于主鍵重復(fù)導(dǎo)致的插入失敗問題,此問題通常發(fā)生在連續(xù)插入多條數(shù)據(jù)時(shí),如果selectKey的order配置錯(cuò)誤,如使用BEFORE而不是AFTER,將會(huì)導(dǎo)致獲取的ID未更新,引起主鍵重復(fù)錯(cuò)誤,正確的配置應(yīng)使用AFTER2024-10-10
基于spring 方法級(jí)緩存的多種實(shí)現(xiàn)
下面小編就為大家?guī)硪黄趕pring 方法級(jí)緩存的多種實(shí)現(xiàn)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
如何利用 Either 和 Option 進(jìn)行函數(shù)式錯(cuò)誤處理
這篇文章主要介紹了如何利用 Either 和 Option 進(jìn)行函數(shù)式錯(cuò)誤處理。在 Java 中,錯(cuò)誤的處理在傳統(tǒng)上由異常以及創(chuàng)建和傳播異常的語言支持進(jìn)行。但是,如果不存在結(jié)構(gòu)化異常處理又如何呢?,需要的朋友可以參考下2019-06-06
java中計(jì)算字符串長度的方法及u4E00與u9FBB的認(rèn)識(shí)
字符串采用unicode編碼的方式時(shí),計(jì)算字符串長度的方法找出UNICODE編碼中的漢字的代表的范圍“\u4E00” 到“\u9FBB”之間感興趣的朋友可以參考本文,或許對(duì)你有所幫助2013-01-01
IDEA調(diào)試功能使用總結(jié)(step?over/step?into/force?step?into/step?o
本文主要介紹了IDEA調(diào)試功能使用總結(jié)(step?over/step?into/force?step?into/step?out),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
java數(shù)據(jù)結(jié)構(gòu)之棧的詳解
這篇文章主要為大家詳細(xì)介紹了Java數(shù)據(jù)結(jié)構(gòu)的棧的應(yīng)用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能給你帶來幫助2021-08-08
SpringDataJPA實(shí)體類關(guān)系映射配置方式
這篇文章主要介紹了SpringDataJPA實(shí)體類關(guān)系映射配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12

