记一次死锁

业务场景:微信支付模块,每一次请求都会在pay_log表里插入订单号等记录,支付回调时会更新pay_log表的记录

同时还有一个定时任务用来定时将所有1小时前的订单标记为交易超时

编写单元测试,模拟支付回调通知,结果出了异常,更新记录时出现死锁

大致SQL如下:

事务A

1
Update pay_log SET tradeState = 2 WHERE tradeState=0 AND createTime < '2019-08-20 15:23:04.167'

事务B

1
2
INSERT pay_log VALUES(NULL,?,NOW(),?,?,?,?,?,?,?,?,?,?,?,?) 
Update pay_log SET tradeState =? WHERE outTradeNo = ? AND tradeState = ? AND payType = ?

然后事务B被回滚了

解决方案:要么禁用定时任务,要么去掉单元测试的事务手动回滚,要么单元测试不要打断点调试(定时任务设置了项目启动后10秒后启动)

附上MySQL死锁日志,其实这玩意只能大致的定位故障,因为可以看出事务2的SQL只有最后执行的那一条

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
2019-08-20 16:23:07 0x7ff1b2039700
*** (1) TRANSACTION:
TRANSACTION 43869195, ACTIVE 3 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1136, 269 row lock(s)
MySQL thread id 746830, OS thread handle 140676071700224, query id 184518127 192.168.3.108 root updating
Update pay_log SET tradeState = 2 WHERE tradeState=0 AND createTime < '2019-08-20 15:23:04.167'

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3179 page no 7 n bits 104 index PRIMARY of table `pay`.`pay_log` trx id 43869195 lock_mode X waiting
Record lock, heap no 37 PHYSICAL RECORD: n_fields 17; compact format; info bits 0


*** (2) TRANSACTION:
TRANSACTION 43869181, ACTIVE 7 sec starting index read
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 747008, OS thread handle 140676050425600, query id 184518193 192.168.3.49 root updating
Update pay_log SET tradeState =1 WHERE outTradeNo = 'T9744695806' AND tradeState = 0 AND payType = 0

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3179 page no 7 n bits 104 index PRIMARY of table `pay`.`pay_log` trx id 43869181 lock_mode X locks rec but not gap
Record lock, heap no 37 PHYSICAL RECORD: n_fields 17; compact format; info bits 0

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3179 page no 4 n bits 168 index PRIMARY of table `pay`.`pay_log` trx id 43869181 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 17; compact format; info bits 0

*** WE ROLL BACK TRANSACTION (2)

白菜的开发日记3

许久没更博客了,写点白菜最近的进度吧。

整个Web版重构完成,我还在路由器上搭了一个mysql server,然后用mysqldump每天凌晨备份白菜的数据库。

顺手加上了凌晨把获取失败的ID以邮件形式发到自己邮箱,还加了清空当日生成的临时文件。

弱智啪啪啪提了一个req,!sleep 按小时为单位禁言自己,没想到做出来真的一堆弱智用……

日记2里提到的绘图类,被我从静态代码块改成了静态方法,然后在!sudo bg命令时调用它(否则会导致修改用户bg不能马上生效)。

顺手做了一个!fp功能,不过mp4和5没什么人用……

我还研究了一下HTTP Client,做了模拟登陆osu并且下图,还写了解析.osu的正则表达式,来从官网获取BG。

甚至还处理了0,0,”sb\bg.png”这种情况……

在这里吹自己一波,我居然想到了把官网下图时的InputStream包装为ZIPInputStream,直接在内存中解压osz……

写完才发现“卧槽我居然真的把这个想法实现了”。。可以说白菜现在的粗壮性又上了一个台阶(

白菜的开发日记2

白菜之前的代码基本已经稳定运行了,于是我准备把它改成一个Web项目,采用HTTP API来收发消息,同时将来可能扩展出网页什么的……

在重构的时候遇到一个问题:之前我在绘图类,用nio扫出结算界面需要的所有图片,然后用static代码块包裹:

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
static {
final Path resultPath = Paths.get(rb.getString("path") + "\\data\\image\\resource\\result");
//使用NIO扫描文件夹
final List<File> resultFiles = new ArrayList<>();
Images = new ArrayList<>();
Nums = new ArrayList<>();
Mods = new ArrayList<>();
SimpleFileVisitor<Path> resultFinder = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
resultFiles.add(file.toFile());
return super.visitFile(file, attrs);
}
};

try {
//将所有文件分为三个List
java.nio.file.Files.walkFileTree(resultPath, resultFinder);
for (int i = 0; i < 23; i++) {
Images.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 23; i < 37; i++) {
Nums.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 37; i < 48; i++) {
Mods.add(ImageIO.read(resultFiles.get(i)));
}
zPP = ImageIO.read(resultFiles.get(48));
zPPTrick = ImageIO.read(resultFiles.get(49));
} catch (IOException e) {
logger.error("读取result相关资源失败");
logger.error(e.getMessage());
}

这样这些对象只会在这个类第一次被加载的时候生成,避免了每次绘图重复扫描文件夹,降低效率的问题。

然后后面画图的时候,我只需要调用List中的BufferedImage对象

1
2
3
//右下角两个FPS
g2.drawImage(Images.get(1), 1300, 699, null);
g2.drawImage(Images.get(2), 1300, 723, null);

但是如果我要新加功能就会显得不便:我只能添加z开头的文件名,否则我得把所有的Images.get()后面的数字调一下。。

看了一眼它们的文件名,我觉得ppy应该是在osu启动的时候把皮肤全部加载到内存,然后根据文件名绘制……

看了一下nio,好像能生成文件名,那我把List换成Map好了……

白菜的开发日记1

emm。从16号早上出门前Initial Commit,到23号中午12点白菜正式上线,这一周的时间过的真是快啊……

https://github.com/Arsenolite/osubot/commits/master

43次commit之后,终于有时间坐下来写点开发日记了。严格的说这是第一个我写出来能用的程序啊(x

先介绍一下她吧,她是一个面向osu群的QQ机器人,具体用法可以参见github的readme(普通用户就两条指令你还写个readme真是凑不要脸x)

相对别的机器人的亮点就是她返回的是图片,样式主要由啊哇设计,我们从旁修改,当然最后是我实现的。(记得18号那天我画一行丢一个图在群里x

作为Java程序员,老本行就是折腾数据库,当然白菜也有这方面的功能。每天凌晨4点将所有登记了的玩家的数据爬取,然后存入,提供一个对比功能。

18号晚上写完用户名片绘制,19号上午写完凌晨的定时任务,20号做了一些sudo命令。

22号凌晨1点终于搞定BP绘制,然后白天研究maven打包,开通vps,关闭mysql占用空闲内存缓存等等。

22号晚上怕他突然提出来一个大BUG,之前数据库中我存入的是用户名,但是这样会导致无法识别改名玩家的问题。

于是晚上赶工重构代码+改表结构,由于发现操作系统的时区就是北京时间,而BP返回时间是根据API key对应玩家的国籍来的(也是北京时间),砍掉了所有的时区转换。

在vps上运行的时候查API失败率非常低,于是将api工具类中加上重试机制,砍掉了其他地方代码对网络错误的try-catch。

然后就是些小bug(之前改的时候没改彻底导致的各种问题),昨天中午正式上线,下午晚上研究了jsoup爬取网页指定元素,又把爬score rank改成了二分法。

顺手写了个自动欢迎新人,由于啊哇还没想好scorerank的呈现方式,把绘制scorerank的代码注释掉发布到了服务器上。

……挺后悔没有边开发边写博文的,现在有的遇到的bug都已经忘记了……

总之这个项目增强了我对SQL、前端(jsoup提供的是js的getElementById方法,和css的div.Class风格的选择器)的熟练度,顺便还达成了第一次使用WinServer系统的成就,勉强算是linux+WinServer都能干的运维?

复习了以前练手搞的多线程机制,IO流,各种包装类,日期处理,字符串处理之类的基础问题,初次尝试手写二分法(特别新鲜),可以说它弥补了我Java基础代码写得少的缺陷……

毕竟之前在单位搞的SSM框架更多是做填空题,把XML配好,crud和参数验证写好,甚至那个小项目都用不着组装复杂pojo……到处都是提供好的最佳实践(

……虽然这个项目用的也都是给好的类和方法,充其量起到了熟悉JDK本身的作用,毕竟不是和c一样要手写String的查找替换啥的(x

emm,白菜大概是不会弃坑的,以后尽量写出详细的遇到/解决bug的过程吧233

关于数据库删除数据的同时保持完整性的个人做法。

进了单位,要我从头写一个商城出来。
完成了初步的数据库表格设计之后,发现如下一个问题:

我的address表记录了用户的收货地址,而订单中采用aid指向address表的某行。

那么如果用户后期删除了这个aid对应的收货地址,或者修改了地址,会引发bug:早期订单对应的地址会改变或者无法找到。

一开始考虑用外键约束,但是外键约束只能达成这样的目标:

  • 如果存在采用这个aid的订单,就拒绝删除aid
  • 删除aid同时删除所有带这个aid的订单
    这明显不符合常识和业务需求。

我想出的解决办法是:给address表加一个valid的int字段,长度为1(实际上就是boolean,记录这个地址有没有效),删除地址的时候不直接从数据库删除,而是将valid改为0。

修改地址的时候,也不直接改,而是创建一个新的地址,再把老的地址valid值改成0。

这样,如果订单采用了valid为0的address,也能够正常显示,但是用户在选择自己收货地址的时候是看不到这条“被删除”的地址的(WHERE valid =1)。

类似的业务需求也是这样,例如product也加一个valid字段,出现一个包含了已删除产品的订单时,能正常显示,但是这个产品不能被加入购物车和订单。

这样业务需求倒是实现了,但是service感觉会异常复杂……需要重新梳理。

幸好Mapper层不需要太多修改,把删除方法全去掉,再在某些条件里加上valid=1就行了。

记一次在CentOS 7上部署Java测试环境的过程

将之前配置LNMJ测试环境的过程归个档,方便日后查阅。

1) JDK环境

下载并安装
1
2
cd usr
mkdir java

当时不小心下到了JDKDemo的包,直接下JDK包时发现下载下来的是网页,于是只好手动下载再上传。

1
2
cd java
rpm -ivh jdk-8u131-linux-x64.rpm
配置环境
1
vi /etc/profile

按I进入编辑模式,将如下内容加入profile中:

1
2
export JAVA_HOME=/usr/java/jdk1.8.0_131
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

输入

1
source /etc/profile

命令,让配置生效。
最后和Windows平台一样,输入java -version来验证安装。

2)Tomcat的配置

下载并安装

wget下载安装包,并且解压。

1
tar -zxvf apache-tomcat-x.x.xx.tar.gz

修改文件夹名字方便后续使用。

配置环境变量

编辑/etc/profile,在末尾加上:

1
2
3
export CATALINA_HOME=/usr/apache-tomcat
export CATALINA_BASE=/usr/apache-tomcat
export PATH=$PATH:$JAVA_HOME/bin:$CATALINA_HOME/bin

刷新并运行Tomcat进行测试:

1
2
source /etc/profile
/usr/apache-tomcat/bin/startup.sh
将Tomcat配置为系统服务,方便用systemctrl管理:
  • 在Tomcat的bin目录下,新建setenv.sh脚本。Tomcat启动时会自动运行这个脚本。
    1
    CATALINA_PID="$CATALINA_BASE/tomcat.pid"
  • 使用vi编辑/usr/lib/systemd/system/tomcat.service文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [Unit]
    Description=Tomcat
    After=syslog.target network.target remote-fs.target nss-lookup.target
    [Service]
    Type=forking
    PIDFile=/usr/apache-tomcat/tomcat.pid
    ExecStart=/usr/apache-tomcat/bin/startup.sh
    ExecReload=/bin/kill -s HUP $MAINPID
    ExecStop=/bin/kill -s QUIT $MAINPID
    PrivateTmp=true
    [Install]
    WantedBy=multi-user.target

然后就可以用systemctrl命令来管理Tomcat的开机启动等问题了。

当时出了一个很奇怪的bug,我本地运行没有问题的war包,上传到服务器上就是404,在Tomcat管理页面里也能看到这个WebAPP在运行。
最后我直接删掉了服务器Tomcat,将本地Tomcat直接上传上去,并把权限改为0777,最终解决问题。

3)MySQL

MySQL这事比较复杂,Oracle收购了MySQL之后,准备在MySQL6.x收费,然后社区开发了一个叫MariaDB的分支,采用GPL授权,以此应对。
当然我还是不准备采用这个分支,能求稳就别浪……

下载并安装
1
2
3
wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm
rpm -ivh mysql-community-release-el7-5.noarch.rpm
yum install mysql-community-server
启动

运行mysql -u root启动。
(值得注意的是,只有root账户没有密码的时候才能这么启动,否则返回Access denied,不会提示你输入密码。在有密码的时候需要加上-p参数。)
进入mysql命令行之后,运行SQL语句设置密码:

1
set password for 'root'@'localhost' =password('密码');

当时配置完成后死活连不上,最后发现CentOS不是用IPTABLE作为防火墙,而是使用了firewall。
更换为IPTABLE,并且开放端口后问题解决。

一些配置:
  • 更改编码:
    1
    vi /etc/my.cnf
    在文件末尾添加:
    1
    2
    [mysql]
    default-character-set =utf8
  • 设置其他IP可以连接:
    在MySQL命令行中执行:
    1
    grant all privileges on *.* to root@'%'identified by '你的密码';

4)nginx

nginx的安装给我一种印象,下载的似乎是源代码,还要下一个gcc的编译器,现场编译现场用。
(不知道是不是真的,如有谬误请指正)

安装依赖
1
2
3
4
yum install gcc-c++  
yum install pcre pcre-devel
yum install zlib zlib-devel
yum install openssl openssl--devel
安装nginx本体
1
2
wget http://nginx.org/download/nginx-1.7.12.tar.gz
tar -zxvf nginx-1.7.12.tar.gz

并重命名目录,去掉版本号,方便后续使用。
当时安装完后,因为firewall的问题(我看错,认为80端口是打开的)以为配置出了错,最后一怒之下关了防火墙一切正常了。
决定换回熟悉的iptable。
#####配置nginx代理Tomcat
编辑nginx.conf文件,在http-server-location-proxy_pass段中填入:

1
2
http://127.0.0.1:8080
#此处8080为Tomcat默认端口号
将nginx配置为服务
  • 先编辑nginx.conf文件:
    1
    vi /usr/local/ngnix/conf/nginx.conf
    将里面的pid段后面的路径复制出来。
  • 使用vi编辑/usr/lib/systemd/system/nginx.service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    [Unit]
    Description=nginx - high performance web server
    Documentation=http://nginx.org/en/docs/
    After=network.target remote-fs.target nss-lookup.target
    [Service]
    Type=forking
    #与nginx.conf一致
    PIDFile=/usr/local/nginx/logs/nginx.pid
    #启动前检测配置文件 是否正确
    ExecStartPre=/usr/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
    #启动
    ExecStart=/usr/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
    #重启
    ExecReload=/bin/kill -s HUP $MAINPID
    #关闭
    ExecStop=/bin/kill -s QUIT $MAINPID
    PrivateTmp=true
    [Install]
    WantedBy=multi-user.target

至此,JDK Tomcat Nginx MySQL全部配置完成。
可以将war包上传到Tomcat的webapp目录下,Tomcat会自动部署。