开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

帆软报表,今天好好吐槽一下你

发表于 2021-03-17

点上方关注“SQL数据库开发”,

1
php复制代码设为“置顶或星标”,第一时间送达干货

做过可视化报表的大多数人可能都用过或听说过帆软报表,笔者最近这几年转到BI可视化这一块,对帆软有一定的了解。\

先来说说帆软做的好的地方。

帆软报表在国内的占比还是比较可观的,针对国内的一些比较常见的复杂报表做了非常大的创新,基本上只要你会Excel,用帆软做个报表没什么困难。

上面集成了很多可视化控件,一些常见的仪表盘,柱形图,折线图,雷达图,文字云等等都有现成的,直接拖就可以用了。

同时支持在线填报,你如果要做个在线收集信息的表单,分分钟就搞定了。

如果你美术功底不错的话,可以设计出非常精美的报表,自由度相对较大。

最后支持多种数据源,常见的关系数据库支持,非关系型数据库也支持,当然Excel和一些文本文件也不在话下。

都说了这么多优点,是不是感觉都没啥缺点?其实很多时候优点也是它的缺点!

首先就是它的控件了,控件满足一些简单的报表设计是没什么问题,但是如果你想自定义一些好看的控件,没门!

所有的控件都是预设好的,你只能用现有的,当然帆软也有它的市场,但是市场里的那些控件真的好看的没几个,而且还都是另外收费的。

说到收费那就得好好说一下了,帆软是按照模块收费的,一整套软件被分为了6大模块,每个模块都可以单独销售,但是一般买他家的软件都是6个都得买才行。

这种销售策略见多了,先让你用几个必备的,让你觉得买的超值便宜,等哪天你想要其他几个模块,还是得找它买,只是延期付全款而已。

而且吧,这个销售价格非常的不统一,一般像国外的厂商卖软件,很少会有优惠,他家倒好,只要你买,我可以给你打到骨折。

但骨折也不是没有前提的,通常是和实施费用捆绑销售,软件可以少赚点,我再从这实施费用上补回来就是了,好一个连环套。

再来说说它这个报表设计,前面说了如果你美术功底不错,其实这是一个非常高的要求,因为设计报表的大多数都是搞理科的,毕竟得会用SQL取数。

理科生的美学细胞能有多少,不仅要懂报表的排版布局,还要对颜色非常敏感,会灵活搭配各种颜色。

每次做报表之前,都会要求UI设计师先把排版布局颜色这些弄好了才敢开始做报表,因为这对于我来说,真的是太难了。\

就这样一个软件,用的人不少,吐槽它的人也不少。

每次遇到一个bug或者功能实现不了,就会拿它和Tableau或者Power BI作对比,但你不可否认的是在大多数场景下,作为一款国产软件其实还是合格的。

对了,它的报表和BI产品是分开的,报表的功能比较强大,BI产品做的那是真的比较简陋。唯一的亮点就是操作简单,支持联动和钻取。

吐槽归吐槽,吐槽完还是得老老实实的用它来开发设计报表。

今日话题

你有用过帆软吗?遇到过哪些想吐槽的功能?

1
2
3
4
php复制代码
最后给大家分享我写的SQL两件套:《SQL基础知识第二版》和《SQL高级知识第二版》的PDF电子版。里面有各个语法的解释、大量的实例讲解和批注等等,非常通俗易懂,方便大家跟着一起来实操。有需要的读者可以下载学习,在下面的公众号「数据前线」(非本号)后台回复关键字:SQL,就行数据前线
后台回复关键字:1024,获取一份精心整理的技术干货
后台回复关键字:进群,带你进入高手如云的交流群

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot缓存实战 Redis + Caffein

发表于 2021-03-17

公众号:Java小咖秀,网站:javaxks.com

作者 :xiaolyuh,链接: my.oschina.net/xiaolyuh/bl…

问题描述:

通过使用redis和Caffeine来做缓存,我们会发现一些问题。

如果只使用redis来做缓存我们会有大量的请求到redis,但是每次请求的数据都是一样的,假如这一部分数据就放在应用服务器本地,那么就省去了请求redis的网络开销,请求速度就会快很多。但是使用redis横向扩展很方便。
如果只使用Caffeine来做本地缓存,我们的应用服务器的内存是有限,并且单独为了缓存去扩展应用服务器是非常不划算。所以,只使用本地缓存也是有很大局限性的。
至此我们是不是有一个想法了,两个一起用。将热点数据放本地缓存(一级缓存),将非热点数据放redis缓存(二级缓存)。

缓存的选择

一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。Caffeine 缓存详解
二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。

解决思路

Spring 本来就提供了Cache的支持,最核心的就是实现Cache和CacheManager接口。

Cache接口

主要是实现对缓存的操作,如增删查等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vbnet复制代码public interface Cache {
String getName();

Object getNativeCache();

<T> T get(Object key, Class<T> type);

<T> T get(Object key, Callable<T> valueLoader);

void put(Object key, Object value);

ValueWrapper putIfAbsent(Object key, Object value);

void evict(Object key);

void clear();

...
}

CacheManager接口

根据缓存名称来管理Cache,核心方法就是通过缓存名称获取Cache。

1
2
3
4
5
6
7
arduino复制代码public interface CacheManager {

Cache getCache(String name);

Collection<String> getCacheNames();

}

通过上面的两个接口我的大致思路是,写一个LayeringCache来实现Cache接口,LayeringCache类中集成对Caffeine和redis的操作。写一个LayeringCacheManager来管理LayeringCache就行了。

这里的redis缓存使用的是我扩展后的RedisCache详情请看:

  • www.jianshu.com/p/275cb4208…
  • www.jianshu.com/p/e53c1b60c…

LayeringCache

LayeringCache类,因为需要集成对Caffeine和Redis的操作,所以至少需要有name(缓存名称)、CaffeineCache和CustomizedRedisCache三个属性,还增加了一个是否使用一级缓存的开关usedFirstCache。在LayeringCache类的方法里面分别去调用操作一级缓存的和操作二级缓存的方法就可以了。

在这里特别说明一下:

  • 在查询方法如get等,先去查询一级缓存,如果没查到再去查二级缓存。
  • put方法没有顺序要求,但是建议将一级缓存的操作放在前面。
  • 如果是删除方法如evict和clear等,需要先删掉二级缓存的数据,再去删掉一级缓存的数据,否则有并发问题。
  • 删除一级缓存需要用到redis的Pub/Sub(订阅发布)模式,否则集群中其他服服务器节点的一级缓存数据无法删除。
  • redis的Pub/Sub(订阅发布)模式发送消息是无状态的,如果遇到网络等原因有可能导致一些应用服务器上的一级- - 缓存没办法删除,如果对L1和L2数据同步要求较高的话,这里可以使用MQ来做。
    完整代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
typescript复制代码/**
* @author yuhao.wang
*/
public class LayeringCache extends AbstractValueAdaptingCache {
Logger logger = LoggerFactory.getLogger(LayeringCache.class);

/**
* 缓存的名称
*/
private final String name;

/**
* 是否使用一级缓存
*/
private boolean usedFirstCache = true;

/**
* redi缓存
*/
private final CustomizedRedisCache redisCache;

/**
* Caffeine缓存
*/
private final CaffeineCache caffeineCache;

RedisOperations<? extends Object, ? extends Object> redisOperations;

/**
* @param name 缓存名称
* @param prefix 缓存前缀
* @param redisOperations 操作Redis的RedisTemplate
* @param expiration redis缓存过期时间
* @param preloadSecondTime redis缓存自动刷新时间
* @param allowNullValues 是否允许存NULL,默认是false
* @param usedFirstCache 是否使用一级缓存,默认是true
* @param forceRefresh 是否强制刷新(走数据库),默认是false
* @param caffeineCache Caffeine缓存
*/
public LayeringCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations,
long expiration, long preloadSecondTime, boolean allowNullValues, boolean usedFirstCache,
boolean forceRefresh, com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache) {

super(allowNullValues);
this.name = name;
this.usedFirstCache = usedFirstCache;
this.redisOperations = redisOperations;
this.redisCache = new CustomizedRedisCache(name, prefix, redisOperations, expiration, preloadSecondTime, forceRefresh, allowNullValues);
this.caffeineCache = new CaffeineCache(name, caffeineCache, allowNullValues);
}

@Override
public String getName() {
return this.name;
}

@Override
public Object getNativeCache() {
return this;
}

public CustomizedRedisCache getSecondaryCache() {
return this.redisCache;
}

public CaffeineCache getFirstCache() {
return this.caffeineCache;
}

@Override
public ValueWrapper get(Object key) {
ValueWrapper wrapper = null;
if (usedFirstCache) {
// 查询一级缓存
wrapper = caffeineCache.get(key);
logger.debug("查询一级缓存 key:{},返回值是:{}", key, JSON.toJSONString(wrapper));
}

if (wrapper == null) {
// 查询二级缓存
wrapper = redisCache.get(key);
logger.debug("查询二级缓存 key:{},返回值是:{}", key, JSON.toJSONString(wrapper));
}
return wrapper;
}

@Override
public <T> T get(Object key, Class<T> type) {
T value = null;
if (usedFirstCache) {
// 查询一级缓存
value = caffeineCache.get(key, type);
logger.debug("查询一级缓存 key:{},返回值是:{}", key, JSON.toJSONString(value));
}

if (value == null) {
// 查询二级缓存
value = redisCache.get(key, type);
caffeineCache.put(key, value);
logger.debug("查询二级缓存 key:{},返回值是:{}", key, JSON.toJSONString(value));
}
return value;
}

@Override
public <T> T get(Object key, Callable<T> valueLoader) {
T value = null;
if (usedFirstCache) {
// 查询一级缓存,如果一级缓存没有值则调用getForSecondaryCache(k, valueLoader)查询二级缓存
value = (T) caffeineCache.getNativeCache().get(key, k -> getForSecondaryCache(k, valueLoader));
} else {
// 直接查询二级缓存
value = (T) getForSecondaryCache(key, valueLoader);
}

if (value instanceof NullValue) {
return null;
}
return value;
}

@Override
public void put(Object key, Object value) {
if (usedFirstCache) {
caffeineCache.put(key, value);
}
redisCache.put(key, value);
}

@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
if (usedFirstCache) {
caffeineCache.putIfAbsent(key, value);
}
return redisCache.putIfAbsent(key, value);
}

@Override
public void evict(Object key) {
// 删除的时候要先删除二级缓存再删除一级缓存,否则有并发问题
redisCache.evict(key);
if (usedFirstCache) {
// 删除一级缓存需要用到redis的订阅/发布模式,否则集群中其他服服务器节点的一级缓存数据无法删除
Map<String, Object> message = new HashMap<>();
message.put("cacheName", name);
message.put("key", key);
// 创建redis发布者
RedisPublisher redisPublisher = new RedisPublisher(redisOperations, ChannelTopicEnum.REDIS_CACHE_DELETE_TOPIC.getChannelTopic());
// 发布消息
redisPublisher.publisher(message);
}
}

@Override
public void clear() {
redisCache.clear();
if (usedFirstCache) {
// 清除一级缓存需要用到redis的订阅/发布模式,否则集群中其他服服务器节点的一级缓存数据无法删除
Map<String, Object> message = new HashMap<>();
message.put("cacheName", name);
// 创建redis发布者
RedisPublisher redisPublisher = new RedisPublisher(redisOperations, ChannelTopicEnum.REDIS_CACHE_CLEAR_TOPIC.getChannelTopic());
// 发布消息
redisPublisher.publisher(message);
}
}

@Override
protected Object lookup(Object key) {
Object value = null;
if (usedFirstCache) {
value = caffeineCache.get(key);
logger.debug("查询一级缓存 key:{},返回值是:{}", key, JSON.toJSONString(value));
}
if (value == null) {
value = redisCache.get(key);
logger.debug("查询二级缓存 key:{},返回值是:{}", key, JSON.toJSONString(value));
}
return value;
}

/**
* 查询二级缓存
*
* @param key
* @param valueLoader
* @return
*/
private <T> Object getForSecondaryCache(Object key, Callable<T> valueLoader) {
T value = redisCache.get(key, valueLoader);
logger.debug("查询二级缓存 key:{},返回值是:{}", key, JSON.toJSONString(value));
return toStoreValue(value);
}
}

LayeringCacheManager

因为我们需要在CacheManager中来管理缓存,所以我们需要在CacheManager定义一个容器来存储缓存。在这里我们新建一个ConcurrentMap<String, Cache> cacheMap来存在缓存,CacheManager的两个方法getCache和getCacheNames都通过操作这个cacheMap来实现。

Map<String, FirstCacheSetting> firstCacheSettings和Map<String, SecondaryCacheSetting> secondaryCacheSettings属性是针对每一个缓存的特殊配置,如一级缓存的过期时间配置,二级缓存的过期时间和自动刷新时间配置。剩下的属性就不一一介绍了,可直接看下面的源码。

getCache 方法

这可以说是CacheManager最核心的方法,所有CacheManager操作都围绕这个方法进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码@Override
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
cache = createCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}

从这段逻辑我们可以看出这个方法就是根据名称获取缓存,如果没有找到并且动态创建缓存的开关dynamic为true的话,就调用createCache方法动态的创建缓存。

createCache 方法

去创建一个LayeringCache

1
2
3
4
5
6
scss复制代码protected Cache createCache(String name) {

return new LayeringCache(name, (usePrefix ? cachePrefix.prefix(name) : null), redisOperations,
getSecondaryCacheExpirationSecondTime(name), getSecondaryCachePreloadSecondTime(name),
isAllowNullValues(), getUsedFirstCache(name), getForceRefresh(name), createNativeCaffeineCache(name));
}

在创建缓存的时候我们会调用getSecondaryCacheExpirationSecondTime、getSecondaryCachePreloadSecondTime和getForceRefresh等方法去获取二级缓存的过期时间、自动刷新时间和是否强制刷新(走数据库)等值,这些都在secondaryCacheSettings属性中获取;调用createNativeCaffeineCache方法去创建一个一级缓存Caffeine的实例。

createNativeCaffeineCache在这个方法里面会调用getCaffeine方法动态的去读取一级缓存的配置,并根据配置创建一级缓存,如果没有找到特殊配置,就使用默认配置,而这里的特殊配置则在firstCacheSettings属性中获取。

getCaffeine

动态的获取一级缓存配置,并创建对应Caffeine对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码private Caffeine<Object, Object> getCaffeine(String name) {
if (!CollectionUtils.isEmpty(firstCacheSettings)) {
FirstCacheSetting firstCacheSetting = firstCacheSettings.get(name);
if (firstCacheSetting != null && StringUtils.isNotBlank(firstCacheSetting.getCacheSpecification())) {
// 根据缓存名称获取一级缓存配置
return Caffeine.from(CaffeineSpec.parse(firstCacheSetting.getCacheSpecification()));
}
}

return this.cacheBuilder;
}private Caffeine<Object, Object> getCaffeine(String name) {
if (!CollectionUtils.isEmpty(firstCacheSettings)) {
FirstCacheSetting firstCacheSetting = firstCacheSettings.get(name);
if (firstCacheSetting != null && StringUtils.isNotBlank(firstCacheSetting.getCacheSpecification())) {
// 根据缓存名称获取一级缓存配置
return Caffeine.from(CaffeineSpec.parse(firstCacheSetting.getCacheSpecification()));
}
}

return this.cacheBuilder;
}

setFirstCacheSettings和setSecondaryCacheSettings

我们借用了RedisCacheManager的setExpires(Map<String, Long> expires)方法的思想。用setFirstCacheSettings和setSecondaryCacheSettings方法对一级缓存和二级缓存的特殊配置进行设值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码/**
* 根据缓存名称设置一级缓存的有效时间和刷新时间,单位秒
*
* @param firstCacheSettings
*/
public void setFirstCacheSettings(Map<String, FirstCacheSetting> firstCacheSettings) {
this.firstCacheSettings = (!CollectionUtils.isEmpty(firstCacheSettings) ? new ConcurrentHashMap<>(firstCacheSettings) : null);
}

/**
* 根据缓存名称设置二级缓存的有效时间和刷新时间,单位秒
*
* @param secondaryCacheSettings
*/
public void setSecondaryCacheSettings(Map<String, SecondaryCacheSetting> secondaryCacheSettings) {
this.secondaryCacheSettings = (!CollectionUtils.isEmpty(secondaryCacheSettings) ? new ConcurrentHashMap<>(secondaryCacheSettings) : null);
}

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
typescript复制代码/**
* @author yuhao.wang
*/
@SuppressWarnings("rawtypes")
public class LayeringCacheManager implements CacheManager {
// 常量
static final int DEFAULT_EXPIRE_AFTER_WRITE = 60;
static final int DEFAULT_INITIAL_CAPACITY = 5;
static final int DEFAULT_MAXIMUM_SIZE = 1_000;

private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16);


/**
* 一级缓存配置
*/
private Map<String, FirstCacheSetting> firstCacheSettings = null;

/**
* 二级缓存配置
*/
private Map<String, SecondaryCacheSetting> secondaryCacheSettings = null;

/**
* 是否允许动态创建缓存,默认是true
*/
private boolean dynamic = true;

/**
* 缓存值是否允许为NULL
*/
private boolean allowNullValues = false;

// Caffeine 属性
/**
* expireAfterWrite:60
* initialCapacity:5
* maximumSize:1_000
*/
private Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder()
.expireAfterWrite(DEFAULT_EXPIRE_AFTER_WRITE, TimeUnit.SECONDS)
.initialCapacity(DEFAULT_INITIAL_CAPACITY)
.maximumSize(DEFAULT_MAXIMUM_SIZE);

// redis 属性
/**
* 操作redis的RedisTemplate
*/
private final RedisOperations redisOperations;

/**
* 二级缓存使用使用前缀,默认是false,建议设置成true
*/
private boolean usePrefix = false;
private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix();

/**
* redis缓存默认时间,默认是0 永不过期
*/
private long defaultExpiration = 0;

public LayeringCacheManager(RedisOperations redisOperations) {
this(redisOperations, Collections.<String>emptyList());
}

public LayeringCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
this(redisOperations, cacheNames, false);
}

public LayeringCacheManager(RedisOperations redisOperations, Collection<String> cacheNames, boolean allowNullValues) {
this.allowNullValues = allowNullValues;
this.redisOperations = redisOperations;

setCacheNames(cacheNames);
}

@Override
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
cache = createCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}

@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}

@SuppressWarnings("unchecked")
protected Cache createCache(String name) {
return new LayeringCache(name, (usePrefix ? cachePrefix.prefix(name) : null), redisOperations,
getSecondaryCacheExpirationSecondTime(name), getSecondaryCachePreloadSecondTime(name),
isAllowNullValues(), getUsedFirstCache(name), getForceRefresh(name), createNativeCaffeineCache(name));
}

/**
* Create a native Caffeine Cache instance for the specified cache name.
*
* @param name the name of the cache
* @return the native Caffeine Cache instance
*/
protected com.github.benmanes.caffeine.cache.Cache<Object, Object> createNativeCaffeineCache(String name) {
return getCaffeine(name).build();
}

/**
* 使用该CacheManager的当前状态重新创建已知的缓存。
*/
private void refreshKnownCaches() {
for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
entry.setValue(createCache(entry.getKey()));
}
}

/**
* 在初始化CacheManager的时候初始化一组缓存。
* 使用这个方法会在CacheManager初始化的时候就会将一组缓存初始化好,并且在运行时不会再去创建更多的缓存。
* 使用空的Collection或者重新在配置里面指定dynamic后,就可重新在运行时动态的来创建缓存。
*
* @param cacheNames
*/
public void setCacheNames(Collection<String> cacheNames) {
if (cacheNames != null) {
for (String name : cacheNames) {
this.cacheMap.put(name, createCache(name));
}
this.dynamic = cacheNames.isEmpty();
}
}

/**
* 设置是否允许Cache的值为null
*
* @param allowNullValues
*/
public void setAllowNullValues(boolean allowNullValues) {
if (this.allowNullValues != allowNullValues) {
this.allowNullValues = allowNullValues;
refreshKnownCaches();
}
}

/**
* 获取是否允许Cache的值为null
*
* @return
*/
public boolean isAllowNullValues() {
return this.allowNullValues;
}

/**
* 在生成key的时候是否是否使用缓存名称来作为缓存前缀。默认是false,但是建议设置成true。
*
* @param usePrefix
*/
public void setUsePrefix(boolean usePrefix) {
this.usePrefix = usePrefix;
}

protected boolean isUsePrefix() {
return usePrefix;
}

/**
* 设置redis默认的过期时间(单位:秒)
*
* @param defaultExpireTime
*/
public void setSecondaryCacheDefaultExpiration(long defaultExpireTime) {
this.defaultExpiration = defaultExpireTime;
}


/**
* 根据缓存名称设置一级缓存的有效时间和刷新时间,单位秒
*
* @param firstCacheSettings
*/
public void setFirstCacheSettings(Map<String, FirstCacheSetting> firstCacheSettings) {
this.firstCacheSettings = (!CollectionUtils.isEmpty(firstCacheSettings) ? new ConcurrentHashMap<>(firstCacheSettings) : null);
}

/**
* 根据缓存名称设置二级缓存的有效时间和刷新时间,单位秒
*
* @param secondaryCacheSettings
*/
public void setSecondaryCacheSettings(Map<String, SecondaryCacheSetting> secondaryCacheSettings) {
this.secondaryCacheSettings = (!CollectionUtils.isEmpty(secondaryCacheSettings) ? new ConcurrentHashMap<>(secondaryCacheSettings) : null);
}


/**
* 获取过期时间
*
* @return
*/
public long getSecondaryCacheExpirationSecondTime(String name) {
if (StringUtils.isEmpty(name)) {
return 0;
}

SecondaryCacheSetting secondaryCacheSetting = null;
if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
secondaryCacheSetting = secondaryCacheSettings.get(name);
}
Long expiration = secondaryCacheSetting != null ? secondaryCacheSetting.getExpirationSecondTime() : defaultExpiration;
return expiration < 0 ? 0 : expiration;
}

/**
* 获取自动刷新时间
*
* @return
*/
private long getSecondaryCachePreloadSecondTime(String name) {
// 自动刷新时间,默认是0
SecondaryCacheSetting secondaryCacheSetting = null;
if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
secondaryCacheSetting = secondaryCacheSettings.get(name);
}
Long preloadSecondTime = secondaryCacheSetting != null ? secondaryCacheSetting.getPreloadSecondTime() : 0;
return preloadSecondTime < 0 ? 0 : preloadSecondTime;
}

/**
* 获取是否使用二级缓存,默认是true
*/
public boolean getUsedFirstCache(String name) {
SecondaryCacheSetting secondaryCacheSetting = null;
if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
secondaryCacheSetting = secondaryCacheSettings.get(name);
}

return secondaryCacheSetting != null ? secondaryCacheSetting.getUsedFirstCache() : true;
}

/**
* 获取是否强制刷新(走数据库),默认是false
*/
public boolean getForceRefresh(String name) {
SecondaryCacheSetting secondaryCacheSetting = null;
if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
secondaryCacheSetting = secondaryCacheSettings.get(name);
}

return secondaryCacheSetting != null ? secondaryCacheSetting.getForceRefresh() : false;
}

public void setCaffeineSpec(CaffeineSpec caffeineSpec) {
Caffeine<Object, Object> cacheBuilder = Caffeine.from(caffeineSpec);
if (!ObjectUtils.nullSafeEquals(this.cacheBuilder, cacheBuilder)) {
this.cacheBuilder = cacheBuilder;
refreshKnownCaches();
}
}

private Caffeine<Object, Object> getCaffeine(String name) {
if (!CollectionUtils.isEmpty(firstCacheSettings)) {
FirstCacheSetting firstCacheSetting = firstCacheSettings.get(name);
if (firstCacheSetting != null && StringUtils.isNotBlank(firstCacheSetting.getCacheSpecification())) {
// 根据缓存名称获取一级缓存配置
return Caffeine.from(CaffeineSpec.parse(firstCacheSetting.getCacheSpecification()));
}
}

return this.cacheBuilder;
}
}

FirstCacheSettings

一级缓存配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码public class FirstCacheSetting {

/**
* 一级缓存配置,配置项请点击这里 {@link CaffeineSpec#configure(String, String)}
* @param cacheSpecification
*/
public FirstCacheSetting(String cacheSpecification) {
this.cacheSpecification = cacheSpecification;
}

private String cacheSpecification;

public String getCacheSpecification() {
return cacheSpecification;
}
}

SecondaryCacheSetting

二级缓存的特殊配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
java复制代码
/**
* @author yuhao.wang
*/
public class SecondaryCacheSetting {

/**
* @param expirationSecondTime 设置redis缓存的有效时间,单位秒
* @param preloadSecondTime 设置redis缓存的自动刷新时间,单位秒
*/
public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime) {
this.expirationSecondTime = expirationSecondTime;
this.preloadSecondTime = preloadSecondTime;
}

/**
* @param usedFirstCache 是否启用一级缓存,默认true
* @param expirationSecondTime 设置redis缓存的有效时间,单位秒
* @param preloadSecondTime 设置redis缓存的自动刷新时间,单位秒
*/
public SecondaryCacheSetting(boolean usedFirstCache, long expirationSecondTime, long preloadSecondTime) {
this.expirationSecondTime = expirationSecondTime;
this.preloadSecondTime = preloadSecondTime;
this.usedFirstCache = usedFirstCache;
}

/**
* @param expirationSecondTime 设置redis缓存的有效时间,单位秒
* @param preloadSecondTime 设置redis缓存的自动刷新时间,单位秒
* @param forceRefresh 是否使用强制刷新(走数据库),默认false
*/
public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime, boolean forceRefresh) {
this.expirationSecondTime = expirationSecondTime;
this.preloadSecondTime = preloadSecondTime;
this.forceRefresh = forceRefresh;
}

/**
* @param expirationSecondTime 设置redis缓存的有效时间,单位秒
* @param preloadSecondTime 设置redis缓存的自动刷新时间,单位秒
* @param usedFirstCache 是否启用一级缓存,默认true
* @param forceRefresh 是否使用强制刷新(走数据库),默认false
*/
public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime, boolean usedFirstCache, boolean forceRefresh) {
this.expirationSecondTime = expirationSecondTime;
this.preloadSecondTime = preloadSecondTime;
this.usedFirstCache = usedFirstCache;
this.forceRefresh = forceRefresh;
}

/**
* 缓存有效时间
*/
private long expirationSecondTime;

/**
* 缓存主动在失效前强制刷新缓存的时间
* 单位:秒
*/
private long preloadSecondTime = 0;

/**
* 是否使用二级缓存,默认是true
*/
private boolean usedFirstCache = true;

/**
* 是否使用强刷新(走数据库),默认是false
*/
private boolean forceRefresh = false;

public long getPreloadSecondTime() {
return preloadSecondTime;
}

public long getExpirationSecondTime() {
return expirationSecondTime;
}

public boolean getUsedFirstCache() {
return usedFirstCache;
}

public boolean getForceRefresh() {
return forceRefresh;
}
}

使用方式

在上面我们定义好了LayeringCacheManager和LayeringCache接下来就是使用了。

新建一个配置类CacheConfig,在这里指定一个LayeringCacheManager的Bean。我那的缓存就生效了。完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
typescript复制代码/**
* @author yuhao.wang
*/
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class CacheConfig {

// redis缓存的有效时间单位是秒
@Value("${redis.default.expiration:3600}")
private long redisDefaultExpiration;

// 查询缓存有效时间
@Value("${select.cache.timeout:1800}")
private long selectCacheTimeout;
// 查询缓存自动刷新时间
@Value("${select.cache.refresh:1790}")
private long selectCacheRefresh;

@Autowired
private CacheProperties cacheProperties;

@Bean
@Primary
public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
LayeringCacheManager layeringCacheManager = new LayeringCacheManager(redisTemplate);
// Caffeine缓存设置
setFirstCacheConfig(layeringCacheManager);

// redis缓存设置
setSecondaryCacheConfig(layeringCacheManager);
return layeringCacheManager;
}

private void setFirstCacheConfig(LayeringCacheManager layeringCacheManager) {
// 设置默认的一级缓存配置
String specification = this.cacheProperties.getCaffeine().getSpec();
if (StringUtils.hasText(specification)) {
layeringCacheManager.setCaffeineSpec(CaffeineSpec.parse(specification));
}

// 设置每个一级缓存的过期时间和自动刷新时间
Map<String, FirstCacheSetting> firstCacheSettings = new HashMap<>();
firstCacheSettings.put("people", new FirstCacheSetting("initialCapacity=5,maximumSize=500,expireAfterWrite=10s"));
firstCacheSettings.put("people1", new FirstCacheSetting("initialCapacity=5,maximumSize=50,expireAfterAccess=10s"));
layeringCacheManager.setFirstCacheSettings(firstCacheSettings);
}

private void setSecondaryCacheConfig(LayeringCacheManager layeringCacheManager) {
// 设置使用缓存名称(value属性)作为redis缓存前缀
layeringCacheManager.setUsePrefix(true);
//这里可以设置一个默认的过期时间 单位是秒
layeringCacheManager.setSecondaryCacheDefaultExpiration(redisDefaultExpiration);

// 设置每个二级缓存的过期时间和自动刷新时间
Map<String, SecondaryCacheSetting> secondaryCacheSettings = new HashMap<>();
secondaryCacheSettings.put("people", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh));
secondaryCacheSettings.put("people1", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh, true));
secondaryCacheSettings.put("people2", new SecondaryCacheSetting(false, selectCacheTimeout, selectCacheRefresh));
secondaryCacheSettings.put("people3", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh, false, true));
layeringCacheManager.setSecondaryCacheSettings(secondaryCacheSettings);
}

/**
* 显示声明缓存key生成器
*
* @return
*/
@Bean
public KeyGenerator keyGenerator() {

return new SimpleKeyGenerator();
}

}

在cacheManager中指定Bean的时候,我们通过调用LayeringCacheManager 的setFirstCacheSettings和setSecondaryCacheSettings方法为缓存设置一级缓存和二级缓存的特殊配置。

剩下的就是在Service方法上加注解了,如:

1
2
3
4
5
6
7
less复制代码@Override
@Cacheable(value = "people1", key = "#person.id", sync = true)//3
public Person findOne1(Person person, String a, String[] b, List<Long> c) {
Person p = personRepository.findOne(person.getId());
logger.info("为id、key为:" + p.getId() + "数据做了缓存");
return p;
}

@Cacheable的sync属性建议设置成true。

测试

最后通过jmeter测试,50个线程,使用多级缓存,比只使用redis级缓存性能提升2倍多,只是用redis吞吐量在1243左右,使用多级缓存后在2639左右。

源码

github.com/wyh-spring-…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springboot二手交易市场实现

发表于 2021-03-17

项目简介

后端框架:SSM

技术栈:Java、Springboot、Mybatis

数据库:Mysql

源码 :完整源码

项目介绍:这是一套基于springboot+Mybatis实现的二手市场交易系统,模块包括:

个人中心->订单中心、关注列表、发布物品、我的闲置

通用模块->登录、注册

主页->二手市场中心、商品详情、最新发布、闲置数码、校园代步、电器日用、图书教材、美妆衣物、运动棋牌、票券小物

使用方法:本项目代码简洁,容易上手,二次开发更简单,系统不复杂,可用于平时设计以及小型个人用途。

项目截图

msyy_aJaT5SpC1BcObo3AXIUwKgRMK.png

msyy_JzILl6diCgoqoQXndDNOmMUzF.png

msyy_ikgWWC8PQWADZNpcnZ4dft3fr.png

msyy_kI8DlEVgFhakujVXZs1HCHPYe.png

msyy_kp6s77fo8J06D3qzDGrklVuEv.png

msyy_Pzq48OMZN69rX6JhFtlqP2wlN.png

msyy_Xywm51KjbbOMk53iwY3iEFp6z.png

msyy_QDcnunhoxX1k5yojMUhwNRwHN.png

msyy_q2tFIISM2lkj6qYo6y64XKqwu.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于SSM的教务查询系统

发表于 2021-03-17

项目简介

后端框架:SSM

技术栈:Java、SpringMvc、Mybatis

数据库:Mysql

源码 :完整源码

项目介绍:这是一套基于SSM框架的教务查询系统,mysql作为底层数据库,使用Mybatis作为持久化数据库框架。

前端采用bootstrap+jquery渲染页面:
使用技术 IOC容器:Spring

Web框架:SpringMVC ORM框架:Mybatis 数据源:C3P0 日志:log4j

前端框架:Bootstrap 运行环境 jdk8+tomcat8+mysql+Eclipse+maven

项目技术: spring+spring mvc+mybatis+bootstrap+jquery

使用方法:本项目代码简洁,容易上手,二次开发更简单,系统不复杂,可用于平时设计以及小型个人用途。

项目截图

1.png

1.png

2.png

3.png

5.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我所理解的 Redis AOF 持久化

发表于 2021-03-16

介绍

Redis 是内存型数据库,一旦主进程退出就会造成数据丢失。它的持久化主要有两大机制,即 AOF 日志和 RDB 快照,本文主要关心 AOF 日志。

过程

Redis 执行完一个写命令后,将写命令以协议文本的形式追加到 AOF 缓冲区末尾,再通过同步策略来决定是否将 AOF 缓冲区中的内容写入 & 同步到 AOF 日志。

同步策略

AOF 机制提供了三个选择,也就是配置项 appendfsync 的三个可选值:

  • always:将 AOF 缓冲区中的所有内容写入并同步到 AOF 日志。
  • everysec:将 AOF 缓冲区中的所有内容写入到 AOF 日志,如果上次同步 AOF 日志的时间距离现在超过 1 秒钟,那么对 AOF 日志进行同步。
  • no:将 AOF 缓冲区中的所有内容写入到 AOF 日志,但并不对 AOF 日志进行同步, 何时同步由操作系统来决定。

三种策略的优缺点也显而易见了,always 可靠性高性能低、no 可靠性低性能高,everysec 取两者折中。

重写机制

AOF 日志是以文件的形式记录接收到的所有写命令。随着接收的写命令越来越多,文件体积会越来越大。这也就意味着会带来一些问题。

  • 操作系统对文件体积有限制,不能保存过大的文件。
  • 对体积比较大的文件进行追加写入,效率会降低。
  • AOF 日志体积过大,故障恢复过程缓慢。

为了解决上面的问题,Redis 提供了重写功能。通过该功能,Redis 可以创建一份新的 AOF 日志来代替现有的 AOF 日志。

  • 两份 AOF 日志所保存的数据库状态相同。
  • 新 AOF 日志不会包含浪费空间的冗余命令。
  • 新 AOF 日志体积小于等于原 AOF 日志。

重写过程

为什么要放在子进程里执行?

不会造成主线程阻塞,子进程进行 AOF 重写期间,主线程可以继续处理命令请求。

重写过程中有写命令,会造成数据不一致吗?

不会,Redis 维护了 AOF 重写缓冲区,在主线程创建重写子进程后开始使用。如果重写期间有写命令请求,Redis 会追加写入 AOF 缓冲区和 AOF重写缓冲区。当子进程完成创建新 AOF 日志后,主线程会将 AOF 重写缓冲区中的所有内容追加写入新 AOF 日志,此时新旧两份 AOF日志所保存的数据库状态完全一致。最后,用新 AOF 日志覆盖旧 AOF 日志,完成 AOF 日志重写操作。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

分享一个开源项目,Torna——企业接口文档解决方案 关于T

发表于 2021-03-16

关于Torna

企业接口文档解决方案,目标是让文档管理变得更加方便、快捷。Torna采用团队协作的方式管理和维护项目API文档,将不同形式的文档纳入进来,形成一个统一的维护方式。

项目地址

Torna

特性介绍

  • 支持接口文档增删改查
  • 支持导入外部接口(支持导入swagger、postman)
  • 支持OpenAPI管理接口
  • 支持字典管理
  • 支持导出为markdown格式、html格式
  • 支持多环境接口调试
  • 支持文档权限管理,访客、开发者、管理员对应不同权限
  • 提供管理模式和浏览模式双模式,管理模式用来编辑文档内容,浏览模式纯粹查阅文档,界面无其它元素干扰
  • 部署简单,直接运行脚本启动程序
  • 支持docker运行

界面预览

管理接口
管理接口

编辑接口
编辑接口

浏览接口
浏览接口

调试接口
调试接口

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

这个Bug的排查之路,真的太有趣了。|项目复盘 为啥停不下来

发表于 2021-03-16

这是why哥的第 92 篇原创文章


在《深入理解Java虚拟机》一书中有这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码public class VolatileTest {

    public static volatile int race = 0;

    public static void increase() {
        race++;
    }

    private static final int THREADS_COUNT=20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0; i < THREADS_COUNT; i++){
           new Thread(new Runnable() {
               @Override
               public void run() {
                   for (int i = 0; i < 10000; i++) {
                       increase();
                   }
               }
           }).start();
        }

        //等待所有累加线程都结束
        while(Thread.activeCount()>1)
            Thread.yield();

        System.out.println(race);
    }
}

你看到这段代码的第一反应是什么?

是不是关注点都在 volatile 关键字上。

甚至马上就要开始脱口而出:volatile 只保证可见性,不保证原子性。而代码中的 race++ 不是原子性的操作,巴拉巴拉巴拉…

反正我就是这样的:


当他把代码发给我,我在 idea 里面一粘贴,然后把 main 方法运行起来后,神奇的事情出现了。

这个代码真的没有执行到输出语句,也没有任何报错。

看起来就像是死循环了一样。

不信的话,你也可以放到你的 idea 里面去执行一下。

等等……

死循环?

代码里面不是就有一个死循环吗?

1
2
3
复制代码//等待所有累加线程都结束
while(Thread.activeCount()>1)
    Thread.yield();

这段代码能有什么小心思呢?看起来人畜无害啊。

但是程序员的直觉告诉我,这个地方就是有问题的。

活跃线程一直是大于 1 的,所以导致 while 一直在死循环。

算了,不想了,先 Debug 看一眼吧。

Debug 了两遍之后,我才发现,这个事情,有点意思了。

因为 Debug 的情况下,程序竟然正常结束了。


啥情况啊?

分析一波走起。

为啥停不下来?

我是怎么分析这个问题的呢。

我就把程序又 Run 了起来,控制台还是啥输出都没有。

我就盯着这个控制台想啊,会是啥原因呢?

这样干看着也不是办法啊。

反正我现在就是咬死这个 while 循环是有问题的,所以为了排除其他的干扰项。

我把程序简化到了这个样子:

1
2
3
4
5
6
7
8
9
10
复制代码public class VolatileTest {

    public static volatile int race = 0;

    public static void main(String[] args) {
        while(Thread.activeCount()>1)
            Thread.yield();
        System.out.println("race = " + race);
    }
}

运行起来之后,还是没有执行到输出语句,也就侧面证实了我的想法:while 循环有问题。

而 while 循环的条件就是 Thread.activeCount()>1

朝着这个方向继续想下去,就是看看当前活跃线程到底有几个。

于是程序又可以简化成这样:


直接运行看到输出结果是 2。


用 Debug 模式运行时返回的是 1。

对比这运行结果,我心里基本上就有数了。

先看一下这个 activeCount 方法是干啥的:


注意看画着下划线的地方:

返回的值是一个 estimate。

estimate 是啥?


你看,又在我这里学一个高级词汇。真是 very good。

返回的是一个预估值。

为什么呢?

因为我们调用这个方法的一刻获取到值之后,线程数还是在动态变化的。

也就是说返回的值只代表你调用的那一刻有几个活跃线程,也许当你调用完成后,有一个线程就立马嗝屁了。

所以,这个值是个预估值。

这一瞬间,我突然想到了量子力学中的测不准原理。


你不可能同时知道一个粒子的位置和它的速度,就像在多线程高并发的情况下你不可能同时知道调用 activeCount 方法得到的值和你要用这个值的时刻,这个值的真实值是多少。

你看,刚学完英语又学量子力学。


好了,回到程序里面。

虽然注释里面说了返回值是 estimate 的,但是在我们的程序中,并不存在这样的问题。

看到 activeCount 方法的实现之后:

1
2
3
复制代码public static int activeCount() {
    return currentThread().getThreadGroup().activeCount();
}

我又想到,既然在直接 Run 的情况下,程序返回的数是 2,那我看看到底有那些线程呢?

其实最开始我想着去 Debug 一下的,但是 Debug 的情况下,返回的数是 1。我意识到,这个问题肯定和 idea 有关,而且必须得用日志调试大法才能知道原因。

于是,我把程序改成了这样:


直接 Run 起来,可以看到,确实有两个线程。

一个是 main 线程,我们熟悉。

一个是 Monitor Ctrl-Break 线程,我不认识。

但是当我用 Debug 的方式运行的时候,有意思的事情就发生了:


Monitor Ctrl-Break 线程不见了!?

于是,我问他:


是啊,问题解决了,但是啥原因啊?

为什么 Run 不可以运行,而 Debug 可以运行呢?


当前线程有哪些?
========

我们先梳理一下当前线程有哪些吧。

可以使用下面的代码获取当前所有的线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码public  static Thread[] findAllThread(){
    ThreadGroup currentGroup =Thread.currentThread().getThreadGroup();

    while (currentGroup.getParent()!=null){
        // 返回此线程组的父线程组
        currentGroup=currentGroup.getParent();
    }
    //此线程组中活动线程的估计数
    int noThreads = currentGroup.activeCount();

    Thread[] lstThreads = new Thread[noThreads];
    //把对此线程组中的所有活动子组的引用复制到指定数组中。
    currentGroup.enumerate(lstThreads);

    for (Thread thread : lstThreads) {
        System.out.println("线程数量:"+noThreads+" " +
                "线程id:" + thread.getId() + 
                " 线程名称:" + thread.getName() + 
                " 线程状态:" + thread.getState());
    }
    return lstThreads;
}

运行之后可以看到有 6 个线程:


也就是说,在 idea 里面,一个 main 方法 Run 起来之后,即使什么都不干,也会有 6 个线程运行。

这 6 个线程分别是干啥的呢?

我们一个个的说。

Reference Handler 线程:

JVM 在创建 main 线程后就创建 Reference Handler 线程,其优先级最高,为 10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。

Finalizer 线程:

这个线程也是在 main 线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的 finalize() 方法。
关于 Finalizer 线程的几点:
1)只有当开始一轮垃圾收集时,才会开始调用 finalize() 方法;因此并不是所有对象的 finalize() 方法都会被执行;
2)该线程也是 daemon 线程,因此如果虚拟机中没有其他非 daemon 线程,不管该线程有没有执行完 finalize() 方法,JVM 也会退出;
3) JVM在垃圾收集时会将失去引用的对象包装成 Finalizer 对象(Reference的实现),并放入 ReferenceQueue,由 Finalizer 线程来处理;最后将该 Finalizer 对象的引用置为 null,由垃圾收集器来回收;
4) JVM 为什么要单独用一个线程来执行 finalize() 方法呢?如果 JVM 的垃圾收集线程自己来做,很有可能由于在 finalize() 方法中误操作导致 GC 线程停止或不可控,这对 GC 线程来说是一种灾难。

Attach Listener 线程:

Attach Listener 线程是负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者。通常我们会用一些命令去要求 jvm 给我们一些反馈信息。
如:java -version、jmap、jstack 等等。如果该线程在 jvm 启动的时候没有初始化,那么,则会在用户第一次执行 jvm 命令时,得到启动。

Signal Dispatcher 线程:

前面我们提到第一个 Attach Listener 线程的职责是接收外部 jvm 命令,当命令接收成功后,会交给 signal dispather 线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather 线程也是在第一次接收外部 jvm 命令时,进行初始化工作。

main 线程:

呃,这个不说了吧。大家都知道。

Monitor Ctrl-Break 线程:

先买个关子,下一小节专门聊聊这个线程。

上面线程的作用,我是从这个网页搬运过来的,还有很多其他的线程,大家可以去看看:

http://ifeve.com/jvm-thread/

我好事做到底,直接给你来个长截图,一网打尽。

你先把图片保存起来,后面慢慢看:


现在跟着我去探寻 Monitor Ctrl-Break 线程的秘密。

继续挖掘

问题解决了,但是问题背后的问题,还没有得到解决:

Monitor Ctrl-Break 线程是啥?它是怎么来的?

我们先 jstack 一把看看线程堆栈呗。

而在 idea 里面,这里的“照相机”图标,就是 jstack 一样的功能。


我把程序恢复为最初的样子,然后把“照相机”就这么轻轻的一点:


从线程堆栈里面可以看到 Monitor Ctrl-Break 线程来自于这个地方:

com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

而这个地方,一看名称,是 idea 的源码了啊?

不属于我们的项目里面了,这咋个搞呢?

思考了一下,想到了一种可能,于是我决定用 jps 命令验证一下:


看到执行结果的时候我笑了,一切就说的通了。

果然,是用了 -javaagent 啊。

那么 javaagent 是什么?

好的,要问答好这个问题,就得另起一篇文章了,本文不讨论,先欠着。

只是简单的提一下。

你在命令行执行 java 命令,会输出一大串东西,其中就包含这个:


什么语言代理的,看不懂。

叫我们参阅 java.lang.instrument。

那它又是拿来干啥的?

简单的一句话解释就是:

使用 instrument 可以更加方便的使用字节码增强的技术,可以认为是一种 jvm 层面的截面。不需要对程序源代码进行任何侵入,就可以对其进行增强或者修改。总之,有点 AOP 内味。

而 -javaagent 命令后面需要紧跟一个 jar 包。

-javaagent:<jar 路径>[=<选项>]

instrument 机制要求,这个 jar 包必须有 MANIFEST.MF 文件,而 MANIFEST.MF 文件里面必须有 Premain-Class 这个东西。

所以,回到我们的程序中,看一下 javaagent 后面跟的包是什么。

在哪看呢?

就这个地方:


你把它点开,命令非常的长。但是我们关心的 -javaagent 就在最开始的地方:


-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.4\lib\idea_rt.jar=61960

可以看到,后面跟着的 jar 包是 idea_rt,按照文件目录找过去,也就是在这里:


我们解压这个 jar 包,打开它的 MANIFEST.MF 文件:


而这个类,不就是我们要找的它吗:


此时此刻,我们距离真相,只有一步之遥了。

进到对应的包里,发现有三个 class 类:


主要关注 AppMainV2.class 文件:


在这个文件里面,就有一个 startMonitor 方法:


我说过什么来着?

来,大声的跟我念一遍:源码之下无秘密。

Monitor Ctrl-Break 线程就是这里来的。

而仔细看一眼这里的代码,这个线程在干啥事呢?

Socket client = new Socket("127.0.0.1", portNumber);

啊,我的天呐,来看看这个可爱的小东西,socket 编程,太熟悉了,简直是梦回大学实验课的时候。

它是链接到 127.0.0.1 的某个端口上,然后 while(true) 死循环等待接收命令。

那么这个端口是哪个端口呢?

就是这里的 62325:


需要注意的是,这个端口并不是固定的,每次启动这个端口都会变化。

玩玩它

既然它是 Socket 编程,那么我就玩玩它呗。

先搞个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public class SocketTest{

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(12345);
        System.out.println("等待客户端连接.");
        Socket socket = serverSocket.accept();
        System.out.println("有客户端连接上了 "+ socket.getInetAddress() + ":" + socket.getPort() +"");
 
        OutputStream outputStream = socket.getOutputStream();
        Scanner scanner = new Scanner(System.in);
        while (true)
        {
            System.out.println("请输入指令: ");
            String s = scanner.nextLine();
            String message = s + "\n";
            outputStream.write(message.getBytes("US-ASCII"));
        }
    }
}

我们把服务端的端口指定为了 12345。

客户端这边的端口也得指定为 12345,那怎么指定呢?

别想复杂了,简单的一比。

把这行日志粘贴出来:


需要说明的是,我这边为了演示效果,在程序里面加了一个 for 循环。

然后我们在这里把端口改为 12345:


把文件保存为 start.bat 文件,随便放一个地方。

万事俱备。

我们先把服务端运行起来:


然后,执行 bat 文件:


在 cmd 窗口里面输出了我们的日志,说明程序正常运行。

而在服务端这边,显示有客户端连接成功。

叫我们输入指令。

输入啥指令呢?

看一下客户端支持哪些指令呗:


可以看到,支持 STOP 命令。

接受到该命令后,会退出程序。

来,搞一波,动图走起:


搞定。

好了,本文技术部分就到这里了,恭喜你知道了 idea 中的 Monitor Ctrl-Break 线程,这个学了没啥卵用的知识 。

如果要深挖的话,往 -javaagent 方向挖一挖。

应用很多的,比如耳熟能详的 Java 诊断工具 Arthas 就是基于 JavaAgent 做的。

有点意思。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文搞懂Mysql索引全部原理

发表于 2021-03-16

一文搞懂Mysql索引全部原理

本文主要详细介绍Mysql索引基本原理,相信读了这篇文章,对于索引不止停留在课堂老师讲的图书馆图书索引,会更加深入理解索引的本质。这样对于怎么优化Mysql,就不止停留在背那几条规则,而是更理解规则之后的原理。

一、重新定义索引

1、索引的定义

索引:实际是一种数据结构。帮助mysql高效获取数据,为什么高效,因为创建索引的数据是按照一定的数据结构排序进行存储的。

索引的数据结构:

数据结构 优缺点
二叉排序树 对于单增的数据,和查全表是一样的复杂度。(例如:1,2,3,4)
红黑树 实际就是二叉树,大数插叶子的右端,小数插叶子的左端,当数据变多时,毕竟是二叉树,就会出现level太高,查询速度慢的问题
Hash表 1、对索引的key进行一次hash计算就可以定位数据存储的位置 。2、很多时候Hash索引要比B+树索引更加高效。3、只能满足“=”,“IN” ,不支持范围查找,因为没有排序
B-tree树:bulb: 实际使用的是B+树,解决以上几种数据结构的问题。相比上面的二叉树,这个树是N叉树,存储更多的索引,且查询更高效。也可顺序查询。

1) 二叉排序树

image.png

2)Hash表
image.png

2、B树、B+树

B树:

节点存放创建index的数据(这里假设为ID),这里存储的时候是K-V存储。K指的是ID,V指的是ID对应行所在磁盘文件的地址。

image.png

B树的特点是(对于Mysql):

  • 叶子节点都有相同的深度,也叶节点的指针为空
  • 所有的索引元素不重复
  • 节点的数据索引从左到右递增排列

B+树:

非叶子节点只存储index的数据,作为冗余节点,来进行对叶子节点进行范围划分。而索引真正存储在叶子节点上,并且也是K-V存储,V存储的是对应行所在磁盘文件的地址。

image.png

B+树的特点(对于mysql):

  • 实际是B树的变种
  • 非叶子节点不存储data,只存储索引,这样可以放更多的索引
  • 叶子节点包含所以索引字段
  • 叶子节点使用双指针连接,可以访问自己双向节点,提高区间访问的性能

现在你肯定还有一个疑问就是,为什么不用B树,要使用B+树

答案就是:

  • 因为为了省出空间存储索引。每一页存储的数据越多,其树的高度就低,查找就会更快。
  • 对于范围查找来说,B+树只需遍历叶子节点链表即可,B树却需要重复地中序遍历

面试官:说一下B树与B+树的区别(咳咳,结合索引我觉得你肯定很清楚2种树的区别)

3、查找一次数据真正的流程

如上图,若查找索引K=10的数据是怎样的一个流程

  • 所有根节点(2,20),从磁盘load在内存上去
  • 把查找的key(10)放在内存进行比对(时间可以忽略不计)
  • 查找到对应范围(在2~20范围内),把第二层的节点(3,9,15)从磁盘load到内存,再进行比对(因为节点都是排序好的,所以可以进行二分查找),范围是(9-15)
  • 最终定位到叶子节点(10,13),找到K对应的value,也就是磁盘存储的地址,这样就找到对应的数据行

二、innoDB引擎

存储引擎都是对于表而言的。每个表格的存储引擎可以不同。

1、innoDB存储引擎索引实现

对于一个表使用innoDB引擎,它在磁盘存储的文件有2个。

image.png

一个是.frm,.ibd。.frm文件存储的就是表的架构,主要组成元素。.ibd存储的是数据和索引。没错,innodb是将数据和索引放在一个文件的。

innoDB索引实现:现在叶子节点的Value值存储的不是对应磁盘文件地址,而是存储的是其他列的值。这就是把值和索引共同存的方法,这样查找到K,对应的行所有值都会被找到,大大节省了查询的效率

image.png

  • 表数据文件本身就是按照B+树组织的一个索引结构文件
  • 聚集索引-叶子节点包含了完整的数据记录
  • 建议innodb表必须建组件,并推荐使用整型的自增主键
  • 非主键索引结构叶子节点存储的是主键值

聚集索引又是神马?

聚集索引:叶节点包含完整的数据记录。 主键索引也即聚集索引

对于主键索引。若数据和索引放在一起的就是聚集索引(innodb引擎),数据和索引放在不同文件就是非聚集索引。(MyISAM引擎)

那么innodb有没有非聚集索引?

有。对于二级索引,其叶子节点的value值,存储的不是一行数据其他值,而是存储的是主键的值。 当使用二级索引的时候,还需要回表从主键进行查找。效率会慢一点。

为什么建议InnoDB表必须建主键,不建可以吗?

因为有了主键就有聚集索引,查找效率会更高。 若不建可以吗,若不建的话,mysql底层会自动维护一个主键列,这样浪费mysql资源,增加mysql的负担。所以一张表有且只有一个聚集索引。

为什么建议使用整型的自增主键:

  • 因为会有大小比较,这样整形比字符串比较肯定会快
  • 非自增可能会出现重新组合节点的值,出现分裂,若影响树的平衡还会调整树的平衡,影响效率。自增的情况,会自动加入叶子节点的右边,若没有空间,会自动开辟空间
2、联合索引

联合索引,是将多个值作为索引进行查找。当然了,底层还是B+树。K对应的值有多个(ID,Name),对应的value值是对应行剩下的所有信息。如下图

image.png

那对于联合索引是怎么排序的,这里排序要符合最左前缀原则。最左前缀原则就是查询从索引的最左列开始并且不跳过索引中的列,进行排序查询。所以在叶子节点,发现ID一致,然后排序就按照Name进行排序。

若查询的时候不按照顺序建立的K值(ID,Name),也就是先查找Name再查ID,则引擎会自动不选择使用index。因为此时没有任何的排序规则。

三、MySql优化

每一次查找都是在和磁盘做IO操作,优化Mysql也就是想办法减少IO操作。

1、EXPLAIN

使用EXPLAIN可以分析查询性能,EXPLAIN只能分析SELECT语句

image.png

对于EXPLAIN需要关注哪些字段呢?

  • type
关键字 备注
ALL 这样说明执行了全表查询。最差的情况
index 执行全表查询,并且可以通过索引完成结果扫描并且直接从索引中取的想要的结果数据,也就是可以避免回表,比ALL略好,因为索引文件通常比全部数据要来的小
index_merge 利用index merge特性用到多个索引,提高查询效率
const 基于主键或唯一索引唯一值查询,最多返回一条结果
system 查询对象表只有一行数据,这是最好的情况
* Extra
关键字 备注
Using filesort 将用外部排序而不是按照索引顺序排列结果,数据较少时从内存排序,否则需要在磁盘完成排序,代价非常高,需要添加合适的索引
Using temporary 需要创建一个临时表来存储结果,这通常发生在对没有索引的列进行GROUP BY时,或者ORDER BY里的列不都在索引里,需要添加合适的索引
Using index 表示MySQL使用覆盖索引避免全表扫描,不需要再到表中进行二次查找数据,这是比较好的结果之一。注意不要和type中的index类型混淆
Using where 通常是进行了全表/全索引扫描后再用WHERE子句完成结果过滤,需要添加合适的索引
* rows

该参数要代表了本次查询遍历了多少行数据

2、索引优化的方法
  • 如果索引了多列,要符合左前缀法则
  • 不在索引做任何的操作(计算、函数、类型转换等),这样会导致索引失效而转向全表扫描
  • 尽量使用访问索引的查询,索引列包含查询列,减少select *语句
  • mysql在使用不等于的时候,不能使用索引导致全表扫描
  • like以通配符(%abc)开始,索引会失效。解决:查询条件是覆盖索引

总之最主要的就是尽可能的使用索引值,避免建立索引而没有使用的情况。

参考文档

  • EXPLAIN执行计划中要重点关注哪些要素

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java性能优化

发表于 2021-03-16

持续地优化代码,提高代码的质量,是提升系统生命力的有效手段之一。软件系统思维有句话“Less coding, more thinking(少编码、多思考)”,也有这么一句俚语“Think more, code less(思考越多,编码越少)”。所以,我们在编码中多思考多总结,努力提升自己的编码水平,才能编写出更优雅、更高质、更高效的代码。

在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。

一、使用通用工具函数

案例一

不完善的写法:

1
csharp复制代码thisName != null && thisName.equals(name);

更完善的写法:

1
ini复制代码(thisName == name) || (thisName != null && thisName.equals(name));

建议方案:

1
csharp复制代码Objects.equals(name, thisName);
案例二

现象描述:

1
ini复制代码!(list == null || list.isEmpty());

建议方案:

1
ini复制代码CollectionUtils.isNotEmpty(list);
主要收益
  • 函数式编程,业务代码减少,逻辑一目了然;
  • 通用工具函数,逻辑考虑周全,出问题概率低。
  • Java常用工具类

二、减少函数代码层级

如果要使函数优美,建议函数代码层级在1-4之间,过多的缩进会让函数难以阅读。

案例一:利用return提前返回函数

现象描述:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码// 获取用户余额函数
public Double getUserBalance(Long userId) {
User user = getUser(userId);
if (Objects.nonNull(user)) {
UserAccount account = user.getAccount();
if (Objects.nonNull(account)) {
return account.getBalance();
}
}
return null;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码// 获取用户余额函数
public Double getUserBalance(Long userId) {
// 获取用户信息
User user = getUser(userId);
if (Objects.isNull(user)) {
return null;
}

// 获取用户账户
UserAccount account = user.getAccount();
if (Objects.isNull(account)) {
return null;
}

// 返回账户余额
return account.getBalance();
}
案例二:利用continue提前结束循环

现象描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码// 获取合计余额函数
public double getTotalBalance(List<User> userList) {
// 初始合计余额
double totalBalance = 0.0D;

// 依次累加余额
for (User user : userList) {
// 获取用户账户
UserAccount account = user.getAccount();
if (Objects.nonNull(account)) {
// 累加用户余额
Double balance = account.getBalance();
if (Objects.nonNull(balance)) {
totalBalance += balance;
}
}
}

// 返回合计余额
return totalBalance;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码// 获取合计余额函数
public double getTotalBalance(List<User> userList) {
// 初始合计余额
double totalBalance = 0.0D;

// 依次累加余额
for (User user : userList) {
// 获取用户账户
UserAccount account = user.getAccount();
if (Objects.isNull(account)) {
continue;
}

// 累加用户余额
Double balance = account.getBalance();
if (Objects.nonNull(balance)) {
totalBalance += balance;
}
}

// 返回合计余额
return totalBalance;
}
  • 特殊说明
    其它方式:在循环体中,先调用案例1的函数getUserBalance(获取用户余额),再进行对余额进行累加。
    在循环体中,建议最多使用一次continue。如果需要有使用多次continue的需求,建议把循环体封装为一个函数。
案例三:利用条件表达式函数减少层级
主要收益
  • 代码层级减少,代码缩进减少;
  • 模块划分清晰,方便阅读维护。

三、封装条件表达式函数

案例一:把简单条件表达式封装为函数

现象描述:

1
2
3
4
5
6
7
8
scss复制代码// 获取门票价格函数
public double getTicketPrice(Date currDate) {
if (Objects.nonNull(currDate) && currDate.after(DISCOUNT_BEGIN_DATE)
&& currDate.before(DISCOUNT_END_DATE)) {
return TICKET_PRICE * DISCOUNT_RATE;
}
return TICKET_PRICE;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码// 获取门票价格函数
public double getTicketPrice(Date currDate) {
if (isDiscountDate(currDate)) {
return TICKET_PRICE * DISCOUNT_RATE;
}
return TICKET_PRICE;
}

// 是否折扣日期函数
private static boolean isDiscountDate(Date currDate) {
return Objects.nonNull(currDate) &&
currDate.after(DISCOUNT_BEGIN_DATE)
&& currDate.before(DISCOUNT_END_DATE);
}
案例二:把复杂条件表达式封装为函数

现象描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码// 获取土豪用户列表
public List<User> getRichUserList(List<User> userList) {
// 初始土豪用户列表
List<User> richUserList = new ArrayList<>();

// 依次查找土豪用户
for (User user : userList) {
// 获取用户账户
UserAccount account = user.getAccount();
if (Objects.nonNull(account)) {
// 判断用户余额
Double balance = account.getBalance();
if (Objects.nonNull(balance) && balance.compareTo(RICH_THRESHOLD) >= 0) {
// 添加土豪用户
richUserList.add(user);
}
}
}

// 返回土豪用户列表
return richUserList;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码// 获取土豪用户列表
public List<User> getRichUserList(List<User> userList) {
// 初始土豪用户列表
List<User> richUserList = new ArrayList<>();

// 依次查找土豪用户
for (User user : userList) {
// 判断土豪用户
if (isRichUser(user)) {
// 添加土豪用户
richUserList.add(user);
}
}

// 返回土豪用户列表
return richUserList;
}

// 是否土豪用户
private boolean isRichUser(User user) {
// 获取用户账户
UserAccount account = user.getAccount();
if (Objects.isNull(account)) {
return false;
}

// 获取用户余额
Double balance = account.getBalance();
if (Objects.isNull(balance)) {
return false;
}

// 比较用户余额
return balance.compareTo(RICH_THRESHOLD) >= 0;
}

以上代码也可以用采用流式(Stream)编程的过滤来实现。

主要收益
  • 把条件表达式从业务函数中独立,使业务逻辑更清晰;
  • 封装的条件表达式为独立函数,可以在代码中重复使用。

四、尽量避免不必要的空指针判断

本章只适用于项目内部代码,并且是自己了解的代码,才能够尽量避免不必要的空指针判断。对于第三方中间件和系统接口,必须做好空指针判断,以保证代码的健壮性。

案例一:调用函数保证参数不为空,被调用函数尽量避免不必要的空指针判断

现象描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码// 创建用户信息
User user = new User();
... // 赋值用户相关信息
createUser(user);

// 创建用户函数
private void createUser(User user){
// 判断用户为空
if(Objects.isNull(user)) {
return;
}

// 创建用户信息
userDAO.insert(user);
userRedis.save(user);
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码// 创建用户信息
User user = new User();
... // 赋值用户相关信息
createUser(user);

// 创建用户函数
private void createUser(User user){
// 创建用户信息
userDAO.insert(user);
userRedis.save(user);
}
案例二:被调用函数保证返回不为空,调用函数尽量避免不必要的空指针判断

现象描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码// 保存用户函数
public void saveUser(Long id, String name) {
// 构建用户信息
User user = buildUser(id, name);
if (Objects.isNull(user)) {
throw new BizRuntimeException("构建用户信息为空");
}

// 保存用户信息
userDAO.insert(user);
userRedis.save(user);
}

// 构建用户函数
private User buildUser(Long id, String name) {
User user = new User();
user.setId(id);
user.setName(name);
return user;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码// 保存用户函数
public void saveUser(Long id, String name) {
// 构建用户信息
User user = buildUser(id, name);

// 保存用户信息
userDAO.insert(user);
userRedis.save(user);
}

// 构建用户函数
private User buildUser(Long id, String name) {
User user = new User();
user.setId(id);
user.setName(name);
return user;
}
案例三:赋值逻辑保证列表数据项不为空,处理逻辑尽量避免不必要的空指针判断

现象描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
scss复制代码// 查询用户列表
List<UserDO> userList = userDAO.queryAll();
if (CollectionUtils.isEmpty(userList)) {
return;
}

// 转化用户列表
List<UserVO> userVoList = new ArrayList<>(userList.size());
for (UserDO user : userList) {
UserVO userVo = new UserVO();
userVo.setId(user.getId());
userVo.setName(user.getName());
userVoList.add(userVo);
}

// 依次处理用户
for (UserVO userVo : userVoList) {
// 判断用户为空
if (Objects.isNull(userVo)) {
continue;
}

// 处理相关逻辑
...
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码// 查询用户列表
List<UserDO> userList = userDAO.queryAll();
if (CollectionUtils.isEmpty(userList)) {
return;
}

// 转化用户列表
List<UserVO> userVoList = new ArrayList<>(userList.size());
for (UserDO user : userList) {
UserVO userVo = new UserVO();
userVo.setId(user.getId());
userVo.setName(user.getName());
userVoList.add(userVo);
}

// 依次处理用户
for (UserVO userVo : userVoList) {
// 处理相关逻辑
...
}
案例四:MyBatis查询函数返回列表和数据项不为空,可以不用空指针判断

MyBatis是一款优秀的持久层框架,是在项目中使用的最广泛的数据库中间件之一。通过对MyBatis源码进行分析,查询函数返回的列表和数据项都不为空,在代码中可以不用进行空指针判断。

现象描述:
这种写法没有问题,只是过于保守了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
scss复制代码// 查询用户函数
public List<UserVO> queryUser(Long id, String name) {
// 查询用户列表
List<UserDO> userList = userDAO.query(id, name);
if (Objects.isNull(userList)) {
return Collections.emptyList();
}

// 转化用户列表
List<UserVO> voList = new ArrayList<>(userList.size());
for (UserDO user : userList) {
// 判断对象为空
if (Objects.isNull(user)) {
continue;
}

// 添加用户信息
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
voList.add(vo);
}

// 返回用户列表
return voList;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码// 查询用户函数
public List<UserVO> queryUser(Long id, String name) {
// 查询用户列表
List<UserDO> userList = userDAO.query(id, name);

// 转化用户列表
List<UserVO> voList = new ArrayList<>(userList.size());
for (UserDO user : userList) {
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
voList.add(vo);
}

// 返回用户列表
return voList;
}
主要收益
  • 避免不必要的空指针判断,精简业务代码处理逻辑,提高业务代码运行效率;
  • 这些不必要的空指针判断,基本属于永远不执行的Death代码,删除有助于代码维护。

五、内部函数参数尽量使用基础类型

案例一:内部函数参数尽量使用基础类型

现象描述:

1
2
3
4
5
6
7
8
9
ini复制代码// 调用代码
double price = 5.1D;
int number = 9;
double total = calculate(price, number);

// 计算金额函数
private double calculate(Double price, Integer number) {
return price * number;
}

建议方案:

1
2
3
4
5
6
7
8
9
arduino复制代码// 调用代码
double price = 5.1D;
int number = 9;
double total = calculate(price, number);

// 计算金额函数
private double calculate(double price, int number) {
return price * number;
}
案例二:内部函数返回值尽量使用基础类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码现象描述:
// 获取订单总额函数
public double getOrderAmount(List<Product> productList) {
double amount = 0.0D;
for (Product product : productList) {
if (Objects.isNull(product) || Objects.isNull(product.getPrice())
|| Objects.isNull(product.getNumber())) {
continue;
}
amount += calculate(product.getPrice(), product.getNumber());
}
return amount;
}

// 计算金额函数
private Double calculate(double price, double number) {
return price * number;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码// 获取订单总额函数
public double getOrderAmount(List<Product> productList) {
double amount = 0.0D;
for (Product product : productList) {
if (Objects.isNull(product) || Objects.isNull(product.getPrice())
|| Objects.isNull(product.getNumber())) {
continue;
}
amount += calculate(product.getPrice(), product.getNumber());
}
return amount;
}

// 计算金额函数
private double calculate(double price, double number) {
return price * number;
}

此处只是举例说明这种现象,更好的方式是采用流式(Stream)编程。

主要收益
  • 内部函数尽量使用基础类型,避免了隐式封装类型的打包和拆包
  • 内部函数参数使用基础类型,用语法上避免了内部函数的参数空指针判断
  • 内部函数返回值使用基础类型,用语法上避免了调用函数的返回值空指针判断

六、尽量避免返回的数组和列表为null

案例一:尽量避免返回的数组为null,引起不必要的空指针判断

现象描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
scss复制代码// 调用代码
UserVO[] users = queryUser();
if (Objects.nonNull(users)) {
for (UserVO user : users) {
// 处理用户信息
}
}

// 查询用户函数
private UserVO[] queryUser() {
// 查询用户列表
List<UserDO> userList = userDAO.queryAll();
if (CollectionUtils.isEmpty(userList)) {
return null;
}

// 转化用户数组
UserVO[] users = new UserVO[userList.size()];
for (int i = 0; i < userList.size(); i++) {
UserDO user = userList.get(i);
users[i] = new UserVO();
users[i].setId(user.getId());
users[i].setName(user.getName());
}

// 返回用户数组
return users;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ini复制代码// 调用代码
UserVO[] users = queryUser();
for (UserVO user : users) {
// 处理用户信息
}

// 查询用户函数
private UserVO[] queryUser() {
// 查询用户列表
List<UserDO> userList = userDAO.queryAll();
if (CollectionUtils.isEmpty(userList)) {
return new UserVO[0];
}

// 转化用户数组
UserVO[] users = new UserVO[userList.size()];
for (int i = 0; i < userList.size(); i++) {
UserDO user = userList.get(i);
users[i] = new UserVO();
users[i].setId(user.getId());
users[i].setName(user.getName());
}

// 返回用户数组
return users;
}
案例二:尽量避免返回的列表为null,引起不必要的空指针判断

现象描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
scss复制代码// 调用代码
List<UserVO> userList = queryUser();
if (Objects.nonNull(userList)) {
for (UserVO user : userList) {
// 处理用户信息
}
}

// 查询用户函数
private List<UserVO> queryUser(){
// 查询用户列表
List<UserDO> userList = userDAO.queryAll();
if(CollectionUtils.isEmpty(userList)) {
return null;
}

// 转化用户列表
List<UserVO> userVoList = new ArrayList<>(userList.size());
for(UserDO user : userList) {
UserVO userVo = new UserVO();
userVo.setId(user.getId());
userVo.setName(user.getName());
userVoList.add(userVo);
}

// 返回用户列表
return userVoList;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
scss复制代码// 调用代码
List<UserVO> userList = queryUser();
for (UserVO user : userList) {
// 处理用户信息
}

// 查询用户函数
private List<UserVO> queryUser(){
// 查询用户列表
List<UserDO> userList = userDAO.queryAll();
if(CollectionUtils.isEmpty(userList)) {
return Collections.emptyList();
}

// 转化用户列表
List<UserVO> userVoList = new ArrayList<>(userList.size());
for(UserDO user : userList) {
UserVO userVo = new UserVO();
userVo.setId(user.getId());
userVo.setName(user.getName());
userVoList.add(userVo);
}

// 返回用户列表
return userVoList;
}
主要收益
  • 保证返回的数组和列表不为null, 避免调用函数的空指针判断

七、封装函数传入参数

案例一:当传入参数过多时,应封装为参数类

Java规范不允许函数参数太多,不便于维护也不便于扩展。
现象描述:

1
2
3
4
5
typescript复制代码// 修改用户函数
public void modifyUser(Long id, String name, String phone, Integer age,
Integer sex, String address, String description) {
// 具体实现逻辑
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
less复制代码// 修改用户函数
public void modifyUser(User user) {
// 具体实现内容
}

// 用户类
@Getter
@Setter
@ToString
private class User{
private Long id;
private String name;
private String phone;
private Integer age;
private Integer sex;
private String address;
private String description;
}
案例二:当传入成组参数时,应封装为参数类

既然参数成组出现,就需要封装一个类去描述这种现象。
现象描述:

1
2
3
4
arduino复制代码// 获取距离函数
public double getDistance(double x1, double y1, double x2, double y2) {
// 具体实现逻辑
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码// 获取距离函数
public double getDistance(Point point1, Point point2) {
// 具体实现逻辑
}

// 点类
@Getter
@Setter
@ToString
private class Point{
private double x;
private double y;
}
主要收益
  • 封装过多函数参数为类,使函数更便于扩展和维护;
  • 封装成组函数参数为类,使业务概念更明确更清晰。

八、利用return精简不必要的代码

案例一:删除不必要的if

现象描述:

1
2
3
4
5
6
7
kotlin复制代码// 是否通过函数
public boolean isPassed(Double passRate) {
if (Objects.nonNull(passRate) && passRate.compareTo(PASS_THRESHOLD) >= 0) {
return true;
}
return false;
}

建议方案:

1
2
3
4
typescript复制代码// 是否通过函数
public boolean isPassed(Double passRate) {
return Objects.nonNull(passRate) && passRate.compareTo(PASS_THRESHOLD) >= 0;
}
案例二:删除不必要的else

现象描述:

1
2
3
4
5
6
7
8
9
scss复制代码// 结算工资函数
public double settleSalary(Long workId, int workDays) {
// 根据是否合格处理
if (isQualified(workId)) {
return settleQualifiedSalary(workDays);
} else {
return settleUnqualifiedSalary(workDays);
}
}

建议方案:

1
2
3
4
5
6
7
8
scss复制代码// 结算工资函数
public double settleSalary(Long workId, int workDays) {
// 根据是否合格处理
if (isQualified(workId)) {
return settleQualifiedSalary(workDays);
}
return settleUnqualifiedSalary(workDays);
}
案例三:删除不必要的变量

现象描述:

1
2
3
4
5
6
7
8
ini复制代码// 查询用户函数
public List<UserDO> queryUser(Long id, String name) {
UserQuery userQuery = new UserQuery();
userQuery.setId(id);
userQuery.setName(name);
List<UserDO> userList = userDAO.query(userQuery);
return userList;
}

建议方案:

1
2
3
4
5
6
7
scss复制代码// 查询用户函数
public List<UserDO> queryUser(Long id, String name) {
UserQuery userQuery = new UserQuery();
userQuery.setId(id);
userQuery.setName(name);
return userDAO.query(userQuery);
}
主要收益
  • 精简不必要的代码,让代码看起来更清爽

九、利用临时变量优化代码

在一些代码中,经常会看到a.getB().getC()…getN()的写法,代码健壮性和可读性太差。建议:杜绝函数的级联调用,利用临时变量进行拆分,并做好对象空指针检查。

案例一:利用临时变量厘清逻辑

现象描述:

1
2
3
4
5
6
scss复制代码// 是否土豪用户函数
private boolean isRichUser(User user) {
return Objects.nonNull(user.getAccount())
&& Objects.nonNull(user.getAccount().getBalance())
&& user.getAccount().getBalance().compareTo(RICH_THRESHOLD) >= 0;
}

这是精简代码控的最爱,但是可读性实在太差。
建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 是否土豪用户函数
private boolean isRichUser(User user) {
// 获取用户账户
UserAccount account = user.getAccount();
if (Objects.isNull(account)) {
return false;
}

// 获取用户余额
Double balance = account.getBalance();
if (Objects.isNull(balance)) {
return false;
}

// 比较用户余额
return balance.compareTo(RICH_THRESHOLD) >= 0;
}

这个方案,增加了代码行数,但是逻辑更清晰。
有时候,当代码的精简性和可读性发生冲突时,更偏向于保留代码的可读性。

案例二:利用临时变量精简代码

现象描述:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码// 构建用户函数
public UserVO buildUser(UserDO user) {
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setName(user.getName());
if (Objects.nonNull(user.getAccount())) {
vo.setBalance(user.getAccount().getBalance());
vo.setDebt(user.getAccount().getDebt());
}
return vo;
}

建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码// 构建用户函数
public UserVO buildUser1(UserDO user) {
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setName(user.getName());
UserAccount account = user.getAccount();
if (Objects.nonNull(account)) {
vo.setBalance(account.getBalance());
vo.setDebt(account.getDebt());
}
return vo;
}
主要收益
  • 利用临时变量厘清逻辑,显得业务逻辑更清晰;
  • 利用临时变量精简代码,看变量名称即知其义,减少了大量无用代码;
  • 如果获取函数比较复杂耗时,利用临时变量可以提高运行效率;
  • 利用临时变量避免函数的级联调用,可有效预防空指针异常。

十、其他细节

1. 尽量避免随意使用静态变量

当某个对象被定义为static变量所引用,那么GC通常是不会回收这个对象所占有的内存,如

1
2
3
java复制代码public class A{
private static B b = new B();
}

此时静态变量b的生命周期与A类同步,如果A类不会卸载,那么b对象会常驻内存,直到程序终止。

2. 尽量避免过多过常地创建Java对象

尽量避免在经常调用的方法,循环中new对象,由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可以控制的范围内,最大限度地重用对象,最好能用基本的数据类型或数组来替代对象。

3. 尽量使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快;其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。

4. 慎用synchronized,尽量减小synchronize的方法

都知道,实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。synchronize方法被调用时,直接会把当前对象锁了,在方法执行完之前其他线程无法调用当前对象的其他方法。所以,synchronize的方法尽量减小,并且应尽量使用方法同步代替代码块同步。

5. 尽量使用基本数据类型代替对象

1
ini复制代码String str = "hello";

上面这种方式会创建一个“hello”字符串,而且JVM的字符缓存池还会缓存这个字符串;

1
ini复制代码String str = new String("hello");

此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o

6. 多线程在未发生线程安全前提下应尽量使用HashMap、ArrayList

HashTable、Vector等使用了同步机制,降低了性能。

7. 尽量合理的创建HashMap

当你要创建一个比较大的hashMap时,充分利用这个构造函数

1
arduino复制代码public HashMap(int initialCapacity, float loadFactor);

避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认中initialCapacity只有16,而loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小,同样的Hashtable,Vectors也是一样的道理。

8. 尽量减少对变量的重复计算

如:

1
css复制代码for(int i=0;i<list.size();i++)

应该改为:

1
css复制代码for(int i=0,len=list.size();i<len;i++)

并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。

9. 尽量避免不必要的创建

如:

1
2
3
4
ini复制代码A a = new A();
if(i==1){
list.add(a);
}

应该改为:

1
2
3
4
ini复制代码if(i==1){ 
A a = new A();
list.add(a);
}

10. 尽量在finally块中释放资源

程序中使用到的资源应当被释放,以避免资源泄漏,这最好在finally块中去做。不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。

11. 尽量确定StringBuffer的容量

StringBuffer 的构造器会创建一个默认大小(通常是16)的字符数组。在使用中,如果超出这个大小,就会重新分配内存,创建一个更大的数组,并将原先的数组复制过来,再丢弃旧的数组。在大多数情况下,你可以在创建 StringBuffer的时候指定大小,这样就避免了在容量不够的时候自动增长,以提高性能。

如:

1
ini复制代码StringBuffer buffer = new StringBuffer(1000);

12. 尽量早释放无用对象的引用

大部分时,方法局部引用变量所引用的对象会随着方法结束而变成垃圾,因此,大部分时候程序无需将局部,引用变量显式设为null。

例如:

1
2
3
4
5
typescript复制代码public void test(){
Object obj = new Object();
……
Obj=null;
}

上面这个就没必要了,随着方法test()的执行完成,程序中obj引用变量的作用域就结束了。但是如果是改成下面:

1
2
3
4
5
6
7
typescript复制代码public void test(){
Object obj = new Object();
……
Obj=null;
……
//执行耗时,耗内存操作;或调用耗时,耗内存的方法
}

这时候就有必要将obj赋值为null,可以尽早的释放对Object对象的引用。

13. 尽量避免使用二维数组

二维数据占用的内存空间比一维数组多得多,大概10倍以上。

14. ArrayList & LinkedList

一个是线性表,一个是链表,一句话,随机查询尽量使用ArrayList,ArrayList优于LinkedList,LinkedList还要移动指针,添加删除的操作LinkedList优于ArrayList,ArrayList还要移动数据,不过这是理论性分析,事实未必如此,重要的是理解好2者得数据结构,对症下药。

15. 尽量使用System.arraycopy ()代替通过来循环复制数组

System.arraycopy() 要比通过循环来复制数组快的多

  • public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
    src:源数组;
    srcPos:源数组要复制的起始位置;
    dest:目的数组;
    destPos:目的数组放置的起始位置;
    length:复制的长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ini复制代码public static void main(String[] args) {
String[] a = new String[100];
Arrays.fill(a, "hello");
Long start, end, start1, end1;
int n = 1000000;
int i;
int j;
start = System.currentTimeMillis();
{
String[] b = new String[100];
for (i = 0; i < n; i++) {
for (j = 0; j < a.length; j++) {
b[j] = a[j];
}
}

}
end = System.currentTimeMillis();
i = 0;
start1 = System.currentTimeMillis();
{
String[] c = new String[100];
for (i = 0; i < n; i++) {
System.arraycopy(a, 0, c, 0, a.length);
}

}
end1 = System.currentTimeMillis();
System.out.println("for循环复制数组1000000次花费时间:" + (end - start));
System.out.println("System.arraycopy()循环复制数组1000000次花费时间:" + (end1 - start1));
}

运行结果

1
2
scss复制代码for循环复制数组1000000次花费时间:161
System.arraycopy()循环复制数组1000000次花费时间:26

16. 尽量重用对象

过分的创建对象会消耗系统的大量内存,严重时,会导致内存泄漏,因此,保证过期的对象的及时回收具有重要意义。JVM的GC并非十分智能,因此建议在对象使用完毕后,手动设置成null。

特别是String对象的使用中,出现字符串连接情况时应使用StringBuffer代替,由于系统不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理。因此生成过多的对象将会给程序的性能带来很大的影响。

17. 在java+Oracle的应用系统开发中,java中内嵌的SQL语言应尽量使用大写形式,以减少Oracle解析器的解析负担。

18. 在java编程过程中,进行数据库连接,I/O流操作,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销。

19. 不要在循环中使用Try/Catch语句,应把Try/Catch放在循环最外层

20. 通过StringBuffer的构造函数来设定它的初始化容量,可以明显提升性能

StringBuffer的默认容量为16,当StringBuffer的容量达到最大容量时,它会将自身容量增加到当前的2倍+2,也就是2*n+2。无论何时,只要StringBuffer到达它的最大容量,它就不得不创建一个新的对象数组,然后复制旧的对象数组,这会浪费很多时间。所以给StringBuffer设置一个合理的初始化容量值,是很有必要的!

21. StringBuffer和StringBuilder

区别在于:java.lang.StringBuffer 线程安全的可变字符序列。一个类似于String的字符串缓冲区,但不能修改。StringBuilder与该类相比,通常应该优先使用StringBuilder类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。为了获得更好的性能,在构造StringBuffer或StringBuilder时应尽量指定她的容量。当然如果不超过16个字符时就不用了。 相同情况下,使用StringBuilder比使用StringBuffer仅能获得10%~15%的性能提升,但却要冒多线程不安全的风险。综合考虑还是建议使用StringBuffer。

####22. 考虑使用静态方法
如果没有必要去访问对象的外部,那么就使之成为静态方法。它会被更快地调用,因为它不需要一个虚拟函数导向表。这同时也是一个很好的实践,因为它告诉你如何区分方法的性质,调用这个方法不会改变对象的状态。

23. 避免在循环条件中使用复杂表达式

在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。例子:

1
2
3
4
5
6
arduino复制代码class CEL {
void method (Vector vector) {
for (int i = 0; i < vector.size (); i++)
; // ...
}
}

更正:

1
2
3
4
5
6
arduino复制代码class CEL_fixed {
int size = vector.size ()
void method (Vector vector) {
for (int i = 0; i < size; i++) ; // ...
}
}

24. 为’Vectors’ 和 ‘Hashtables’定义初始大小

JVM为Vector扩充大小的时候需要重新创建一个更大的数组,将原原先数组中的内容复制过来,最后,原先的数组再被回收。可见Vector容量的扩大是一个颇费时间的事。

通常,默认的10个元素大小是不够的。你最好能准确的估计你所需要的最佳大小。

1
2
ini复制代码public Vector v = new Vector(20);
public Hashtable hash = new Hashtable(10);

25. 对于常量字符串,用’String’ 代替 ‘StringBuffer’

常量字符串并不需要动态改变长度。

例子:

1
ini复制代码StringBuffer s = new StringBuffer ("Hello");

更正:把StringBuffer换成String,如果确定这个String不会再变的话,这将会减少运行开销提高性能。

26. 在字符串相加的时候,使用 ‘ ‘ 代替 “ “,如果该字符串只有一个字符的话

例子:

1
2
3
typescript复制代码public void method(String s) {
String string = s + "d";
}

更正: 将一个字符的字符串替换成’ ‘

1
2
3
typescript复制代码public void method(String s) {
String string = s + 'd';
}

以上仅是Java方面编程时的性能优化,性能优化大部分都是在时间、效率、代码结构层次等方面的权衡,各有利弊,不要把上面内容当成教条,或许有些对我们实际工作适用,有些不适用,还望根据实际工作场景进行取舍,活学活用,变通为宜。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot中使用多线程

发表于 2021-03-16

一、介绍

Spring是通过任务执行器(TaskExecutor)来实现多线程和并发编程,使用Spring提供的ThreadPoolTaskExecutor来创建一个基于线城池的TaskExecutor。在使用线程池的大多数情况下都是异步非阻塞的。节省更多的时间,提高效率。

工作原理

image

当主线程中调用execute接口提交执行任务时:
则执行以下步骤:
注意:线程池初始时,是空的。

  1. 如果当前线程数<corePoolSize,如果是则创建新的线程执行该任务
  2. 如果当前线程数>=corePoolSize,则将任务存入BlockingQueue
  3. 如果阻塞队列已满,且当前线程数<maximumPoolSize,则新建线程执行该任务。
  4. 如果阻塞队列已满,且当前线程数>=maximumPoolSize,则抛出异常RejectedExecutionException,告诉调用者无法再接受任务了。

在Springboot中对其进行了简化处理,只需要配置一个类型为java.util.concurrent.TaskExecutor或其子类的bean,并在配置类或直接在程序入口类上声明注解@EnableAsync,即可可以开启异步任务。

调用也简单,在由Spring管理的对象的方法上标注注解@Async,声明是异步任务,显式调用即可生效。

二、声明

让配置类实现AsyncConfigurer接口,并重写getAsyncExecutor方法,并返回一个ThreasPoolTaskExecutor,就可以获取一个基于线程池的TaskExecutor
使用注解@EnableAsync开启异步,会自动扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码@Configuration
@EnableAsync
public class ThreadConfig implements AsyncConfigurer {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(15);
executor.setQueueCapacity(25);
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}

三、调用

通过@Async注解表明该方法是异步方法,如果注解在类上,那表明这个类里面的所有方法都是异步的

1
2
3
4
5
6
7
8
9
java复制代码@Service
public class AsyncTaskService {

@Async
public void executeAsyncTask(int i) {
System.out.println("线程" + Thread.currentThread().getName() + " 执行异步任务:" + i);
}

}

四、进阶

有时候我们不止希望异步执行任务,还希望任务执行完成后会有一个返回值,在java中提供了Future泛型接口,用来接收任务执行结果,springboot也提供了此类支持,使用实现了ListenableFuture接口的类如AsyncResult来作为返回值的载体。比如上例中,我们希望返回一个类型为String类型的值,可以将返回值改造为:

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码    @Async
public Future<String> executeAsyncTaskWithResult2(int i) {
System.out.println("线程" + Thread.currentThread().getName() + " 开始执行异步任务" + i);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + " 结束执行异步任务" + i);
return new AsyncResult<>("线程" + Thread.currentThread().getName() + " 执行异步任务:" + i);
}

调用返回值:
get()是阻塞式,等待当前线程完成才返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码    public void threadTest() {
try {
List<Future> futures = new ArrayList<>();
for (int i = 0; i < 20; i++) {
futures.add(asyncTaskService.executeAsyncTaskWithResult2(i));
}
// 获取值。get是阻塞式,等待当前线程完成才返回值
for (Future<String> future : futures) {
System.out.println("返回结果:" + future.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}

补充

  1. 实际上,@Async还有一个参数,通过Bean名称来指定调用的线程池-比如上例中设置的线程池参数不满足业务需求,可以另外定义合适的线程池,调用时指明使用这个线程池-缺省时springboot会优先使用名称为’taskExecutor’的线程池,如果没有找到,才会使用其他类型为TaskExecutor或其子类的线程池。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…704705706…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%