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

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


  • 首页

  • 归档

  • 搜索

SpringBoot整合Swagger2

发表于 2021-11-22

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战」

前言:最近在参与一个二手交易平台网站的项目,因为是多人协作,打算采用整合swagger2,编写接口文档,方便团队成员参考和测试。

Swagger 的优势

  • 支持 API 自动生成同步的在线文档:使用 Swagger 后可以直接通过代码生成文档,不再需要自己手动编写接口文档了,对程序员来说非常方便,可以节约写文档的时间去学习新技术。
  • 提供 Web 页面在线测试 API:光有文档还不够,Swagger 生成的文档还支持在线测试。参数和格式都定好了,直接在界面上输入参数对应的值即可在线测试接口。

整合swagger

第一步:

引入pom依赖:

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码		<!--引入swagger2依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--引入swaggerui-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.3</version>
</dependency>

swagger-bootstrap-ui是国人开发的一个依赖jar包,用来美化原有的swagger界面

第二步:

编写配置类

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
java复制代码@Configuration
@EnableSwagger2
public class Swagger2Config {

@Bean
public Docket createRestApi(){

return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.jxauflea.controller"))// 扫描接口的包路径
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("接口文档")// swagger文档的标题
.description("")// 自定义相关描述
.contact(new Contact("xxx","http:localhost/doc.html","xxx@xxx.com"))
.version("1.0")// 当前的版本
.build();
}

}

注意:这里为了自动装配我们的swagger配置,需要在配置类的上方添加@EnableSwagger2注解

第三步

访问页面,通过地址访问文档:http://localhost:8080/doc.html

最终效果

image-20211122215400776

整合过程中出现的问题

项目一启动时,报错

image-20211122215239449

刚开始,我也在各大平台上搜过此错误,大多数的回答都是,swagger的pom中带的版本过低,需要升级版本

image-20211122215510090

我把解决措施放在这,可能会有小伙伴用的上

方法:升级guava

1
2
3
4
5
xml复制代码    <dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>

但我试过这样并没有效果,在我几番周折下,发现是springboot版本的问题,刚开始我是2.6.0,后面将版本到2.5.4,完美解决问题。

方法:降低springboot版本

image-20211122215741145

本文转载自: 掘金

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

学习 Elasticsearch 的第18天

发表于 2021-11-22

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

列出、删除和更新已安装的插件

可以使用以下list选项检索当前加载的插件列表:

1
bash复制代码sudo bin/elasticsearch-plugin list

或者,使用node-info API找出集群中每个节点上安装了哪些插件

删除插件

可以手动删除插件,方法是删除 下的相应目录 plugins/,或使用公共脚本:

1
arduino复制代码sudo bin/elasticsearch-plugin remove [pluginname]

删除 Java 插件后,您需要重新启动节点才能完成删除过程。

默认情况下,插件配置文件(如果有)保存在磁盘上;这是为了在升级插件时不会丢失配置。如果您希望在删除插件时清除配置文件,请使用-p或--purge。可以在删除插件后使用此选项来删除任何延迟的配置文件。

删除多个插件

可以在一次调用中删除多个插件,如下所示:

1
css复制代码sudo bin/elasticsearch-plugin remove [pluginname] [pluginname] ... [pluginname]

插件是为特定版本的 Elasticsearch 构建的,因此每次更新 Elasticsearch 时都必须重新安装。

1
2
bash复制代码sudo bin/elasticsearch-plugin remove [pluginname]
sudo bin/elasticsearch-plugin install [pluginname]

智能中文分析插件

智能中文分析插件将Lucene的智能中文分析模块集成到elasticsearch中。

它提供了中文或中英文混合文本的分析器。该分析器使用概率知识来寻找简体中文文本的最佳分词。文本首先被分解成句子,然后每个句子被分割成单词。

安装编辑

这个插件可以使用插件管理器安装:

1
bash复制代码sudo bin/elasticsearch-plugin install analysis-smartcn

该插件必须安装在集群中的每个节点上,安装后必须重新启动每个节点。

您可以从artifacts.elastic.co/downloads/e…下载此插件进行离线安装。要验证文件,请使用 SHA 哈希或 ASC 密钥。.zip

本文转载自: 掘金

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

SpringBoot基础之集成使用Redis

发表于 2021-11-22

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

前言

Redis是比较著名的NoSql数据库,主要用于存放KV型数据等非关系行数据,但随着Redis的发展,它所能做的功能越来越多,能够实现的场景包括但不限于:缓存,配置,排行榜,计数,分布式锁,限流,消息队列等等,当然我们提到他最多的时候是应用在缓存场景,因为redis是为缓存而生.

集成

添加Maven包

这里使用了spring-boot-starter-data-redis,它自带的客户端连接工具是lettuce.

当然你也可以使用redisson或者jedis,不过需要先排除lettuce,再引入对应的包

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

如果使用FastJson序列化 也需要引入fastjson ,你喜欢Jackson的话,不想写😂

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>

application.yml配置

1
2
3
4
5
6
yaml复制代码spring:
redis:
database: 0 #选择第一个数据库,可选0-15
host: 127.0.0.1
port: 6379
password: #无密码留空 ,有密码则设置密码

添加配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arduino复制代码
@Configuration
public class MyRedisConfig {

/**
* 指定FastJson序列化
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){

RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
GenericFastJsonRedisSerializer jsonRedisSerializer = new GenericFastJsonRedisSerializer();
template.setDefaultSerializer(jsonRedisSerializer);
template.setKeySerializer(jsonRedisSerializer);
template.setValueSerializer(jsonRedisSerializer);
return template;
}
}

这里使用的是FastJson序列化,并指定默认序列化方式,Key序列化方式和Value序列化方式都是FastJson.

在业务中注入方式为

1
2
typescript复制代码@Autowired
private RedisTemplate<String,Object> redisTemplate;

其中<String,Object>部分可以根据序号更换为其他类型

测试使用

一定要测试呀,我同事项目组,项目上线之后发现redis配置有问题,线程不释放,用上一段时间就卡死…

单线程 存储 100000 数据测试 : 取第二次测试结果耗时 17834 ms

1
2
3
4
5
6
7
8
9
10
ini复制代码@GetMapping("/string1")
public R string1() {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
String uuid = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(uuid, uuid);
}
long end = System.currentTimeMillis();
return R.success("redisTemplate单线程,存储 100000 key:需要时间", end - start);
}

100线程 并发 存储 100000 数据测试 :取第二次测试结果耗时 2795 ms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码    @GetMapping("/string100")
public ResultVo string100() throws InterruptedException {
redisTemplate.hasKey("123");

CountDownLatch countDownLatch = new CountDownLatch(100000);
ExecutorService pool = Executors.newFixedThreadPool(100);

long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
pool.execute(() -> {
String uuid = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(uuid, uuid);
countDownLatch.countDown();
});
}
countDownLatch.await();
long end = System.currentTimeMillis();

return ResultVo.success("redisTemplate 100线程 并发,存储 100000 Key 需要时间", end - start);
}

管道 存储 100000 数据测试 :取第二次测试结果耗时 2071 ms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码@GetMapping("/stringPipe")
public ResultVo stringPipe() throws InterruptedException {

long start = System.currentTimeMillis();
redisTemplate.executePipelined((RedisCallback) redisConnection ->{
for (int i = 0; i < 100000; i++) {
byte[] uuid = UUID.randomUUID().toString().getBytes();
redisConnection.set(uuid, uuid);
}
return null;
}, redisTemplate.getDefaultSerializer());

long end = System.currentTimeMillis();
return ResultVo.success("redisTemplate 管道 ,存储 100000 Key 需要时间", end - start);
}
1
2
3
4
arduino复制代码    作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

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

Java中发送Http请求之OkHttpClient 1 O

发表于 2021-11-22

这是我参与11月更文挑战的第13天,活动详情查看:11月更文挑战

Java中Http请求的方式很多, OkHttpClient因其独特的特性,非常适合在常见的场景中使用.

1 OkHttpClient的简介

1 OkHttpClient说明

OkHttpClient是一个高效的HTTP客户端,其特性包含:

  • 支持HTTP/2,允许所有同一个主机地址的请求共享同一个socket连接
  • 连接池减少请求延时
  • 透明的GZIP压缩减少响应数据的大小
  • 缓存响应内容,避免一些完全重复的请求

2 OkHttpClient使用步骤

  • 创建OkHttpClient对象
  • 创建Request对象
  • 将Request 对象封装为Call
  • 通过Call 来执行同步或异步请求,调用execute方法同步执行,调用enqueue方法异步执行

3 OkHttpClient案例

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
java复制代码@Slf4j
public class OkHttpDemo {

// 创建OkHttpClient对象, 并设置超时时间 添加拦截器LoginInterceptor
private static final OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor(new LoginInterceptor())
.build();

public static void main(String[] args) throws IOException {
String url = "http://www.baidu.com";
Request request = new Builder()
.url(url)
.get() // 不写,默认是GET请求
.build();

Call call = okHttpClient.newCall(request);
// 异步调用
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
log.info("请求失败,异常信息为: {}", e.getMessage());
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response)
throws IOException {
log.info("请求成功,返回信息为: {}", response.body().toString());
}
});
}
}

Interceptor拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Slf4j
public class LoginInterceptor implements Interceptor {

/**
* 统计登录接口完成时间
*/
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {

Request request = chain.request();
long begin = System.currentTimeMillis();
Response response = chain.proceed(request);
long end = System.currentTimeMillis();

// 请求路径中有/login 统计花费时间
if (request.url().toString().contains("/login")) {
log.info("接口处理总用时: {} ", end - begin);
}

return response;
}
}

OkHttpClient对象

通过其内部类Builder的构造器模式,进行属性参数的初始化,常见的包括: 任务调度,协议,连接池,连接超时,读取超时等属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码 public static final class Builder {
@NotNull
private Dispatcher dispatcher;
@NotNull
private ConnectionPool connectionPool;
@NotNull
private final List interceptors;
private int callTimeout;
private int connectTimeout;
private int readTimeout;
private int writeTimeout;
private int pingInterval;
private long minWebSocketMessageToCompress;
@Nullable
private RouteDatabase routeDatabase;

}

Request对象

通过其内部类构造器模式,进行属性参数的初始化,常见的包括: 请求地址url,请求方式,请求头,请求体,标签参数等, 并且该构造中默认是GET请求.

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
java复制代码  open class Builder {
internal var url: HttpUrl? = null
internal var method: String
internal var headers: Headers.Builder
internal var body: RequestBody? = null

/** A mutable map of tags, or an immutable empty map if we don't have any. */
internal var tags: MutableMap<Class<*>, Any> = mutableMapOf()

constructor() {
this.method = "GET"
this.headers = Headers.Builder()
}

internal constructor(request: Request) {
this.url = request.url
this.method = request.method
this.body = request.body
this.tags = if (request.tags.isEmpty()) {
mutableMapOf()
} else {
request.tags.toMutableMap()
}
this.headers = request.headers.newBuilder()
}
}

Call对象

通过OkHttpClient和Request对象构造Call对象,Call接口的唯一实现类RealCall. 其execute方法表示同步执行, enqueue方法表示异步执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    override fun execute(): Response {
check(executed.compareAndSet(false, true)) { "Already Executed" }

timeout.enter()
callStart()
try {
client.dispatcher.executed(this)
return getResponseWithInterceptorChain()
} finally {
client.dispatcher.finished(this)
}
}

override fun enqueue(responseCallback: Callback) {
check(executed.compareAndSet(false, true)) { "Already Executed" }

callStart()
client.dispatcher.enqueue(AsyncCall(responseCallback))
}

Interceptor拦截器

Interceptor为所有拦截器的接口, 其实现类有 桥接拦截器BridgeInterceptor, 缓存拦截器CacheInterceptor, 服务拦截器CallServerInterceptor, 错误、重定向拦截器RetryAndFollowUpInterceptor, 连接拦截器ConnectInterceptor.

Call中execute方法调用的getResponseWithInterceptorChain()方法, 创建一个拦截器集合容器,首先添加用户自定义的拦截器, 错误、重定向拦截器,桥接拦截器,缓存拦截器,连接拦截器,服务拦截器.

整个拦截器执行链路,按照添加先后顺序执行,即先执行用户自定义拦截器.

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
java复制代码  internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)

val chain = RealInterceptorChain(
call = this,
interceptors = interceptors,
index = 0,
exchange = null,
request = originalRequest,
connectTimeoutMillis = client.connectTimeoutMillis,
readTimeoutMillis = client.readTimeoutMillis,
writeTimeoutMillis = client.writeTimeoutMillis
)

var calledNoMoreExchanges = false
try {
val response = chain.proceed(originalRequest)
if (isCanceled()) {
response.closeQuietly()
throw IOException("Canceled")
}
return response
} catch (e: IOException) {
calledNoMoreExchanges = true
throw noMoreExchanges(e) as Throwable
} finally {
if (!calledNoMoreExchanges) {
noMoreExchanges(null)
}
}
}

4 OkHttpClient常用工具类:

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
java复制代码@Slf4j
public class OkHttpUtils {

// 创建OkHttpClient对象, 并设置超时时间
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build();

/**
* 同步GET请求
*
* @param url 请求地址
*/
public static String getRequest(String url) {
try {
// 1 创建OkHttpClient对象
// 2 构建Request对象
Request request = new Request.Builder()
.get()// 不写默认为GET请求
.url(url)
.build();
// 3 发起请求获取响应值
Response response = client.newCall(request).execute();
// 4 根据响应结果判断
if (response.isSuccessful()) {
return response.body().string();
} else {
throw new RuntimeException("请求异常,错误码为: " + response.code());
}
} catch (Exception e) {
log.info("请求失败,错误信息为= {} ", e.getMessage());
}
return null;
}

/**
* 同步POST请求
*
* @param url 请求地址
* @param params 请求参数
*/
public static String postRequest(String url, Map<String, String> params) {

try {
// 1 创建OkHttpClient对象
// 2 构建请求体
MultipartBody body = new MultipartBody.Builder()
.setType(MediaType.parse("multipart/form-data"))
.addFormDataPart("username", params.get("username"))
.addFormDataPart("password", params.get("password"))
.build();
// 3 构建Request对象
Request request = new Request.Builder()
.post(body)
.url(url)
.build();
// 4 发起请求获取响应值
Response response = client.newCall(request).execute();

// 5 根据响应结果判断
if (response.isSuccessful()) {
return response.body().string();
} else {
throw new RuntimeException("请求异常,错误码为: " + response.code());
}
} catch (Exception e) {
log.info("请求失败,错误信息为= {} ", e.getMessage());
}
return null;
}


/**
* 同步GET请求
*/
public static String getRequest(String url) throws IOException {
Request request = new Builder().url(url).build();
Response response = execute(request);

if (response.isSuccessful()) {
return response.body().string();
} else {
throw new ArithmeticException("请求异常,错误码为: " + response.code());
}

}

/**
* 同步请求
*/
public static Response execute(Request request) throws IOException {
return client.newCall(request).execute();
}

/**
* 开启异步线程访问网络, 需要返回结果
*/
public static void enqueue(Request request, Callback callback) {
client.newCall(request).enqueue(callback);
}

/**
* 开启异步线程访问网络,不需要返回结果( Callback 返回为空)
*/
public static void enqueue(Request request) {
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
log.info("请求失败,异常信息为: {} ", e.getMessage());
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response)
throws IOException {
log.info("请求成功");
}
});
}

}

参考资料:

blog.csdn.net/workingman_…

blog.csdn.net/weixin_4477…

blog.csdn.net/sinat_34241…

square.github.io/okhttp/3.x/…

本文转载自: 掘金

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

Redis 如何批量设置过期时间?PIPLINE的使用

发表于 2021-11-22

合理的使用缓存策略对开发同学来讲,就好像孙悟空习得自在极意功一般~

Redis如何批量设置过期时间呢?

不要说在foreach中通过set()函数批量设置过期时间

我们引入redis的PIPLINE,来解决批量设置过期时间的问题。

PIPLINE的原理是什么?

  1. 未使用pipline执行N条命令

image.png

  1. 使用pipline执行N条命令

image.png

通过图例可以很明显的看出来PIPLINE的原理:

客户端通过PIPLINE拼接子命令,只需要发送一次请求,在redis收到PIPLINE命令后,处理PIPLINE组成的命令块,减少了网络请求响应次数。

网络延迟越大PIPLINE的优势越能体现出来

拼接的子命令条数越多使用PIPLINE的优势越能体现出来

注意:并不是拼接的子命令越多越好,N值也有是上限的,当拼接命令过长时会导致客户端等待很长时间,造成网络堵塞;我们可以根据实际情况,把大批量命令拆分成几个PIPLINE执行。

代码封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
php复制代码//批量设置过期时间
public static function myPut(array $data, $ttl = 0)
{
if (empty($data)) {
return false;
}

$pipeline = Redis::connection('cache')
->multi(\Redis::PIPELINE);
foreach ($data as $key => $value) {
if (empty($value)) {
continue;
}
if ($ttl == 0) {
$pipeline->set(trim($key), $value);
} else {
$pipeline->set(trim($key), $value, $ttl);
}
}
$pipeline->exec();
}

项目实战

需求描述

  1. 打开APP,给喜欢我的人发送我的上线通知(为了避免打扰,8小时内重复登录不触发通知)
  2. 每个人每半小时只会收到一次这类上线通知(即半小时内就算我喜欢的1万人都上线了,我也只收到一次喜欢的人上线通知)

要点分析

  1. 合理使用缓存,减少DB读写次数
  2. 不仅要减少DB读写次数,也要减少Redis的读写次数,使用PIPLINE

代码实现解析

  1. canRecall() 写的比较优雅,先判断是否已发送的标记,再判断HouseOpen::getCurrentOpen(),因为HouseOpen::getCurrentOpen()是要查询DB计算的,这种代码要尽可能少的被执行到,减少DB查询。
  2. array_diff() 取差集的思路,获得需要推送的人

封装工具类

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
php复制代码<?php

namespace App\Model\House;

.
.
.

class HouseLikeRecallUser
{
protected $_userid = '';
protected $_availableUser = [];
protected $_recallFlagKey = '';

const TYPE_TTL_HOUSE_LIKE_RECALL = 60 * 30; //半小时后可以再次接收到喜欢的xxx进入通知
const TYPE_TTL_HOUSE_LIKE_RECALL_FLAG = 60 * 60 * 8; //8小时重复登录不触发

//初始化 传入setRecalled 的过期时间
public function __construct($userid)
{
$this->_userid = $userid;
//登录后给喜欢我的人推送校验:同一场次重复登录不重复发送
$this->_recallFlagKey = CacheKey::getCacheKey(CacheKey::TYPE_HOUSE_LIKE_RECALL_FLAG, $this->_userid);
}

//设置当前用户推送标示
public function setRecalled()
{
Cache::put($this->_recallFlagKey, 1, self::TYPE_TTL_HOUSE_LIKE_RECALL_FLAG);
}

//获取当前用户是否触发推送
public function canRecall()
{
$res = false;
if (empty(Cache::get($this->_recallFlagKey))) {
$houseOpen = HouseOpen::getCurrentOpen();
if ($houseOpen['status'] == HouseOpen::HOUSE_STATUS_OPEN) {
$res = true;
}
}
return $res;
}

//获取需要推送用户
public function getAvailableUser()
{
//获得最近喜欢我的用户
$recentLikeMeUser = UserRelationSingle::getLikeMeUserIds($this->_userid, 100, Utility::getBeforeNDayTimestamp(7));

//获得最近喜欢我的用户的 RECALL缓存标记
foreach ($recentLikeMeUser as $userid) {
$batchKey[] = CacheKey::getCacheKey(CacheKey::TYPE_HOUSE_LIKE_RECALL, $userid);
}

//获得最近喜欢我的且已经推送过的用户
$cacheData = [];
if (!empty($batchKey)) {
$cacheData = Redis::connection('cache')->mget($batchKey);
}

//计算最近喜欢我的用户 和 已经推送过的用户 的差集:就是需要推送的用户
$this->_availableUser = array_diff($recentLikeMeUser, $cacheData);
return $this->_availableUser;
}

//更新已经推送的用户
public function updateRecalledUser()
{
//批量更新差集用户
$recalledUser = [];
foreach ($this->_availableUser as $userid) {
$cacheKey = CacheKey::getCacheKey(CacheKey::TYPE_HOUSE_LIKE_RECALL, $userid);
$recalledUser[$cacheKey] = $userid;
}
//批量更新 设置过期时间
self::myPut($recalledUser, self::TYPE_TTL_HOUSE_LIKE_RECALL);
}

//批量设置过期时间
public static function myPut(array $data, $ttl = 0)
{
if (empty($data)) {
return false;
}

$pipeline = Redis::connection('cache')
->multi(\Redis::PIPELINE);
foreach ($data as $key => $value) {
if (empty($value)) {
continue;
}
if ($ttl == 0) {
$pipeline->set(trim($key), $value);
} else {
$pipeline->set(trim($key), $value, $ttl);
}
}
$pipeline->exec();
}
}

调用工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
php复制代码public function handle()
{
$userid = $this->_userid;
$houseLikeRecallUser = new HouseLikeRecallUser($userid);
if ($houseLikeRecallUser->canRecall()) {
$recallUserIds = $houseLikeRecallUser->getAvailableUser();
$houseLikeRecallUser->setRecalled();
$houseLikeRecallUser->updateRecalledUser();
//群发推送消息
.
.
.
}
}

总结

不同量级的数据需要不同的处理办法,减少网络请求次数,合理使用缓存,是性能优化的必经之路。

进一步思考

如果我喜欢的1万人同时上线(秒级并发),我只收到一个消息推送,要避免被通知轰炸,怎么解决这类并发问题呢?

小伙伴们有没有解决思路,可以在评论区讨论哦~

相关阅读推荐

性能优化反思:不要在for循环中操作DB

性能优化反思:不要在for循环中操作DB 进阶版

最后

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!

本文转载自: 掘金

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

设计模式之单例模式

发表于 2021-11-22

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战」

本篇文章是设计模式专题的第一篇文章,我会将遇到的设计模式都一一总结在该专题下,我会把自己对每一种设计模式的感悟写下来,以及在实际工作中我们该如何去灵活应用这些设计模式,欢迎大家关注。本篇文章我们就来讲一讲,应用最为广泛的单例模式。

单例模式的简单介绍

单例模式很简单,就是在整个系统运行过程中,保证类的实例只有一个。

单例模式的类图如下:

单例模式类图

单例模式的具体实现思路

  • 将构造函数私有化,防止从外部通过new实例化对象
  • 在类内部生成一个实例化对象
  • 通过public类型方法返回这个唯一的实例化对象

单例模式的具体实现方案

  • 饿汉模式,通过直接给static、final修饰的内部属性赋值,然后通过静态方法返回

该种方式可以说是一种“最优解”,它的代码最为简单、直观,而且能够保证线程安全,虽然说懒汉式能够优化饿汉式,让其在调用时才创建对象,而不是类装载时就创建对象,但是懒汉式就会带来线程不安全的问题,让人非常恼火,虽说有解决方案,但是不如饿汉式更加简单直观。懒汉式最要命的问题就是,一但通过类加载器加载,就会分配空间,如果没使用就会造成空间浪费,但是换个角度想象一下,我不使用它,我干嘛去将它装载到类加载器中。

它可能是工作中用到最多的单例写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码public class Singleton {
​
   // 用static、final修饰
   private static final Singleton INSTANCE = new Singleton();
   
   // 构造方法私有化
   private Singleton() {}
   
   // 静态方法返回该实例
   public static Singleton getInstance() {
       return INSTANCE;
  }
}

还有一些写法是不直接赋值,通过静态代码块赋值,效果是一样的。

  • 最安全解决方案,枚举单例

枚举单例可能用到的会很少,但是他是单例最安全的一种实现。它不仅能够保证线程安全性,还能防止反射破坏单例,防止序列化破坏单例,但是他也是饿汉式的。

1
2
3
4
5
6
arduino复制代码public enum Singleton {
​
   // 单例对象
   INSTANCE;
   // 其它方法  
}
  • 最完美解决方案,静态内部类 最推荐写法

通过静态内部类创建单例模式,不但是懒汉式的,而且它通过静态初始化类也是能够避免线程安全问题的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码public class Singleton {
​
   // 静态内部类
   private static class SingletonHolder {
       private static final Singleton INSTANCE = new Singleton();
  }
   
   // 构造方法私有化
   private Singleton() {}
   
   // 静态方法返回该实例
   public static Singleton getInstance() {
       return SingletonHolder.INSTANCE;
  }
}
  • 懒汉模式

懒汉模式虽然写法简单直观,但是带来严重的线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码public class Singleton {
​
   // 属性
   private static Singleton instance;
   
   // 构造方法私有化
   private Singleton() {}
   
   // 静态方法返回该实例
   public static Singleton getInstance() {
       if (instance == null) {
           instance = new Singleton();
      }
       return instance;
  }
}
  • DCL双重检测锁模式

复杂,不易理解,但是能够解决懒汉式,线程不安全的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
csharp复制代码public class Singleton {
​
   // volatile修饰,保证原子性
   private static volatile Singleton instance;
   
   // 构造方法私有化
   private Singleton() {}
   
   // 静态方法返回该实例
   public static Singleton getInstance() {
       if (instance == null) {
           synchronized (Singleton.class) {
               if (instance == null) {
          instance = new Singleton();
      }
          }
      }
       return instance;
  }
}

双重检测锁模式,还可以借助一个开关,未创建对象时打开,创建完成关闭,这样就能在一定程度上防止反射破坏单例,因为反射虽然可以忽略private,但是它并不清楚开关具体是如何实现的,字段名,开关值都不知道。

可以这么说,双重检测锁虽然是单例中相对完美的实现,但是它的代码不易理解,并不是一种比较好的写法。

单例模式的优缺点

优点:

  • 在单例模式中,活动的实例只有一个,对单例类的所有实例化得到的都是相同的一个实例。这样就可以防止其它对象对单例类的实例化,确保所有的对象都访问同一个实例
  • 提供了对唯一实例的受控访问。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
  • 避免对共享资源的多重占用。

缺点:

  • 不适用于变化的对象,如果同一类型的对象总是要在不同的场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  • 由于单利模式没有抽象层,因此单例类的扩展很困难。
  • 单例类的职责过重,在一定程度上违背了单一职责原则。

单例模式的适用场景

适用场景:

  1. 需要频繁实例化然后销毁的对象。
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  3. 不怎么发生变化,但又经常被用到。
  4. 有状态的工具类对象。
  5. 频繁访问数据库或文件的对象。

举例:

  • 各种Manager,一般管理器都只有一个实例,如Windows的任务管理器
  • 计数器
  • 应用的配置,系统的配置
  • 共享的资源
  • 各种池化技术,数据库连接池,线程池…
  • …

单例模式总结

通过对单例的学习,我最大的收获是,我们在使用设计模式是不要一味去追求最优的实现,能解决我们项目中的问题,并且更容易被其他人所接受,才是最好的实现。单例模式比较完备的实现方案就是我文中提到的这五种,最推荐大家使用的就是静态内部类的方式,次推荐的就是枚举和饿汉式,我认为一般的项目饿汉式带来的内存损耗微乎其微,但是如果出现并发,致使单例不再是单例,这样是我所不能接受的。双重检测锁模式,虽然也相对完美,但是写法复杂不易理解,如果忘记volatile修饰,也是无法保证线程安全问题,所以我也不是很推荐双重检测这种写法。

本文转载自: 掘金

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

还在CRUD吗? 给你一招一剑封喉,彻底摆脱简单的增删改查

发表于 2021-11-22
1
bash复制代码小明接了个项目,业务方的需求里面有很多组织管理、人员管理、事件管理的需求,这些业务有面向终端客户的查询,也有面向内部管理人员的管理。需求的复杂度不高,很多都是表单的增删改查,但架不住需求点太多,小明还是得加班加点的当搬砖码农。其实,针对这类场景,有很多公司尝试使用打破分层界限的编码工具,比如宇宙第一编码语言PHP、ruby/rails等,方便快捷,奈何业务复杂了以后,都得慢慢迁移到企业级开发语言java上。也有公司,开始尝试低代码平台,通过简单的托拉拽就完成后台管理界面的开发,奈何,现在的低代码开发还真只能胜任上面需求中的增删改查。

image.png

1
2
3
4
arduino复制代码有没有一种办法,在java的企业级开发中,既能满足复杂的需求开发;又可以快速的完成简单的增删改查(有一定的业务逻辑),同时能保证技术的统一性和延续性呢? fluent-mybatis在完成了对mybatis封装,实现了在java代码中使用流式语言,满足条件设置、复杂关联、嵌套、union、多数据库支持、个性化扩展等便捷能力外。现在又推出了,专门面向表单级的增删改查,声明即实现的一剑封喉的能力。
呵呵呵,作者吹牛不用上税啊,我们来看个简单的例子:

我们定义一个spring rest api接口如下
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
java复制代码@RestController
@FormService(table = "student")
public interface StudentQueryApi {
@PostMapping("/student")
Student findStudentBy(@RequestBody StudentQuery student);

@PostMapping("/listStudentBy")
List<Student> listStudentBy(@RequestBody StudentQuery student);
}

@Data
@Accessors(chain = true)
public class StudentQuery implements Serializable {
private String userName;

@Entry(type = EntryType.LikeLeft)
private String address;

@Entry(type = Between)
private Integer[] age;

private Integer gender;
/**
* 默认正序
*/
@Entry(type = EntryType.OrderBy, value = "userName")
private boolean byUserName = true;
/**
* 默认倒序
*/
@Entry(type = EntryType.OrderBy, value = "age")
private boolean byAge;
}

@Data
@Accessors(chain = true)
public class Student implements Serializable {

private String userName;

private String status;

private String phone;

@Entry("email")
private String hisEmail;

private Integer age;

private String address;
}

然后用@FormServiceScan注解把API路径加入到spring configuration中, 用法和mybatis的@MapperScan类似

1
2
3
4
java复制代码@FormServiceScan({"你定义api的package路径"})
public class SpringConfig {
// 你其它bean配置
}

很简单,虽然你仅仅是定义了一个接口,没有写一行实现代码,你已经完整的实现了一个查询逻辑。查询入参包括了,相等条件,between条件,like条件和排序设置。

启动springboot应用,我们用rest client调用一下看看

image.png

是不是很简单,声明即实现,对于单表的CRUD,产品画完原型,开发也基本实现完了,form-service功能也算一种低代码实现框架了。

下面我们重点将一下form-service的核心概念

  • @FormService, 定义在接口类上, 表明该接口是个FormService接口
  • @FormServiceScan, 定义在Spring的@Configuration类上,功能类似Mybatis的@MapperScan,用来扫描所有的@FormService接口
  • @FormMethod,可选,定义在Service方法上,如果是插入或更新方法,必须声明,如果是查询方法,无需声明
  • @Entry,可选,定义在表单字段上,在下列场景下需要显式定义@Entry
    1. 条件字段,条件不是相等,而是大于,小于,between等
    2. 表单字段名称和Entity字段名称不一致
    3. 分页条件字段(页码,每页数量)
    4. 排序字段
    5. 更新字段

同时,在接口和属性上,除了可以定义form-service的注解外,还需要有下面增强

  • 还可以结合spring rest注解 @RestController+@PostMapping,直接将接口暴露为rest api
  • 结合javax.validate注解,实现入参的合法性校验

当然,以上所有的功能都是基于你已经按照FluentMybatis的规范生成了数据库Entity类。
fluent mybatis代码生成

关联查询和 1 + N问题解决

但还不是fluent-mybatis FormService的所有功能,FormService还可以通过简单的声明,解决1对1和1对N的关联查询,同时解决在查询列表时,引起的1+N查询问题,而这所有的一切,都不需要你编码, 有一定编码经验的同学,就问你这样 爽不爽!!!

image.png

  • 生成fluent mybatis Entity类时,通过@Relation注解定义1:1, 1:N的关联关系
    比如这样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码    @Tables(url = URL, username = "root", password = "password",
srcDir = SrcDir, testDir = TestDir, basePack = BasePack + 2,
gmtCreated = "gmt_created", gmtModified = "gmt_modified", logicDeleted = "is_deleted",
tables = {
@Table(value = {"student", "student_score"},
columns = @Column(value = "version", isLarge = true)
),
relations = {
@Relation(method = "findDeskMate", type = RelationType.OneWay_0_1,
source = "student", target = "student", where = "id=desk_mate_id"),
@Relation(source = "student", target = "student_score", type = RelationType.TwoWay_1_N,
where = "id=student_id")
})
static class RelationDef1 {
}
1
2
markdown复制代码1. Student和同桌是1 vs 1的关系
2. Student和成绩Score是 1 vs N关系

上面的代码生成的Entity类如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@FluentMybatis(
table = "student",
schema = "fluent_mybatis"
)
public class StudentEntity extends RichEntity {
// ... 省略属性定义

/**
* 1对1关联查询方法
*/
@RefMethod("deskMateId = id")
public StudentEntity findDeskMate() {
return super.invoke("findDeskMate", true);
}

/**
* 1对多关联查询方法
*/
@RefMethod("studentId = id")
public List<StudentScoreEntity> findStudentScoreList() {
return super.invoke("findStudentScoreList", true);
}
}
  • 在查询接口返回值中定义关联对象,比如下面这样
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
java复制代码@Data
@Accessors(chain = true)
public class Student {
private Long id;

private String userName;

private String status;

private String phone;

@Entry("email")
private String hisEmail;

private Integer age;

private String address;
/**
* 同桌对象, 对应 findDeskMate方法
*/
private Student deskMate;
/**
* 分数列表, 属性名称是scores, 无法按规则和findStudentScoreList对应
* 需在 @Entry 注解中显式指定关联方法
*/
@Entry(value = "findStudentScoreList")
private List<Score> scores;
}

现在我们重新执行 listStudentBy 的测试方法, 看一下结果情况

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
java复制代码@BeforeEach
void setup() {
// 准备2个学生的数据,互为同桌
ATM.dataMap.student.table(2)
.env.values("test_env")
.userName.values("li ming", "xiao qiang")
.age.values(23, 34)
.email.values("xxx@test")
.address.values("hangzhou binjiang")
.deskMateId.values(2, 1)
.cleanAndInsert();
// 准备3条成绩数据,liming有二门成绩,小强一门成绩
ATM.dataMap.studentScore.table(3)
.env.values("test_env")
.studentId.values(1, 1, 2)
.subject.values("yuwen", "english")
.score.values(79, 67, 98)
.cleanAndInsert();
}

@Test
void listEntity() {
List<Student> students = service.listStudentBy(new StudentQuery()
.setAddress("hangzhou")
.setAge(new Integer[]{20, 40}));

// 验证列表数据
want.object(students).eqDataMap(
ATM.dataMap.student.entity(2)
.userName.values("li ming", "xiao qiang")
.age.values(23, 34)
.address.values("hangzhou binjiang")
.kv("hisEmail", "xxx@test")
);
/* 同桌的同桌 = 自己 */
want.object(students.get(0)).eqReflect(students.get(1).getDeskMate(), EqMode.IGNORE_DEFAULTS);
/* 验证成绩列表 */
want.object(students.get(0).getScores()).eqDataMap(new DataMap(2)
.kv("score", 79, 67)
.kv("subject", "yuwen", "english"));
want.object(students.get(1).getScores()).eqDataMap(new DataMap(1)
.kv("score", 98)
.kv("subject", "english"));
}

接着我观察一下控制台打印出来的sql语句, 总共执行了3条SQL语句

  1. 查询学生列表
  2. 查询学生的同桌, 条件 desk_mate_id IN (?, ?)
  3. 查询学生的成绩, 条件 student_id IN (?, ?)
1
2
3
4
5
6
7
8
9
shell复制代码==>  Preparing: SELECT `id`, `...省略字段` FROM `student` WHERE `is_deleted` = ? AND `env` = ? AND `address` LIKE ? AND `age` BETWEEN ? AND ? ORDER BY `user_name` ASC, `age` DESC 
==> Parameters: false(Boolean), test_env(String), hangzhou%(String), 20(Integer), 40(Integer)
<== Total: 2
==> Preparing: SELECT `id`, `...省略字段` FROM `student` WHERE `is_deleted` = ? AND `env` = ? AND `desk_mate_id` IN (?, ?)
==> Parameters: false(Boolean), test_env(String), 1(Long), 2(Long)
<== Total: 2
==> Preparing: SELECT `id`, `...省略字段` FROM `student_score` WHERE `is_deleted` = ? AND `env` = ? AND `student_id` IN (?, ?)
==> Parameters: false(Boolean), test_env(String), 1(Long), 2(Long)
<== Total: 3

通过,上面观察,框架完美的解决了1+N查询的问题

image.png

框架使用

maven引用, 详细的使用文档请参考 fluent mybatis文档

1
2
3
4
5
6
7
8
9
10
11
xml复制代码    <dependency>
<groupId>com.github.atool</groupId>
<artifactId>fluent-mybatis</artifactId>
<version>1.9.3</version>
</dependency>
<dependency>
<groupId>com.github.atool</groupId>
<artifactId>fluent-mybatis-processor</artifactId>
<version>1.9.3</version>
<scope>provided</scope>
</dependency>

完整的例子见 fluent mybatis

本文转载自: 掘金

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

力扣第103题-二叉树的锯齿形层序遍历 前言 一、思路 二、

发表于 2021-11-22

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战」

前言

力扣第103题 二叉树的锯齿形层序遍历 如下所示:

给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

例如:给定二叉树 [3,9,20,null,null,15,7]

1
2
3
4
5
markdown复制代码    3
/ \
9 20
/ \
15 7

返回锯齿形层序遍历如下:

1
2
3
4
5
csharp复制代码[
[3],
[20,9],
[15,7]
]

一、思路

题目意思很简单,需要返回锯齿遍历的结果。

锯齿遍历是指从 第一层从右至左,第二层从左至右(根节点可忽略)。为了更好的理解这一行为,具体的路径如下图所示:

image.png

上图中 锯齿遍历 的结果为:[[1], [3,2],[4,5,6,7]]

这一题刚开始做的时候还是有点绕的,不过只要画个图模拟一下遍历的顺序就会清晰很多了。我们还是用上图中的树作为例子,用来分析。

在观察了第二层的节点遍历顺序和第三层节点的遍历顺序后,不难发现:当前层最先遍历到的节点的子节点,是下一层最后遍历的节点

例如第二层中的节点 3 是第二层最先遍历的。但是它的左右孩子子节点 6 和 7 是第三层最后面遍历的。

还有一点就是:在同一层中,如果当前层数是偶数,则右节点优先遍历。如果当前层数为奇数,则左节点优先遍历
(例如第三层中的 6 是要先于 7 遍历的)

综上所述,基于这种先进后出的结构,我们可以使用 栈 来保存遍历的节点,正好可以对应上当前层中先遍历,下一层中后遍历。

举个例子

我们以 [3,9,20,null,null,15,7] 来举例,重点讲述一下 栈 中存储元素

Deque<TreeNode> currentLevel:当前层节点

Deque<TreeNode> nextLevel:下一层节点

  1. 遍历 第一层 节点 [3],先将它的左孩子 9 入栈,再将右孩子 20 入栈。nextLevel 栈中为 9 -> 20
  2. 遍历第二层,currentLevel 栈顶一直出栈。第一个节点为 20,将它的右孩子 7 入 nextLevel 栈,再将它的右孩子 15 入 nextLevel 栈。第二个节点为 9,没有左右孩子故不入栈。nextLevel 栈中为 7 -> 15
  3. 遍历第三层,currentLevel出栈。第一个节点为 15,第二个节点为 7,都没有左右孩子,故不入栈。
  4. 最终返回遍历的各节点值 [[3], [20, 9], [15, 7]] 即可

二、实现

实现代码

注意:因为考虑到不特殊处理根节点,所以我们认为根节点为第 0 层。所以奇偶性判断与思路中不是一致的

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
java复制代码    List<List<Integer>> ret = new ArrayList<>();

public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
if (root!=null) {
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
dfs(stack);
}
return ret;
}

public void dfs(Deque<TreeNode> currentLevel) {
if (currentLevel.size() < 1)
return;
List<Integer> temp = new ArrayList<>();
Deque<TreeNode> nextLevel = new LinkedList<>();
// 遍历当前层级
while (!currentLevel.isEmpty()){
TreeNode treeNode = currentLevel.pop();
temp.add(treeNode.val);
// 因为需要添加下一层,所以是 size + 1
if((ret.size() + 1) % 2 == 1) { // 奇数层:先加左边,再加右边
if (treeNode.left != null){
nextLevel.push(treeNode.left);
}
if (treeNode.right != null){
nextLevel.push(treeNode.right);
}
} else { // 偶数层:先加右边,再加左边
if (treeNode.right != null){
nextLevel.push(treeNode.right);
}
if (treeNode.left != null){
nextLevel.push(treeNode.left);
}
}
}
ret.add(temp);
dfs(nextLevel);
}

测试代码

1
2
3
4
5
6
java复制代码public static void main(String[] args) {
TreeNode treeNode = new TreeNode(1,
new TreeNode(2,new TreeNode(4), null),
new TreeNode(3, null, new TreeNode(5)));
new Number103().zigzagLevelOrder(treeNode);
}

结果

image.png

三、总结

感谢看到最后,非常荣幸能够帮助到你~♥

如果你觉得我写的还不错的话,不妨给我点个赞吧!如有疑问,也可评论区见~

本文转载自: 掘金

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

NLP 中文形近字相似度算法开源实现 项目简介 变更日志 快

发表于 2021-11-22

项目简介

nlp-hanzi-similar 为汉字提供相似性的计算。

在这里插入图片描述

创作目的

有一个小伙伴说自己在做语言认知科学方向的课题研究,看了我以前写的 NLP 中文形近字相似度计算思路

就想问下有没有源码或者相关资料。

国内对于文本的相似度计算,开源的工具是比较丰富的。

但是对于两个汉字之间的相似度计算,国内基本一片空白。国内的参考的资料少的可怜,国外相关文档也是如此。

于是将以前写的相似度算法整理开源,希望能帮到这位小伙伴。

本项目旨在抛砖引玉,实现一个基本的相似度计算工具,为汉字 NLP 贡献一点绵薄之力。

特性

  • fluent 方法,一行代码搞定一切
  • 高度自定义,允许用户定义自己的实现
  • 词库自定义,适应各种应用场景
  • 丰富的实现策略

默认实现了基于 四角编码+拼音+汉字结构+汉字偏旁+笔画数 的相似度比较。

变更日志

变更日志

快速开始

需要

jdk1.7+

maven 3.x+

maven 引入

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>nlp-hanzi-similar</artifactId>
<version>1.0.0</version>
</dependency>

快速开始

基本用法

HanziSimilarHelper.similar 获取两个汉字的相似度。

1
java复制代码double rate1 = HanziSimilarHelper.similar('末', '未');

结果为:

1
复制代码0.9629629629629629

自定义权重

默认是根据 四角编码+拼音+汉字结构+汉字偏旁+笔画数 进行相似度比较。

如果默认的系统权重无法满足你的需求,你可以通过自定义权重调整:

1
2
3
4
5
6
7
java复制代码double rate = HanziSimilarBs.newInstance()
.jiegouRate(10)
.sijiaoRate(8)
.bushouRate(6)
.bihuashuRate(2)
.pinyinRate(1)
.similar('末', '未');

自定义相似度

有些情况下,系统的计算是无法满足的。

用户可以在根目录下 hanzi_similar_define.txt 进行自定义。

1
2
复制代码入人 0.9
人入 0.9

这样在计算 人 和 入 的相似度时,会优先以用户自定义的为准。

1
java复制代码double rate = HanziSimilarHelper.similar('人', '入');

此时的结果为用户自定义的值。

引导类

说明

为了便于用户自定义,HanziSimilarBs 支持用户进行自定义配。

HanziSimilarBs 中允许自定义的配置列表如下:

序号 属性 说明
1 bihuashuRate 笔画数权重
2 bihuashuData 笔画数数据
3 bihuashuSimilar 笔画数相似度策略
4 jiegouRate 结构权重
5 jiegouData 结构数据
6 jiegouSimilar 结构相似度策略
7 bushouRate 部首权重
8 bushouData 部首数据
9 bushouSimilar 部首相似度策略
10 sijiaoRate 四角编码权重
12 sijiaoData 四角编码数据
13 sijiaoSimilar 四角编码相似度策略
14 pinyinRate 拼音权重
15 pinyinData 拼音数据
16 pinyinSimilar 拼音相似度策略
17 hanziSimilar 汉字相似度核心策略
18 userDefineData 用户自定义数据

所有的配置都可以基于接口,用户进行自定义。

快速体验

说明

如果 java 语言不是你的主要开发语言,你可以通过下面的 exe 文件快速体验一下。

下载地址

github.com/houbb/nlp-h…

下载后直接解压得到 hanzi-similar.exe 免安装的可执行文件。

执行效果

界面是使用 java swing 实现的,所以美观什么的,已经完全放弃治疗 T_T。

使用 exe4j 打包。

字符一输入一个汉字,字符二输入另一个汉字,点击计算,则可以获取对应的相似度。

在这里插入图片描述

字典的弊端

这个项目开源,是因为有一位小伙伴有相关的需求,但是他不懂 java。

一开始想把项目设计成为字典的形式,两个字对应一个相似度。

但是有一个问题,2W 汉字,和 2W 汉字的相似度字典,数量已经是近亿的数据量。

空间复杂度过高,同时会导致时间复杂度问题。

所以目前采用的是实时计算,有时间做一下其他语言的迁移 :)

实现原理

实现思路

不同于文本相似度,汉字相似度的单位是汉字。

所以相似度是对于汉字的拆解,比如笔画,拼音,部首,结构等。

推荐阅读:

NLP 中文形近字相似度计算思路

计算思路描述了实现的原理,但是小伙伴反应不会实现,于是才有了本项目。

核心代码

核心实现如下,就是各种相似度,进行加权计算。

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
java复制代码/**
* 相似度
*
* @param context 上下文
* @return 结果
* @since 1.0.0
*/
@Override
public double similar(final IHanziSimilarContext context) {
final String charOne = context.charOne();
final String charTwo = context.charTwo();

//1. 是否相同
if(charOne.equals(charTwo)) {
return 1.0;
}

//2. 是否用户自定义
Map<String, Double> defineMap = context.userDefineData().dataMap();
String defineKey = charOne+charTwo;
if(defineMap.containsKey(defineKey)) {
return defineMap.get(defineKey);
}

//3. 通过权重计算获取
//3.1 四角编码
IHanziSimilar sijiaoSimilar = context.sijiaoSimilar();
double sijiaoScore = sijiaoSimilar.similar(context);

//3.2 结构
IHanziSimilar jiegouSimilar = context.jiegouSimilar();
double jiegouScore = jiegouSimilar.similar(context);

//3.3 部首
IHanziSimilar bushouSimilar = context.bushouSimilar();
double bushouScore = bushouSimilar.similar(context);

//3.4 笔画
IHanziSimilar biahuashuSimilar = context.bihuashuSimilar();
double bihuashuScore = biahuashuSimilar.similar(context);

//3.5 拼音
IHanziSimilar pinyinSimilar = context.pinyinSimilar();
double pinyinScore = pinyinSimilar.similar(context);

//4. 计算总分
double totalScore = sijiaoScore + jiegouScore + bushouScore + bihuashuScore + pinyinScore;
//4.1 避免浮点数比较问题
if(totalScore <= 0) {
return 0;
}

//4.2 正则化
double limitScore = context.sijiaoRate() + context.jiegouRate()
+ context.bushouRate() + context.bihuashuRate() + context.pinyinRate();

return totalScore / limitScore;
}

具体的细节,如果感兴趣,可以自行阅读源码。

开源地址

为了便于大家的学习和使用,本项目已开源。

开源地址:

github.com/houbb/nlp-h…

欢迎大家,fork&star 鼓励一下老马~

算法的优缺点

优点

为数不多的几篇 paper 是从汉字的结构入手的。

本算法引入了四角编码+结构+部首+笔画+拼音的方式,使其更加符合国内的使用直觉。

缺点

部首这部分因为当时数据问题,实际上是有缺憾的。

后续准备引入拆字字典,对汉字的所有组成部分进行对比,而不是目前一个简单的部首。

后期 Road-MAP

  • 丰富相似度策略
  • 优化默认权重
  • 优化 exe 界面

NO_SIGN.png

本文转载自: 掘金

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

一题学会链表的拆分,翻转,合并 题目 链表拆分 链表翻转 合

发表于 2021-11-22

题目

先看题目,给定一个链表,奇数项为递增序列,偶数项为递减序列,如图:

image.png
要求将链表按从小到大进行排序。

这个题目看上去可能就是一个简单的链表,但是要怎么做到将链表按照升序的顺序进行排序。这里我们可以用三步,可能比较繁琐,但是经过这三步的过程,我们会对链表的一些常规操作有一个很清晰的认识和理解,这三个步骤就是:

  1. 链表拆分,先将整个链表拆为两个部分,升序子链表和降序子链表,也就是将下标为奇数和偶数的元素分别放到两个链表中
  2. 链表翻转,将下标为偶数链表元素组成的新的子链表进行翻转,使其按从小到大排列
  3. 链表合并,前面两个步骤得到了两个从小到大的链表,现在只需要将这两个链表进行合并
    Node的定义(为了减少代码,用了lombok):
1
2
3
4
5
6
7
8
9
10
11
less复制代码@Data
@NoArgsConstructor
@AllArgsConstructor
public class Node {
public int val;
public Node next;

public Node(int anInt) {
this.val = anInt;
}
}

链表拆分

image.png

image.png

这个过程比较简单,就按下标的奇偶性进行拆分就行了,代码如下:

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
ini复制代码public static void main(String[] args) {
/**
* 拆分链表,注意最后一项的处理
*/
Node head = new Node(1, new Node(9, new Node(2, new Node(8, new Node(3, new Node(7, new Node(
4, new Node(6, new Node(5, new Node(5, new Node(6, new Node(4))))))))))));

//为了后面代码的统一,建了两个虚拟头结点
Node node0 = new Node(-1, head);
Node node1 = new Node(-1, head.next);

Node head0 = node0;
Node head1 = node1;
int i = 0;

while (head != null) {
if (i % 2 == 0) {
node0.next = head;
node0 = node0.next;
} else {
node1.next = head;
node1 = node1.next;
}
head = head.next;
i++;
}
//循环结束之后这里要注意 处理一下,不然总会有一个子链表多一个元素
if (i % 2 == 0) {
node0.next = null;
} else {
node1.next = null;
}
System.out.println(head0.next);
System.out.println(head1.next);

head0.next和head1.next就是拆分出来的两个子链表了
输出结果如下:

image.png

链表翻转

翻转的过程主要的思想是使用双指针,一前一后两个指针来遍历链表并且改变next指针的方向,这里并没有另外创建一个新的链表来实现翻转,整个过程的流程如下动图,不明白的话,更详细的的步骤可以参考文末的参考文章。

640.gif
有动图我就直接上代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码private static Node reverse(Node head) {
Node cur = head;
Node pre = null;
Node tmp;
while (cur != null) {
tmp = cur.next;
cur.next = pre;

pre = cur;
cur = tmp;
}
return pre;
}

合并链表

力扣有一个剑指 Offer类似的题目可以参考一下:合并有序链表难度为简单。可以参考这一篇题解:题解
其实整个过程就是先遍历一个链表,遍历的时候和另一个链表当前的头结点的大小比较谁的值更小就取谁的值,然后将头结点指向下一个节点,如果另一个链表元素还有剩下的,说明都是比最后一个元素更大的,直接拼到后面就可以了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码private static Node merge(Node node0, Node node1){
Node node = new Node(0);
Node head = node;
while (node0 !=null){
if (node1 != null && node0.val <= node1.val){
node.next = node0;
node0 = node0.next;
}else {
if (node1 != null){
node.next = node1;
node1 = node1.next;
}
}
node = node.next;
}
if (node1 != null){
node.next = node1;
}
return head.next;
}

完整代码

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
ini复制代码public class ReSort {
public static void main(String[] args) {
/**
* 拆分链表,注意最后一项的处理
*/
Node head = new Node(1, new Node(9, new Node(2, new Node(8, new Node(3, new Node(7, new Node(
4, new Node(6, new Node(5, new Node(5, new Node(6, new Node(4))))))))))));

Node node0 = new Node(0, head);
Node node1 = new Node(0, head.next);

Node head0 = node0;
Node head1 = node1;
int i = 0;

while (head != null) {
if (i % 2 == 0) {
node0.next = head;
node0 = node0.next;
} else {
node1.next = head;
node1 = node1.next;
}
head = head.next;
i++;
}
if (i % 2 == 0) {
node0.next = null;
} else {
node1.next = null;
}
System.out.println(head0.next);
System.out.println(head1.next);

Node reverse = reverse(head1.next);
Node merge = merge(head0.next, reverse);
System.out.println(merge);


}


/**
* 双指针法实现链表翻转
* @param head
* @return
*/
private static Node reverse(Node head) {
Node cur = head;
Node pre = null;
Node tmp;
while (cur != null) {
tmp = cur.next;
cur.next = pre;

pre = cur;
cur = tmp;
}
return pre;
}

//合并链表
private static Node merge(Node node0, Node node1){
Node node = new Node(0);
Node head = node;
while (node0 !=null){
if (node1 != null && node0.val <= node1.val){
node.next = node0;
node0 = node0.next;
}else {
if (node1 != null){
node.next = node1;
node1 = node1.next;
}
}
node = node.next;
}
if (node1 != null){
node.next = node1;
}
return head.next;
}


}

这种解法可能比较繁琐,代码量也比较多,但是思路很清晰,也涉及了多种链表的操作。

【参考】

【1】mp.weixin.qq.com/s/pnvVP-0ZM…

【2】力扣

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

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

1…231232233…956

开发者博客

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