Commit d2d81138 by 仲光辉

init: initialization project

parents
# Created by .ignore support plugin (hsz.mobi)
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
.idea
*.iml
*.ipr
*.iws
.gradle
build
# 蛋壳创意科技--技术分享--缓存
## 缓存的概念
### 外存
> ​ 外储存器是指除计算机内存及CPU 缓存以外的储存器,此类储存器一般断电后仍然能保存数据。
> 常见的外存储器有硬盘、软盘、光盘、U 盘等,一般的软件都是安装在外存中(windows系统指的是CDEF 盘, Linux 系统指的是挂载点)。
### 内存
> ​ 内存是计算机中重要的部件之一,它是与CPU 进行沟通的桥梁。计算机中所有程序的
> 运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。
> ​ 内存(Memory)也被称为内存储器,其作用是用于暂时存放CPU 中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU 就会把需要运算的数据调到内存中进行运算,当运算完成后CPU 再将结果传送出来,内存的运行也决定了计算机的稳定运行,此类储存器一般断电后数据就会被清空。
### 缓存
> ​ 缓存就是把一些外存上的数据保存到内存上而已,怎么保存到内存上呢,我们运行的所有程序,里面的变量值都是放在内存上的,所以说如果要想使一个值放到内存上,实质就是在获得这个变量之后,用一个生存期较长的变量存放你想存放的值,在java中一些缓存一般都是通过map 集合来做的。
>
> ​ 广义的缓存是把一些慢存(较慢的外存)上的数据保存到快存(较快的存储)上,简单讲就是,如果某些资源或者数据会被频繁的使用,而这些资源或数据存储在系统外部,比如数据库、硬盘文件等,那么每次操作这些数据的时候都从数据库或者硬盘上去获取,速度会很慢,会造成性能问题(系统停工待料)。于是我们把这些数据冗余一份到快存里面,每次操作的时候,先到快存里面找,看有没有这些数据,如果有,那么就直接使用,如果没有那么就获取它,并复制一份到快存中,下一次访问的时候就可以直接从快存中获取。从而节省大量的时间,可以看出,缓存是一种典型的空间换时间的方案。
>
> > 生活中这样的例子处处可见,举例:
> >
> > - 举例1 --CPU--L1/L2--内存--磁盘
> > CPU 需要数据时先从L1/L2 中读取,如果没有到内存中找,如果还没有会到磁盘上找。
> > - 举例2 --Maven
> > 还有如用过Maven 的朋友都应该知道,我们找依赖的时候,先从本机仓库找,再从本地服务器仓库找,最后到远程仓库服务器找。
> > - 举例3 --京东仓储
> > 还有如京东的物流为什么那么快?他们在各个地都有分仓库,如果该仓库有货物那么送货的速度是非常快的。
> > - 举例4 --数据库中的索引,也是以空间换取时间,缓存也是以空间换取时间。
#### 缓存的重要指标
##### 缓存命中率
> 表明缓存是否运行良好的
> 即【从缓存中读取数据的次数】与【总读取次数】的比率,命中率越高越好:
>
> ###### 缓存命中率
>
> > 命中率= `从缓存中读取次数`/ (`总读取次数`[==从缓存中读取次数==+ ==从慢速设备上读取
> > 的次数==])
>
> ###### 缓存MISS率
>
> > Miss 率= `没有从缓存中读取的次数`/ (`总读取次数`[==从缓存中读取次数==+ ==从慢速设备
> > 上读取的次数==])
>
> 这是一个非常重要的监控指标,如果做缓存一定要健康这个指标来看缓存是否工作良好
##### 缓存移除策略
> 不同的移除策略实际上看的是不同的指标
> 即如果缓存满了,从缓存中移除数据的策略;常见的有LFU、LRU、FIFO:
>
> ###### FIFO : `First In First Out`
>
> > 先进先出算法,即先放入缓存的先被移除
>
> ###### LRU: `Least Recently Used`
>
> > 久未使用算法,使用时间距离现在最久的那个被移除
>
> ###### LFU: `Least Frequently Used`
>
> > 最近最少使用算法,一定时间段内使用次数(频率)最少的那个被移除
>
> ###### ==TTL: `Time To Live`==
>
> > 存活期,即从缓存中创建时间点开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)
>
> ###### TTI:Time To Idle
>
> > 空闲期,即一个数据多久没被访问将从缓存中移除的时间。
>
> <b>实际设计缓存时以上重要指标都应该考虑进去,当然根据实际需求可能有的指标并不会采用进设计去</b>
## 缓存的逻辑处理流程
![](https://img.mercymodest.com/public/20210326145533.png)
### 代码示例
language:`java`
cache: `redis`
```java
@Override
public Optional<Provinces> detailWithRedisCache(String provincesId) {
if (StrUtil.isBlank(provincesId)) {
return Optional.empty();
}
String key = StrUtil.format(Const.RedisKey.KEY_PATTERN_PROVINCES_DETAIL, provincesId);
String cache = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(cache)) {
if (log.isDebugEnabled()) {
log.debug("detailWithRedisCache ==> {} success hit cache {}", Thread.currentThread().getName(), cache);
}
// 缓存命中
return gson.fromJson(cache, new TypeToken<Optional<Provinces>>() {
}.getType());
} else {
// 数据没有缓存
// TODO 缓存击穿 分布式锁 etc. Redisson
Provinces provinces = this.getProvincesByProvinces(provincesId);
Optional<Provinces> provincesOptional;
if (BeanUtil.isNotEmpty(provinces)) {
if (log.isDebugEnabled()) {
log.debug("detailWithRedisCache ==> {} success get provinces by {} from database.", Thread.currentThread().getName(), provincesId);
}
provincesOptional = Optional.of(provinces);
final long timeoutInMinutes = Const.CacheTimeout.TIMEOUT_PROVINCES_DETAIL_MINUTES;
stringRedisTemplate.opsForValue().set(key, gson.toJson(provincesOptional), timeoutInMinutes, TimeUnit.MINUTES);
} else {
// 防止缓存穿透
if (log.isDebugEnabled()) {
log.debug("detailWithRedisCache ==> {} preven cache break down .", Thread.currentThread().getName());
}
final long timeoutInMinutes = Const.CacheTimeout.TIMEOUT_PROVINCES_DETAIL_MINUTES;
provincesOptional = Optional.empty();
stringRedisTemplate.opsForValue()
.set(key, gson.toJson(provincesOptional), timeoutInMinutes, TimeUnit.MINUTES);
}
// 防止缓存雪崩
// 每一个缓存 30秒 以内的随机差距
Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
expire += RandomUtil.randomLong(30);
stringRedisTemplate.expire(key, expire, TimeUnit.SECONDS);
return provincesOptional;
}
}
```
## Spring Cache
![](https://img.mercymodest.com/public/20210326174415.png)
### `@Cacheable`
> ![](https://img.mercymodest.com/public/20210326174521.png)
### `@CachePut`
> ![](https://img.mercymodest.com/public/20210326174556.png)
### `@CacheEivct`
> ![](https://img.mercymodest.com/public/20210326174630.png)
![](https://img.mercymodest.com/public/20210326174746.png)
### 举个小栗子
#### 使用`spring cache` 的两个前提和一个自定义
##### 前提一: 开启缓存
`@EnableCaching`
![](https://img.mercymodest.com/public/20210326175249.png)
##### 前提二: 注册 `CacheManager`
`org.springframework.cache.CacheManager`
etc.
```java
/**
* 注册一个 {@link CacheManager}
*
* @param connectionFactory {@link RedisConnectionFactory}
* @return {@link CacheManager}
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager
.builder(connectionFactory)
.cacheDefaults(
RedisCacheConfiguration.defaultCacheConfig()
//缓存时间绝对过期时间20s
.entryTtl(Duration.ofSeconds(120)))
.transactionAware()
.build();
}
```
##### 可选自定义: 缓存 `key` :`KeyGenerator`
`org.springframework.cache.interceptor.KeyGenerator`
etc.
tips: Spring Cache 的默认 KeyGenerator 默认也是这么做滴
```java
/**
* {@link Cacheable} 默认 {@link KeyGenerator}
*
* @return {@link KeyGenerator}
*/
@Bean("customerKeyGenerator")
public KeyGenerator keyGenerator() {
return (Object target, Method method, Object... params) -> {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < params.length; i++) {
stringBuilder.append(params[i]);
}
return stringBuilder.toString();
};
}
```
#### 缓存方法
```java
@Cacheable(cacheNames = "provinces_detail_cache", keyGenerator = "customerKeyGenerator")
@Override
public Optional<Provinces> detailWithSpringCache(String provincesId) {
Provinces provinces = this.getProvincesByProvinces(provincesId);
return Optional.ofNullable(provinces);
}
```
#### 缓存更新方法: 直接删除缓存
```java
@CacheEvict(cacheNames = "provinces_detail_cache", key = "#provinces.provinceId")
@Override
public Optional<Integer> updateProvinces(Provinces provinces) {
LambdaQueryWrapper<Provinces> provincesLambdaQueryWrapper = new LambdaQueryWrapper<Provinces>()
.eq(Provinces::getProvinceId, provinces.getProvinceId());
int update = baseMapper.update(provinces, provincesLambdaQueryWrapper);
return Optional.of(update);
}
```
#### 效果演示
url: GET http://localhost:9102/test/440000
##### 调用 缓存方法
![](https://img.mercymodest.com/public/20210326180109.png)
##### 调用 更新缓存方法
![](https://img.mercymodest.com/public/20210326180401.png)
## 缓存过期与数据一致性问题
![](https://img.mercymodest.com/public/20210326174900.png)
![](https://img.mercymodest.com/public/20210326174939.png)
## 缓存击穿
> ​ 在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为**缓存击穿**
![](https://img.mercymodest.com/public/20210326182420.png)
### 解决方案示例
- 分布式锁: etc. `Redis 分布式锁` `Zookeepr 分布式锁` `Redisson`
## 缓存穿透
> ​ 那么请求去查询一条压根儿数据库中根本就不存在的数据,也就是缓存和数据库都查询不到这条数据,但是请求每次都会打到数据库上面去。
>
> 这种查询不存在数据的现象我们称为**缓存穿透**。
![](https://img.mercymodest.com/public/20210326182212.png)
布隆过滤器博文参考:[Bloom Filter](https://www.cnblogs.com/z941030/p/9218356.html)
#### 小示例
##### 数据初始化
```java
@PostConstruct
public void postConstruct() {
bitMapBloomFilter = new BitMapBloomFilter(10);
LambdaQueryWrapper<Provinces> provincesLambdaQueryWrapper = new LambdaQueryWrapper<Provinces>()
.select(Provinces::getProvinceId);
baseMapper.selectList(provincesLambdaQueryWrapper)
.stream()
.filter(Objects::nonNull)
.map(Provinces::getProvinceId)
.forEach(bitMapBloomFilter::add);
}
```
##### 使用 `BitMapBloomFilter` 防止缓存穿透
```java
@Override
public Optional<Provinces> detailsWithBoolmFilter(String provinceId) {
if (StrUtil.isBlank(provinceId)) {
return Optional.empty();
}
if (!bitMapBloomFilter.contains(provinceId)) {
// provinceId 不存在
return Optional.empty();
}
return Optional.ofNullable(this.getProvincesByProvinces(provinceId));
}
```
##### 小提示
> 示例 只是单纯的对 `BitMapBloomFilter`进行初数化,我们还需考虑其数据更新.我们可以在 Provinces进行增删的时候手动对 BitMapBloomFilter`进行更新。当然我们也可以通过订阅 诸如`TiDB`的`Ticdc`来实现`BitMapBloomFilter`的数据更新
### 解决方案示例
- 缓存空值
- BloomFilter : 布隆过滤器
![](https://img.mercymodest.com/public/20210326182241.png)
## 缓存雪崩
> ​ 缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到DB上面。结果就是DB 称不住,挂掉。
![](https://img.mercymodest.com/public/20210326182354.png)
### 解决方案示例
- 使用集群缓存,保证缓存服务的高可用
- 对应用服务进行限流
- 缓存数据的时候,在原来过期时间的基础上加上一定范围的随机数
buildscript {
repositories {
mavenLocal()
maven {
url 'https://maven.aliyun.com/repository/gradle-plugin'
}
maven {
url 'https://maven.aliyun.com/repository/spring-plugin'
}
mavenCentral()
}
ext {
set('springBootVersion', '2.4.4')
set('dependencyManagementPlugin', '1.0.11.RELEASE')
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("io.spring.gradle:dependency-management-plugin:${dependencyManagementPlugin}")
}
}
group 'cn.dankal.share'
description = 'dankal-share-cache'
version '1.0.0.RELEASE'
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.springframework.boot'
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
ext {
//hutool
set('hutoolVersion', "5.6.1")
set('hutoolCryptoVersion', "5.6.1")
set('hutoolBoolmFilterVersion', "5.6.1")
// mybatis plus
set('mybatisPlusVersion', "3.4.2")
set('mybatisPlusGenerateVersion', "3.4.1")
set('velocityEngineCore', "2.3")
// druid spring boot
set('druidVersion', "1.1.22")
// fast json
//set('fastjsonVersion', '1.2.75')
// gson
set('gsonVersion','2.8.6')
// hibernate-validator
set('hibernateValidatorVersion', '6.1.7.Final')
// 微信小程序 Java SDK
set('weixinJavaMiniappVersion', '4.0.0')
// javax.faces.api
set('javaxFacesApiVersion', "2.2")
// redisson
set('redissonVersion', "3.15.2")
// elasticsearch version
set("elasticsearchVersion", "7.10.1")
//velocity engine core version
set("velocityEngineCoreVersion", "2.0")
}
repositories {
mavenLocal()
maven {
url 'https://maven.aliyun.com/repository/public/'
}
maven {
url 'https://maven.aliyun.com/repository/spring/'
}
maven {
url 'http://nexus.dankal.cn/repository/tudgo-host-release'
credentials {
username = 'admin'
password = 'wanxe555'
}
}
maven {
url 'http://nexus.dankal.cn/repository/tudgo-host'
credentials {
username = 'admin'
password = 'wanxe555'
}
}
maven {
url 'https://maven.aliyun.com/repository/gradle-plugin'
}
maven {
url 'https://maven.aliyun.com/repository/spring-plugin'
}
mavenCentral()
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
dependencies {
//spring-boot-web
implementation 'org.springframework.boot:spring-boot-starter-web'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
// spring boot redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.apache.commons:commons-pool2'
compile "org.springframework.boot:spring-boot-starter-aop"
compileOnly "javax.faces:javax.faces-api:${javaxFacesApiVersion}"
implementation "org.redisson:redisson:${redissonVersion}"
// spring-boot-test
testImplementation('org.springframework.boot:spring-boot-starter-test')
//hibernate-validator
implementation "org.hibernate.validator:hibernate-validator:${hibernateValidatorVersion}"
//orm
implementation "com.baomidou:mybatis-plus-boot-starter:${mybatisPlusVersion}"
implementation "com.alibaba:druid-spring-boot-starter:${druidVersion}"
implementation 'mysql:mysql-connector-java'
// mybatis plus generate
implementation "com.baomidou:mybatis-plus-generator:${mybatisPlusGenerateVersion}"
implementation "org.apache.velocity:velocity-engine-core:${velocityEngineCore}"
// hutool -core
implementation "cn.hutool:hutool-core:${hutoolVersion}"
// hutool -crypto
implementation "cn.hutool:hutool-crypto:${hutoolCryptoVersion}"
// hutool bloomfilter
implementation "cn.hutool:hutool-bloomFilter:${hutoolBoolmFilterVersion}"
//gson
implementation "com.google.code.gson:gson:${gsonVersion}"
// lombok
compile 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
}
sourceSets {
main {
resources {
//可以将java目录下的所有非.java资源打包到classes下
srcDir 'src/main/java'
}
}
}
test {
useJUnitPlatform()
}
bootJar.enabled = true
jar.enabled = true
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://res.mercymodest.com/gradle-6.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
pluginManagement {
repositories {
mavenLocal()
maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
maven { url "https://maven.aliyun.com/repository/spring-plugin" }
maven { url "https://maven.aliyun.com/repository/public" }
maven { url 'https://maven.aliyun.com/repository/spring' }
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = 'dankal-share-cache'
package cn.dankal.share.cache;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
@EnableCaching
@SpringBootApplication
public class CacheApplication {
/**
* register a {@link Gson} instanced
*
* @return {@link Gson}
* @see GsonBuilder
*/
@Bean
public Gson gson() {
return new GsonBuilder().create();
}
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
package cn.dankal.share.cache.common;
import cn.hutool.core.util.StrUtil;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
public interface Const {
/**
* redis key /redis pattern
*/
interface RedisKey {
/**
* redis key pattern
*
* @see {@link StrUtil#format(CharSequence, Object...)}
*/
String KEY_PATTERN_PROVINCES_DETAIL = "java_cache_provinces_detail_{}";
}
/**
* Cache timeout
*/
interface CacheTimeout {
/**
* timeout: provinces detail
*/
long TIMEOUT_PROVINCES_DETAIL_MINUTES = 5;
/**
* timeout: null data (缓存穿透)
*/
long TIMEOUT_NULL_DATA_MINUTES = 1;
}
}
package cn.dankal.share.cache.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
@Configuration
@MapperScan("cn.dankal.share.cache.mapper")
public class MybatisPlusConfig {
/**
* MybatisPlusInterceptor: 分页
*
* @return {@link MybatisPlusInterceptor}
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
package cn.dankal.share.cache.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.interceptor.KeyGenerator;
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 java.lang.reflect.Method;
import java.time.Duration;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
@Configuration
public class SpringCacheConfig {
/**
* {@link Cacheable} 默认 {@link KeyGenerator}
*
* @return {@link KeyGenerator}
*/
@Bean("customerKeyGenerator")
public KeyGenerator keyGenerator() {
return (Object target, Method method, Object... params) -> {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < params.length; i++) {
stringBuilder.append(params[i]);
}
return stringBuilder.toString();
};
}
/**
* 注册一个 {@link CacheManager}
*
* @param connectionFactory {@link RedisConnectionFactory}
* @return {@link CacheManager}
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager
.builder(connectionFactory)
.cacheDefaults(
RedisCacheConfiguration.defaultCacheConfig()
//缓存时间绝对过期时间20s
.entryTtl(Duration.ofSeconds(120)))
.transactionAware()
.build();
}
}
package cn.dankal.share.cache.controller;
import cn.dankal.share.cache.entity.Provinces;
import cn.dankal.share.cache.service.IProvincesService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
@RestController
@RequestMapping("/test")
public class TestController {
/**
* provincesService
*/
private final IProvincesService provincesService;
public TestController(IProvincesService provincesService) {
this.provincesService = provincesService;
}
@GetMapping("/{provinceId}")
public Provinces getProvincesWithSpringCache(@PathVariable("provinceId") String provinceId) {
return provincesService.detailWithSpringCache(provinceId).orElse(null);
}
@PostMapping
public Integer updateProvinceWithSpringCache(@RequestBody @Validated Provinces provinces) {
return provincesService.updateProvinces(provinces).orElse(0);
}
}
package cn.dankal.share.cache.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* 省份信息表
*
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("tb_provinces")
public class Provinces implements Serializable {
private static final long serialVersionUID = -8658488154638835028L;
/**
* 主键 id
*/
private Integer id;
/**
* 省份 id
*/
@NotBlank(message = "provinceId 不能为空")
private String provinceId;
/**
* 省份 名称
*/
private String provinceName;
}
\ No newline at end of file
package cn.dankal.share.cache.factory;
import cn.hutool.core.util.StrUtil;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* ThreadExecutorFactory
*
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2020-12-23
*/
public class ThreadExecutorFactory {
/**
* 获取 ThreadPoolExecutor
*
* @param corePoolSize 核心线程数
* @param maximumPoolSize 最大线程数
* @param keepAliveTime 超过核心线程数的线程存活时间
* @param timeUnit 超过核心线程数的线程存活时间单位
* @param threadNamePrefix 创建线程的名称前缀
* @param blockQueueCapacity 线程池阻塞队列的容量
* @return ThreadPoolExecutor
* @see ThreadPoolExecutor
*/
public static ThreadPoolExecutor getThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit timeUnit,
String threadNamePrefix,
int blockQueueCapacity) {
// 参数校验
if (corePoolSize <= 0) {
throw new IllegalArgumentException("corePoolSize 必须大于0");
}
if (maximumPoolSize <= 0) {
throw new IllegalArgumentException("maximumPoolSize 必须大于0");
}
if (keepAliveTime < 0) {
throw new IllegalArgumentException("keepAliveTime 必须大于0");
}
if (null == timeUnit) {
throw new IllegalArgumentException("timeUnit 不能为空");
}
if (corePoolSize > maximumPoolSize) {
throw new IllegalArgumentException("corePoolSize 必须小于等于 maximumPoolSize");
}
if (StrUtil.isBlank(threadNamePrefix)) {
throw new IllegalArgumentException("threadNamePrefix 不能为空");
}
if (blockQueueCapacity <= 0) {
throw new IllegalArgumentException("blockQueueCapacity 必须大于0");
}
return new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new ArrayBlockingQueue<>(blockQueueCapacity),
threadFactory(threadNamePrefix)
);
}
/**
* 初始化 ThreadFactory 并为 创建的 线程 命名
*
* @param namePrefix 线程名称前缀
* @return ThreadFactory
*/
private static ThreadFactory threadFactory(String namePrefix) {
return (runnable -> {
Thread thread = new Thread(runnable);
long id = thread.getId();
thread.setName(namePrefix + "-" + id);
return thread;
});
}
/**
* 获取 一个 指定 {@code maximumPoolSize} 的 {@link ThreadPoolExecutor}
* <pre>
* corePoolSize: 1
* keepAliveTime: 300 ms
* blockQueueCapacity: 100
* </pre>
*
* @param namePrefix namePrefix
* @param maximumPoolSize maximumPoolSize
* @return {@link ThreadPoolExecutor}
*/
public static ThreadPoolExecutor getThreadPoolExecutor(String namePrefix, Integer maximumPoolSize) {
return getThreadPoolExecutor(
1,
maximumPoolSize,
300,
TimeUnit.MILLISECONDS,
namePrefix,
100
);
}
}
package cn.dankal.share.cache.mapper.provinces;
import cn.dankal.share.cache.entity.Provinces;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
public interface ProvincesMapper extends BaseMapper<Provinces> {
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.dankal.share.cache.mapper.provinces.ProvincesMapper">
</mapper>
\ No newline at end of file
package cn.dankal.share.cache.service;
import cn.dankal.share.cache.config.SpringCacheConfig;
import cn.dankal.share.cache.entity.Provinces;
import cn.hutool.bloomfilter.BitMapBloomFilter;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.cache.annotation.CacheEvict;
import java.util.Optional;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
public interface IProvincesService extends IService<Provinces> {
/**
* 获取身份详情(无缓存方案)
*
* @param provincesId 身份 id
* @return {@link Optional} contains {@link Provinces}
*/
Optional<Provinces> detailNoCache(String provincesId);
/**
* 获取身份详情(redis 缓存方案)
*
* @param provincesId 身份 id
* @return {@link Optional} contains {@link Provinces}
*/
Optional<Provinces> detailWithRedisCache(String provincesId);
/**
* 更新 {@link Provinces}
* {@link CacheEvict} 删除缓存
*
* @param provinces provinces
* @return {@link Optional} contains {@link Integer}
*/
Optional<Integer> updateProvinces(Provinces provinces);
/**
* 获取身份详情(spring cache 缓存方案)
* 使用 {@link SpringCacheConfig#keyGenerator()} 作为 缓存 key 缓存 方法执行结果
*
* @param provincesId 身份 id
* @return {@link Optional} contains {@link Provinces}
*/
Optional<Provinces> detailWithSpringCache(String provincesId);
/**
* 测试: 使用 {@link BitMapBloomFilter} 防止缓存击穿 (伪代码实现)
*
* @param provinceId provincesId 身份 id
* @return {@link Optional} contains {@link Provinces}
*/
Optional<Provinces> detailsWithBoolmFilter(String provinceId);
}
package cn.dankal.share.cache.service.impl;
import cn.dankal.share.cache.common.Const;
import cn.dankal.share.cache.entity.Provinces;
import cn.dankal.share.cache.mapper.provinces.ProvincesMapper;
import cn.dankal.share.cache.service.IProvincesService;
import cn.hutool.bloomfilter.BitMapBloomFilter;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
@Slf4j
@Service
public class IProvincesServiceImpl extends ServiceImpl<ProvincesMapper, Provinces> implements IProvincesService {
/**
* gson
*/
private final Gson gson;
/**
* stringRedisTemplate
*/
private final StringRedisTemplate stringRedisTemplate;
private BitMapBloomFilter bitMapBloomFilter;
public IProvincesServiceImpl(Gson gson, StringRedisTemplate stringRedisTemplate) {
this.gson = gson;
this.stringRedisTemplate = stringRedisTemplate;
}
@PostConstruct
public void postConstruct() {
bitMapBloomFilter = new BitMapBloomFilter(10);
LambdaQueryWrapper<Provinces> provincesLambdaQueryWrapper = new LambdaQueryWrapper<Provinces>()
.select(Provinces::getProvinceId);
baseMapper.selectList(provincesLambdaQueryWrapper)
.stream()
.filter(Objects::nonNull)
.map(Provinces::getProvinceId)
.forEach(bitMapBloomFilter::add);
}
@Override
public Optional<Provinces> detailNoCache(String provincesId) {
if (StrUtil.isBlank(provincesId)) {
return Optional.empty();
}
Provinces provinces = getProvincesByProvinces(provincesId);
if (log.isDebugEnabled()) {
log.debug("detailNoCache ==> {} get Provinces by {} from database.", Thread.currentThread().getName(), provincesId);
}
return Optional.ofNullable(provinces);
}
/**
* 通过 provincesId 从数据库中查询 {@link Provinces}
*
* @param provincesId provincesId
* @return Provinces
* @see {@link BaseMapper#selectOne(Wrapper)}
*/
private Provinces getProvincesByProvinces(String provincesId) {
if (log.isDebugEnabled()) {
log.debug("getProvincesByProvinces ==> get {} from database.", provincesId);
}
LambdaQueryWrapper<Provinces> provincesLambdaQueryWrapper = new LambdaQueryWrapper<Provinces>()
.eq(Provinces::getProvinceId, provincesId);
return baseMapper.selectOne(provincesLambdaQueryWrapper);
}
@Override
public Optional<Provinces> detailWithRedisCache(String provincesId) {
if (StrUtil.isBlank(provincesId)) {
return Optional.empty();
}
String key = StrUtil.format(Const.RedisKey.KEY_PATTERN_PROVINCES_DETAIL, provincesId);
String cache = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(cache)) {
if (log.isDebugEnabled()) {
log.debug("detailWithRedisCache ==> {} success hit cache {}", Thread.currentThread().getName(), cache);
}
// 缓存命中
return gson.fromJson(cache, new TypeToken<Optional<Provinces>>() {
}.getType());
} else {
// 数据没有缓存
// TODO 缓存击穿 分布式锁 etc. Redisson
Provinces provinces = this.getProvincesByProvinces(provincesId);
Optional<Provinces> provincesOptional;
if (BeanUtil.isNotEmpty(provinces)) {
if (log.isDebugEnabled()) {
log.debug("detailWithRedisCache ==> {} success get provinces by {} from database.", Thread.currentThread().getName(), provincesId);
}
provincesOptional = Optional.of(provinces);
final long timeoutInMinutes = Const.CacheTimeout.TIMEOUT_PROVINCES_DETAIL_MINUTES;
stringRedisTemplate.opsForValue().set(key, gson.toJson(provincesOptional), timeoutInMinutes, TimeUnit.MINUTES);
} else {
// 防止缓存穿透
if (log.isDebugEnabled()) {
log.debug("detailWithRedisCache ==> {} preven cache break down .", Thread.currentThread().getName());
}
final long timeoutInMinutes = Const.CacheTimeout.TIMEOUT_PROVINCES_DETAIL_MINUTES;
provincesOptional = Optional.empty();
stringRedisTemplate.opsForValue()
.set(key, gson.toJson(provincesOptional), timeoutInMinutes, TimeUnit.MINUTES);
}
// 防止缓存雪崩
// 每一个缓存 30秒 以内的随机差距
Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
expire += RandomUtil.randomLong(30);
stringRedisTemplate.expire(key, expire, TimeUnit.SECONDS);
return provincesOptional;
}
}
@CacheEvict(cacheNames = "provinces_detail_cache", key = "#provinces.provinceId")
@Override
public Optional<Integer> updateProvinces(Provinces provinces) {
LambdaQueryWrapper<Provinces> provincesLambdaQueryWrapper = new LambdaQueryWrapper<Provinces>()
.eq(Provinces::getProvinceId, provinces.getProvinceId());
int update = baseMapper.update(provinces, provincesLambdaQueryWrapper);
return Optional.of(update);
}
@Cacheable(cacheNames = "provinces_detail_cache", keyGenerator = "customerKeyGenerator")
@Override
public Optional<Provinces> detailWithSpringCache(String provincesId) {
Provinces provinces = this.getProvincesByProvinces(provincesId);
return Optional.ofNullable(provinces);
}
@Override
public Optional<Provinces> detailsWithBoolmFilter(String provinceId) {
if (StrUtil.isBlank(provinceId)) {
return Optional.empty();
}
if (!bitMapBloomFilter.contains(provinceId)) {
// provinceId 不存在
return Optional.empty();
}
return Optional.ofNullable(this.getProvincesByProvinces(provinceId));
}
}
server:
port: 9102
spring:
application:
name: dankal-share-cache
redis:
host: 127.0.0.1
port: 6379
datasource:
druid:
url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=Asia/Shanghai&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
# Mybatis Plus Common Config
mybatis-plus:
mapper-locations: classpath*:/cn/dankal/share/cache/mapper/**/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
cn.dankal.share: debug
org.springframework.boot.autoconfigure: error
debug: true
\ No newline at end of file
package cn.dankal.share;
import cn.dankal.share.cache.CacheApplication;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
@SpringBootTest(classes = {CacheApplication.class})
public class ApplicationContextProvider {
}
package cn.dankal.share.provicens;
import cn.dankal.share.ApplicationContextProvider;
import cn.dankal.share.cache.entity.Provinces;
import cn.dankal.share.cache.factory.ThreadExecutorFactory;
import cn.dankal.share.cache.service.IProvincesService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author ZGH.MercyModest
* @version V1.0.0
* @create 2021-03-26
*/
public class ProvincesServiceTest extends ApplicationContextProvider {
/**
* provincesService
*/
@Autowired
private IProvincesService provincesService;
/**
* 测试 {@link IProvincesService#page(IPage)}
* <br/>
* 分页插件有效性校验
*/
@Test
@Disabled
public void testProductServicePage() {
final int page = 1;
final int num = 10;
Page<Provinces> pageObj = new Page<>(page, num);
provincesService.page(pageObj)
.getRecords()
.forEach(System.out::println);
}
/**
* 测试: 高并发对于程序的影响 无缓存
*/
@Test
@Disabled
public void testConcurrentEffectiveWithNoCache() throws InterruptedException {
final int maximumPoolSize = 5000;
ThreadPoolExecutor threadPoolExecutor = ThreadExecutorFactory.getThreadPoolExecutor("test-no-cache", maximumPoolSize);
final String provincesId = "440000";
for (int i = 0; i < maximumPoolSize; i++) {
threadPoolExecutor.execute(() -> this.provincesService.detailNoCache(provincesId));
}
// 让程序保持运行
new CountDownLatch(1).await();
}
/**
* 测试: 高并发对于程序的影响 无缓存
*/
@Test
@Disabled
public void testConcurrentEffectiveWithCache() throws InterruptedException {
final int maximumPoolSize = 10000;
ThreadPoolExecutor threadPoolExecutor = ThreadExecutorFactory.getThreadPoolExecutor("test-cache", maximumPoolSize);
final String provincesId = "440000";
for (int i = 0; i < maximumPoolSize; i++) {
threadPoolExecutor.execute(() -> this.provincesService.detailWithRedisCache(provincesId));
}
// 让程序保持运行
new CountDownLatch(1).await();
}
}
{
"dev": {
"host-test": "http://localhost:9102/test"
}
}
\ No newline at end of file
GET {{host-test}}/440000
###
POST {{host-test}}
Content-Type: application/json
{
"id": 19,
"provinceId": "440000",
"provinceName": "广东省2"
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment