分类 fish 下的文章

新项目使用了 Reids 的过期监听来执行延时任务,在不搭建 redis 高可用集群的情况下,还是想尽量保证 Redis 数据不丢失,于是就打算把 Redis 的持久化配置上,研究了下相关资料,这里做下记录:

在配置 Redis 的持久化时,通常会选择 RDB(Redis Database)快照和 AOF(Append Only File)日志的组合,以兼顾性能和数据持久性。以下是一个合理的混合持久化配置示例:

1. RDB 配置

RDB 是 Redis 的快照持久化机制,它会在特定时间间隔内保存数据快照到磁盘。RDB 配置如下:

# RDB 持久化间隔配置(在满足条件时生成快照)
save 900 1  # 900 秒(15 分钟)内至少有 1 个键被修改
save 300 10 # 300 秒(5 分钟)内至少有 10 个键被修改
save 60 10000 # 60 秒内至少有 10000 个键被修改

# RDB 文件名称(可选)
dbfilename dump.rdb

# RDB 文件保存路径(可选)
dir /var/lib/redis

2. AOF 配置

AOF 是 Redis 的追加日志持久化机制,它会记录每个写操作,以便在重启时重放这些操作。AOF 配置如下:

# 启用 AOF 持久化
appendonly yes

# AOF 文件名称
appendfilename "appendonly.aof"

# AOF 写入策略
appendfsync everysec # 每秒同步一次

# AOF 重写配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

3. 混合持久化策略

结合 RDB 和 AOF 的优点,可以配置如下:

# 混合持久化策略
aof-use-rdb-preamble yes

启用 aof-use-rdb-preamble 选项后,AOF 文件在重写时会包含一个 RDB 预备文件,这样可以加快重启速度,并减少 AOF 文件的大小。

4. 完整配置示例

# RDB 配置
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir /var/lib/redis

# AOF 配置
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 混合持久化策略
aof-use-rdb-preamble yes

JVM 就是 java 虚拟机,我们可以把它理解成一个操作系统,每个不同的平台都有不同的 JVM,比如 Linux 系统和 Windows 系统,就是因为这个原因所以 Java 程序就有了一个很突出的特性就是 跨平台性

:在jvm中栈用来存储一些对象的引用、局部变量以及计算过程的中间数据,在方法退出后那么这些变量也会被销毁。它的存储比堆快得多,只比 CPU 里的寄存器慢

:用来存储程序中的一些对象,比如你用 new 关键字创建的对象,它就会被存储在堆内存中,但是这个对象在堆内存中的首地址会存储在栈中。

栈内存在 JVM 中默认是 1M,可以通过下面的参数进行设置:

-Xss

最小堆内存在 JVM 中默认物理内存的 64 分之 1,最大堆内存在 JVM 中默认物理内存 4 分之一,且建议最大堆内存不大于 4G,并且设置 -Xms=-Xmx 避免每次 GC 后,调整堆的大小,减少系统内存分配开销:

-Xms  
-Xmx

在 jvm 的堆内存中有三个区域

1.年轻代:用于存放新产生的对象。
2.老年代:用于存放被长期引用的对象。
3.持久带:用于存放 Class,method元信息。

JVM 内存垃圾收集算法

  • 引用计数器的方法
    类似于 Objective-C 的内存回收,在 OC 语言不使用 arc 机制时,在创建对象,且被引用时候也会对计数器加 1,使用完成后会调用 release 方法就会对计数器减 1

jvm 的这个算法也和上面的机制类似,但是他不能解决对象循环引用问题,也不好解决精确的计算,因为 java 程序开发,内存回收是对我们透明的,而 OC 是在代码层面自己去手动控制

  • 根搜索算法

有向图算法,从 GC Roots 开始向下面搜索,搜索走过的路径叫做引用链接,当一个对象到达 GC Roots 没有任何引用链时,则这个对象就是不可达对象,就可以被回收,但是回收的时候会筛查出覆盖了 finalze() 方法且该对象 finalze() 方法没有被虚拟机调用过放入 F-Queue 队列然后执行 F-Queue 队列中对象的 finalze() 方法后如果该对象重新与某个 GC Roots 对象相关联,那么会将该对象从回收队列中移除

比如 jvm 中栈中指向堆中对象的指针就可以理解成一种 GC Roots,因为栈中保存的是对象的指针指向的是堆中对象的首地址,当栈中的指针没有了那么堆中的对象就是不可达对象,就会被GC回收

jvm内存垃圾回收算法

  • 复制算法 (用于新生代)

用于新生代从 Eden 区到 survivor0 或者 survivor1 移动的时候

从根集合扫描如果对象被引用,就会被 copy 到 survivor0 或者 survivor1,然后剩下的都是不可达对象,就可以被回收掉,然后 S0 或者 S1 的对象就会到老年代中,在存活对象比较少的时候很高效并且不会产生内存碎片,就是内存需要额外划分一块 survivor 区出来

  • 标记清除算法 (用于老年代)

就是使用根搜索算法扫描,如果可达就标记,当扫描完成后,就对未标记的对象进行回收,它不需要像复制算法一样,需要一个新的内存区,而且不会对对象进行 copy,这种对对象存活的多的情况下很高效,但是这样会产生内存碎片

  • 标记整理压缩算法 (用于老年代)

就是在标记清除算法之后对内存碎片进行整理,只是他会对没有清理的对象进行移动,代价高

1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,

如:select id from t where num is null可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:select id from t where num=0

3.应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。

4.应尽量避免在 where 子句中使用or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,

如:select id from t where num=10 or num=20可以这样查询:select id from t where num=10 union all select id from t where num=20

5.in 和 not in 也要慎用,否则会导致全表扫描,

如:select id from t where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了:select id from t where num between 1 and 3

6.下面的查询也将导致全表扫描:select id from t where name like '李%'若要提高效率,可以考虑全文检索。

7..避免在索引列上使用计算,也就是说,应尽量避免在 where 子句中对字段进行表达式操作和函数操作,这将导致引擎放弃使用索引而进行全表扫描。

如:select id from t where num/2=100应改为:select id from t where num=100*2

select id from t where substring(name,1,3)='abc' ,name以abc开头的id,应改为:select id from t where name like 'abc%'

8.很多时候用 exists 代替 in 是一个好的选择:exists用于检查子查询是否至少会返回一行数据,该子查询实际上并不返回任何数据,而是返回值true或false。

select num from a where num in(select num from b)

用下面的语句替换:select num from a where exists (select 1 from b where num=a.num)

9.任何地方都不要使用 select from t ,用具体的字段列表代替“”,不要返回用不到的任何字段。

10.用>=替代>

高效: SELECT * FROM EMP WHERE DEPTNO >=4

低效: SELECT * FROM EMP WHERE DEPTNO >3

两者的区别在于, 前者DBMS将直接跳到第一个DEPT等于4的记录,而后者将首先定位到DEPTNO=3的记录并且向前扫描到第一个DEPT大于3的记录。

11.用Where子句替换having子句

问题出现在前司的项目上,虽然已经离职一年多了,但有问题经常会找到我帮忙处理,并支付一点辛苦费。

项目是采用 springCloud 微服务创建的,目前有五六个服务分布在几台服务器上。

这次的问题现象是,用户创建订单后,会莫名其妙自动取消,明白问题后,首先检查该问题订单的服务日志,日志一切正常,正常下单,正常支付,正常修改订单状态。

然后检查代码,发现在另一个服务上,有一个定时任务,采用 spring 的定时任务,通过时间轮来每分钟读取数据库,获取超过设定的事件后还没有支付的订单,然后取消,而这个业务的自动取消设置的未支付 3 分钟后自动取消。顺便提一下,我感觉这种每分钟读取一次数据库来取消订单的做法有点 low,当时在职的时候想去优化一下,但一直没时间,直到拖了我三个月工资后,我提桶跑路(代码和人有一个能跑就行)。

然而这个跑定时任务的服务在另一台服务器上,检查了定时任务的日志,发现定时任务读取未支付订单时,把这单已经支付了的订单给读取出来了,我觉得很不可思议,数据库里检查了一下订单支付时间和订单的取消时间,竟然相差 3 分钟,也就是说,这个订单在支付完成三分钟后,定时任务读取未支付订单把它读取出来了,并做了取消订单的操作。

我又检查了几遍日志和代码,发现代码确实没有问题,订单被取消前,也没有其他的地方修改订单的状态,但一个支付成功的订单就这么被识别为未支付的订单了,这就奇怪了,花费了两三个小时反复检查日志和代码后,我给前司对接的人说,这大概是个灵异事件。

前司同事提议要不换成测试环境再试试,我说可以,然后切换到测试服务器准备查看实时日志,过了一会儿,前司同事给我发来一个截图,上面是新下的一个订单,她特别标记出来了下单时间,和她当时的系统时间,竟然对不上,下单时间竟然比当前时间还早三分钟。

我一下想到一个可能,我去那台业务服务器上查看了当前时间,又去定时任务那台服务器看了下时间,竟然相差 3 分钟,而且定时任务的服务器时间是正确的北京时间,定时任务服务器的时间比北京时间慢了 3 分钟!

一下子就都说得通了,这三分钟的时间差让我耗了两个小时检查代码和日志,业务服务器下单后,下单时间比实际的当前时间早了三分钟,在用户支付的间隙,正常时间服务器跑的定时任务读取超过三分钟的订单自动取消,读取到了这个订单,就造成了用户那边支付完成,定时任务又执行读取好的订单列表去取消。

真相大白后,我准备修改时间不对的服务器时间,检查时区,发现这台服务器的时区是:Time zone: Asia/Shanghai (CST, +0800),而那台正常时间的服务器,时区也是:Time zone: Asia/Shanghai (CST, +0800),但同一个时区,两台服务器获取到的 Universal time 竟然会相差三分钟,网上搜索了这个问题,发现并没有搜索到结果,只好手动卡点设置时间与北京时间同步,如果有知道这个问题原因的小伙伴,麻烦给我解惑一下,谢谢~

一、创建start.bat文件

二、以文本编辑打开文件,编辑如下命令

@echo off
%1 mshta vbscript:CreateObject("WScript.Shell").Run("%~s0 ::",0,FALSE)(window.close)&&exit
java -jar xxx.jar > log.log 2>&1 &
exit

三、如果要指定配置文件,配置Jvm 环境,则编辑如下命令

@echo off
%1 mshta vbscript:CreateObject("WScript.Shell").Run("%~s0 ::",0,FALSE)(window.close)&&exit
set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
java -jar xxx.jar  --spring.config.location=./conf/application.yml  >  log.log 2>&1 &
exit

四、创建stop.bat 关闭特定端口的java程序

@echo off
set port=xxx(项目端口号)
for /f "tokens=1-5" %%i in ('netstat -ano^|findstr ":%port%"') do taskkill /f /pid %%m

五、创建restart.bat ,重启jar包

@echo off
%1 mshta vbscript:CreateObject("WScript.Shell").Run("%~s0 ::",0,FALSE)(window.close)&&exit
set port=xxx(项目端口号)
for /f "tokens=1-5" %%i in ('netstat -ano^|findstr ":%port%"') do taskkill /f /pid %%m &
set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
java -jar xxx.jar  --spring.config.location=./conf/application.yml  >  log.log 2>&1 &
exit

原文地址:https://blog.csdn.net/Crazy_Cw/article/details/125690560

为啥要改?

Docker安装后默认下载的位置在/var/lib/docker ,如果/var分区没有独立分出来,Linux下默认是与/根分区在一起。一般我们装Linux系统的时候,除了做邮件服务器外,都不会把/var分区独立分出来,而且/分区一般不会太大,比如我现在用的这台根分区50G的,在拉镜像的时候提示硬盘空间不足的问题,而其它分区还有很大空间。基于此情此景,我们都要把这个目录改一下

查看当前Docker目录位置

#展示当前docker的配置信息
docker info
-------------------------------------------------------------------
#在信息找到Docker Root Dir,对应的就是了,默认为:
Docker Root Dir: /var/lib/docker

几种改法

注意以下几种方式是互斥的,我折腾了一会,才发现,如你已经注意到,那此坑已平。

1、最简单也是最暴力的方式——修改 /etc/systemd/system/multi-user.target.wants/docker.service

sudo vim /etc/systemd/system/multi-user.target.wants/docker.service
-------------------------------------------------------------------
#找到ExecStart部分,在此行末尾添加--graph=你的目录,我的如下
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock 
--graph=/home/hellxz/docker-home
-------------------------------------------------------------------
#保存退出,接着让systemd重新读取下这些service等的配置
sudo systemctl daemon-reload
#重启docker服务
sudo systemctl restart docker

--graph=/path/to/path可以替换成-g /path/to/path 效果等同

2、官方文档中的方式,修改 /etc/docker/daemon.json

{
  "registry-mirrors": ["http://hub-mirror.c.163.com"],
  "data-root": "/home/hellxz/docker-home"
}

保存退出,重启docker服务

sudo systemctl restart docker

主要是用data-root来修改docker的工作目录

另外要提到的一点是,我尝试了drop-in文件方式,没有作用。

原文链接:https://www.cnblogs.com/ellisonzhang/p/15152402.html

公司最近为了节约成本,将原来部署测试环境的阿里云的服务器停止续费,从腾讯云新购了四台服务器。

为了尽可能的节约成本,四台服务器,只购买了一个公网 IP,其他服务器计划通过内网桥接之类的联网,实际操作后,发现事情没有这么简单,折腾了几天才解决,现在记录一下。

一开始搜了很多教程,都是 window 宿主机安装 VM 虚拟机进行桥接的教程,最后找到一个内网服务器通过代理使用有外网的服务器下载公网数据,但是这样不能 ping 通外网,好像速度也有限制,代理的教程链接如下:

内网服务器如何设置代理访问外网

最后感觉一些 docker 相关的东西还是联网困难,还是需要一个完美的联网方式,又搜了很久,搜到这篇利用建立虚拟网关进行联网的教程,测试一切联网正常,也可以在没有公网的服务器里 ping 通 baidu.com ,教程链接如下:

无公网服务器通过另一台有公网服务器联网

这里也搬运一下教程,防止链接失效

有公网服务器操作:

1、安装

yum install -y pptpd

2、修改配置

vim /etc/pptpd.conf

在编辑的文件里最下面添加以下代码(原文:代码意思是通过虚拟网关实现,localip即本地的虚拟网关,remoteip是开放的网关ip,类似于路由器一样,分配给链接的服务器。):

localip 192.168.0.1
remoteip 192.168.0.234-238,192.168.0.245

3、增加配置

打开 /etc/ppp/chap-secrets 配置文件,执行以下命令。

//用户名    pptpd    密码    *
root    pptpd    123456    *

4、启动pptpd服务

systemctl start pptpd

若想添加自启则执行以下命令,关闭则将on改为off

chkconfig pptpd on

5、开启转发

同时还需要开启转发,及iptables功能。

echo 1 > /proc/sys/net/ipv4/ip_forward

以上命令为临时开启,重启消失。如想永久开启,则执行以下命令编辑,将 net.ipv4.ip_forward=0 改为 net.ipv4.ip_forward=1 即可。

vim /etc/sysctl.conf

6、开启iptables nat功能

iptables -t nat -A POSTROUTING -o eth0 -s 192.168.0.0/24 -j MASQUERADE

若想自动生效,则安装iptables-services,之后开启功能,然后保存配置。并配置自启。

yum install iptables-services
iptables -t nat -A POSTROUTING -o eth0 -s 192.168.0.0/24 -j MASQUERADE
service iptables save
systemctl enable iptables.service

内网服务器操作

1、安装PPTP和PPTP-SETUP

安装PPTP和PPTP-SETUP,在centos无法安装pptp-setup,需要自行配置拨号文件(详见下面)。

yum install -y pptp pptp-setup

2、配置拨号文件

首先要知道 公网服务器的内网ip ,替换下面的 xxx.xxx.xxx.xxx 。这里创建了一个名为test的拨号文件

pptpsetup --create test --server xxx.xxx.xxx.xxx --username root --password 123456 --encrypt

其代表含义为:

pptpsetup --create 配置文件的名称 --server 有公网 IP 的云服务器的内网 IP --username 连接 PPTP 的用户名 --password 连接 PPTP 的密码 --encrypt

3、进行拨号

pppd call test

4、设置路由

route add -net 0.0.0.0 dev ppp0

Ps. 3,4步骤开机自动生效

若想3,4步骤开机自动生效。则在cd /etc/rc.d/init.d并创建个.sh文件,例如叫 a.sh,内容如下:

#!/bin/bash
#chkconfig:2345 65 65 //
pppd call test
sleep 5s
route add -net 0.0.0.0 dev ppp0

之后执行以下代码,即可实现自启。

chkconfig --add a.sh
chkconfig a.sh on

5、测试网络

最后ping www.baidu.com,若能够ping通,则证明联网成功。

最后

经实测、全程按照教程设置,三台内网服务器都能通过有外网的服务器 ping 通 baidu.com 了,但是大量下载时,会出现拨号断开的情况,需要重复操作上面内网服务器的 3、4 步骤操作恢复拨号,网上能搜到拨号断开自动重新拨号的教程,没测试,可以自己搜搜测试一下。

学习分布式的时候,了解到 Redis 分布式锁的时候,觉得这是个很实用的功能,于是实操了一下,使用 Jmeter 进行线程压测,记录一下结论

Synchronized 只能锁当前服务的线程,锁不了其他服务,所以在分布式情况下不实用,而且大并发使用 Synchronized 也存在性能问题

Redis 使用 SetNX 做一把锁,setnx 只有在key不存在的情况下,才会设置value ,已经存在key,不做任何操作,利用这个特性来制作一把锁。

大致流程如下:
使用 StringRedisTemplate 对象的 opsForValue().setIfAbsent(K,V) 方法创建一把锁,类似于 jedis.setnx(K,V),方法返回值的返回值是布尔值,如果结果 false 就不执行逻辑代码返回自定义 error
线程在拿到锁在执行完后,必须去释放锁,使用 StringRedisTemplate 对象的 delete(K) 释放锁,让其他线程来争抢锁

问题一,如果一个线程拿到锁,中途逻辑代码抛异常,导致该线程不能正常释放锁,就会导致锁一直不能释放,解决办法是用 try finally 来释放锁,加锁和逻辑代码放在 try 捕获里,释放锁放在 finally 里。

问题二,线程拿到锁,中途程序挂掉,导致不能执行 finally ,也将不能释放锁,解决办法是使用 StringRedisTemplate 对象的 expire(K,timeout) 设置超时时间,比如设置10秒,不管程序是否挂掉,都能释放锁

问题三,如果线程刚拿到锁就挂了,没来得及设置超时,也就不能释放锁,解决办法是使用 StringRedisTemplate 的 opsForValue.setIfAbSent(K,V,timeout,timeunit) 设置锁的超时,保证加锁和超时是一个原子执行

问题四,如果逻辑代码执行时间超过了超时时间,代码没执行完,锁就被释放掉了,下一个线程又来加锁,最后第一个线程执行完后会去释放锁,但它释放的是其他线程加的锁,多个线程反复,锁会永久失效,解决办法是给当前线程锁设置一个token,比如使用 UUID,当成 value 保存,释放锁时判断 key 取到的 value 是否等于生成的token,不相等就不去释放

问题五,超市时间设置合理问题,解决办法是每10秒分线程检测锁是否存在,存在就续期

最终方案,使用 Redisson 客户端,实现了最完善的分布式锁,加锁之前 redissonLock.getLock(K) 拿锁, try 里加锁 redissonLock.lock() 方法,finally 里面释放锁,redissonLock.unlock() 方法。

前段时间为甲方做了个在线预约的网页,功能很简单,就两三个页面以及表单提交。

环境是:Windows 服务器、Tomcat7、Java、JSP

上线后,一切都挺正常,直到有一天,实施给我发消息说部分手机访问页面异常,我简单了解了一下,发现页面的 CSS 样式和图片并没有正常加载,但是并不知道原因。

由于自己手机一直是正常的,所以拿几个同事的手机试了一下,奇葩的事情出来了,大部分同事的手机访问都是正常的,但有两个同事,手机型号一样,配置一样,甚至是同一天抢购的小米手机,一个能正常访问,一个不能...

多试了几次发现,这种现象只发生在微信的内置浏览器访问,而且用 ip 加端口的方式访问页面正常,用域名访问就会出现异常...

我不知道微信内置浏览器怎么调试查看报错,于是上 V2EX 发了个帖子吐槽了这个现象。很多大佬都发表了看法,其中 @faqqcn 和 @dntilee 两位大佬教了我一种能够在PC 上调试微信网页的方法(只能安卓设备):

微信内打开 http://debugx5.qq.com/ ,然后打开 「打开 TBS 内核 inspector 」
手机连接电脑并打开开发者选项和 USB 调试,打开电脑 Chrome,输"chrome://inspect/#devices",下面会有你的设备和网页

试了试,果然能够在 Chrome 浏览器里实时看到微信网页的信息了,Console 栏里可以看到不能正常访问的原因是因为 CSS 和图片的请求全都失败了,报错信息如下:

QQ截图20210630093548.png

我不知道这个意味着甚么,网上搜了很久也没有解决,对于微信的这种迷惑行为已经绝望了

虽然到现在这个问题也没有解决,但还是学会了一个新的技巧,最后,我想在这里对微信送上我真诚的祝福:NM$L

这两天与第三方接口调试的时候,接口方提供的 API 返回了一个非标准的 JSON 字符串,整个字符串里没有一个双引号,导致不能直接使用 Java 的 JSON.parseObject() 来将字符串直接转换为对象。

字符串如下:

{itemName:血型,itemCode:651,itemSex:9},{itemName:白带常规,itemCode:679,itemSex:2},{itemName:肝功,itemCode:692,itemSex:9},{itemName:身高体重,itemCode:873,itemSex:9},{itemName:妇科检查,itemCode:1059,itemSex:2},{itemName:CRP+血细胞计数五分+异常红细胞形态,itemCode:1119,itemSex:9},{itemName:血常规五分类CRP,itemCode:1121,itemSex:9}

然后接口方这样给我说的:
微信图片编辑_20210422102422.jpg

到这里我就不指望接口方修改返回格式了,而他说的用逗号分隔,想想就知道多复杂,在网上搜了半天,最后找到一个正则的方案,网上给出的正则规则是这样的:

pattern.replaceAll("(\\{|,)([^:]+)", "$1\"$2\"").replaceAll("([^:,\\}]+)(\\}|,)", "\"$1\"$2")

但这个正则有问题,替换完是这样的:

{"itemName":"血型","itemCode":"651","itemSex":"9"},"{itemName":"白带常规","itemCode":"679","itemSex":"2"},"{itemName":"肝功","itemCode":"692","itemSex":"9"},"{itemName":"身高体重","itemCode":"873","itemSex":"9"},"{itemName":"妇科检查","itemCode":"1059","itemSex":"2"},"{itemName":"CRP+血细胞计数五分+异常红细胞形态","itemCode":"1119","itemSex":"9"},"{itemName":"血常规五分类CRP","itemCode":"1121","itemSex":"9"}

替换完的字符串,从第二个开始," 跑到 { 外面来了,于是跟小伙伴交流,小伙伴最后帮我修改了一个正则:

pattern.replaceAll("{(\w+):(.+?),", "{\"$1\":\"$2\",").replaceAll("(\w+):(\d+)", "\"$1\":\"$2\"");

因为Java字符串的 \w 和 \d 需要转义,所以必须这样写:

pattern.replaceAll("{(\\w+):(.+?),", "{\"$1\":\"$2\",").replaceAll("(\\w+):(\\d+)", "\"$1\":\"$2\"");

搞定后,开始测试结果报错:
微信图片_20210422103100.png

小伙伴吐槽辣鸡 Java,没办法,继续网上搜,结果在一篇帖子里的评论里,看到有人说 Java 的字符串里 { 也需要转义,转义后一测试,果然是这样,所以最后的正则变成了这样:

pattern.replaceAll("\\{(\\w+):(.+?),", "{\"$1\":\"$2\",").replaceAll("(\\w+):(\\d+)", "\"$1\":\"$2\"");

替换后字符串变成了正常 JSON 字符串,在群里跟朋友分享了这件事,朋友表示正则很牛逼。

本以为这件事就过去了,第二天看群消息,群里另一个小伙伴分享了一个新的正则:

pattern.replaceAll("(\\w+):(.*?)([,|}])","\"$1\":\"$2\"$3");

测试时正常的,于是好奇正则的工作原理,开始学习正则的使用,翻看了正则的规则 (菜鸟教程 - 正则) 后,大概懂了这个正则工作原理:

三个括号分成了三组,分别匹配 List 里面的每个节点
\w 表示匹配字母、数字、下划线。等价于 [A-Za-z0-9_]
+ 表示匹配前面的子表达式一次或多次。(要匹配 + 字符,使用 \+)
. 表示匹配除换行符 \n 之外的任何单字符。(要匹配 . ,使用 \.)
* 表示匹配前面的子表达式零次或多次。(要匹配 * 字符,使用 \*)。
? 表示匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。(要匹配 ? 字符,使用 \?)
| 表示两项之间的一个选择。(要匹配 |,使用 \|)

正则学会了真的很强大,但这里只了解了一点皮毛,后面还要继续学习。