java 调用shell时出现假死问题(死锁问题),org.springframework.dao.CannotAcquireLockException:Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
现象:在使用java 调用 shell 执行相关命令时,当就会出现卡死等问题.然后就会报错org.springframework.dao.CannotAcquireLockException:Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
刚开始一直以为是mysql的事务锁了,最后排查才发现是java调用shell的bug
解决:
其实主要的问题来源于waitfor()这块,
让我们来看看waitFor()的说明:
JDK帮助文档上这么说:如有必要,一直要等到由该 Process 对象表示的进程已经终止。如果已终止该子进程,此方法立即返回。但是直接调用这个方法会导致当前线程阻塞,直到退出子进程。对此JDK文档上还有如此解释:因为本地的系统对标准输入和输出所提供的缓冲池有效,所以错误的对标准输出快速的写入何从标准输入快速的读入都有可能造成子进程的所,甚至死锁。好了,问题的关键在缓冲区这个地方:可执行程序的标准输出比较多,而运行窗口的标准缓冲区不够大,所以发生阻塞。接着来分析缓冲区,哪来的这个东西,当Runtime对象调用exec(cmd)后,JVM会启动一个子进程,该进程会与JVM进程建立三个管道连接:标准输入,标准输出和标准错误流。假设该程序不断在向标准输出流和标准错误流写数据,而JVM不读取的话,当缓冲区满之后将无法继续写入数据,最终造成阻塞在waitfor()这里。 知道问题所在,我们解决问题就好办了。查看网上说的方法多数是开两个线程在waitfor()命令之前读出窗口的标准输出缓冲区和标准错误流的内容。
解决:
方式1
package com.xxx.xxx;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.InputStreamReader;
public class CallCmd {
// 关流方法
private void closeStream(Closeable stream) {
if (stream != null) {
try {
stream.close();
} catch (Exception e) {
System.out.println("close stream failed.");
}
}
}
// 此方法用来执行脚本或命令
// String cmd:脚本(绝对路径)位置或命令内容
// File dir:执行命令的子进程的工作目录(null则表示和当前主进程工作目录相同)
public String callCMD(String cmd, File dir) {
StringBuilder result = new StringBuilder();
Process process = null;
BufferedReader bufrIn = null;
BufferedReader bufrError = null;
try {
// 执行命令, 返回一个子进程对象(命令在子进程中执行)
process = Runtime.getRuntime().exec(cmd, null, dir);
// 方法阻塞,等待命令执行完成
process.waitFor();
// 获取命令执行结果, 有两个结果: 正常的输出和错误的输出(PS: 子进程的输出就是主进程的输入)
bufrIn =
new BufferedReader(
new InputStreamReader(process.getInputStream(), "UTF-8")); // 正常的输出
bufrError =
new BufferedReader(
new InputStreamReader(process.getErrorStream(), "UTF-8")); // 错误的输出
// 读取输出
String line = null;
while ((line = bufrIn.readLine()) != null) {
result.append(line).append('\n');
}
while ((line = bufrError.readLine()) != null) {
result.append(line).append('\n');
}
} catch (Exception e) {
e.printStackTrace();
} finally {
closeStream(bufrIn);
closeStream(bufrError);
// 销毁子进程
if (process != null) {
process.destroy();
}
}
// 返回执行结果
return result.toString();
}
}
方式2
我是用这种方式实现 ProcessBuilder
private void execReloadAll(){
List<String> cmds = new ArrayList<String>();
## 拼装相关的脚本命令
cmds.add(StrongSwanCommand.COMMAND_SH);
cmds.add(StrongSwanCommand.COMMAND_ENV);
ProcessBuilder pb=new ProcessBuilder(cmds);
int exitCode = 0;
try {
Process process = pb.start();
/**
* 问题:调用shell时出现假死问题
* waitFor()一直要等到由该 Process 对象表示的进程已经终止。如果已终止该子进程,此方法立即返回。但是直接调用这个方法会导致当前线程阻塞,直到退出子进程。
* 对此JDK文档上还有如此解释:因为本地的系统对标准输入和输出所提供的缓冲池有效,所以错误的对标准输出快速的写入何从标准输入快速的读入都有可能造成子进程的所,
* 甚至死锁。问题的关键在缓冲区这个地方:可执行程序的标准输出比较多,而运行窗口的标准缓冲区不够大,所以发生阻塞。接着来分析缓冲区,哪来的这个东西,
* 当Runtime对象调用exec(cmd)后,JVM会启动一个子进程,该进程会与JVM进程建立三个管道连接:标准输入,标准输出和标准错误流。
* 假设该程序不断在向标准输出流和标准错误流写数据,而JVM不读取的话,当缓冲区满之后将无法继续写入数据,最终造成阻塞在waitfor()这里。
*
*/
String result = "";
BufferedReader br = new BufferedReader(new InputStreamReader(
process.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
result = sb.toString();
exitCode = process.waitFor();
log.info("result:---{}",result);
// 等待该进程执行完毕
if (exitCode != 0) {
// 如果进程运行结果不为0,表示进程是错误退出的
// 获得进程实例的错误输出
InputStream error = process.getErrorStream();
log.info("加载失败:{}",error);
throw new BizException(ErrorCode.IKE_LOAD_ALL_ERROR.getCode(),ErrorCode.IKE_LOAD_ALL_ERROR.getMsg());
}
// 获得进程实例的标准输出
InputStream sdin = process.getInputStream();
log.info("加载:{}",sdin);
} catch (Exception e) {
log.error("执行失败", e);
throw new BizException(ErrorCode.LOAD_ALL_ERROR.getCode(),ErrorCode.LOAD_ALL_ERROR.getMsg());
}
assert exitCode == 0;
}
或者通过exec实现
public static String exec(String cmd) {
String result = "";
try {
String[] shellCmd = new String[] { "/bin/sh", "-c", cmd };
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(
ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
result = sb.toString();
ps.waitFor();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}