关键词:java,MySQL,事务处理,事务并发,事务隔离,数据库锁,事务锁,银行账户转账示例,并发
模拟环境:
数据库:MySQL5.5;
开发环境:jdk1.6;
需要dbutils、log4j等jar包;
目的:转账的时候要保证数据的一致性。
首先设计两个测试用表:
表一:账户信息表
/*DDL 信息*/------------ CREATE TABLE `account_info_tab` ( `A_No` varchar(50) NOT NULL COMMENT '账号', `A_Name` varchar(50) DEFAULT NULL COMMENT '姓名', `A_Pswd` varchar(200) DEFAULT NULL COMMENT '密码', `A_Money` decimal(20,2) DEFAULT '0.00' COMMENT '账户余额', PRIMARY KEY (`A_No`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
表二:账户变动记录表
/*DDL 信息*/------------
CREATE TABLE `account_log` (
`A_Id` varchar(100) NOT NULL COMMENT '变动账户',
`AL_DateTime` datetime NOT NULL COMMENT '变动时间',
`AL_Money` decimal(20,2) DEFAULT '0.00' COMMENT '变动金额数',
`AL_Id` bigint(20) NOT NULL AUTO_INCREMENT,
`AL_Balance` decimal(20,2) DEFAULT '0.00' COMMENT '本次变动后的余额',
PRIMARY KEY (`AL_Id`)
) ENGINE=InnoDB AUTO_INCREMENT=881 DEFAULT CHARSET=utf8
完成以上表设计之后,插入两条测试数据:A_Id分别为1413598570031和1413598616890,余额分别为0.00和10001.01,其他字段的插入数字省略。
其次,编写测试类文件:DalAccount_Info_Tab
package com.zytk.dbcommon.dal;
import java.util.List;
import java.util.Map;
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.MapListHandler;
import com.zytk.common.CommonLog;
import com.zytk.db.models.Account_Info_Tab;
import com.zytk.start.ServletUtils;
public class DalAccount_Info_Tab {
private String sql = "";
private Connection connection = null;
public String turnResult="";
public DalAccount_Info_Tab() {
try {
connection = ServletUtils.dataSource.getConnection();//这里用的连接池模式获取数据库连接
connection.setAutoCommit(false);// 关闭自动提交
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);//设置事务隔离级别为8
} catch (SQLException e) {
throw new RuntimeException("无法从数据源获取连接", e);
}
CommonLog.info("新实例获取连接,并且关闭自动提交");
}
/** 转账测试 */
public boolean transferMoney(String fromID, String toID, double money) {
//boolean rtnVal=false;
int rsCnt=0;
try {
Account_Info_Tab objFrom=findId(fromID);
Account_Info_Tab objTo=findId(toID);
turnResult="---》转出前账户"+objFrom.getA_No()+"余额:"+objFrom.getA_Money();
turnResult+=",《---转入前账户"+objTo.getA_No()+"余额:"+objTo.getA_Money();
insertLog(objFrom,objTo,money);
if((Double.parseDouble(objFrom.getA_Money()) - money)>=0)
{
rsCnt=turn2( objFrom, objTo, money);
connection.commit();// 若余额够转账,则提交事务
}
else {
CommonLog.error("转出余额不足:"+ objFrom.getA_Money());
}
} catch (Exception e) {
turnResult="本次执行出现错误";
try {
connection.rollback();
} catch (SQLException e1) {
CommonLog.error("转账操作connection.rollback()时发生错误:"+ e1.toString());
}
CommonLog.error("转账操作时发生错误:" + e.toString());
} finally {
try {
connection.close();
} catch (SQLException e) {
CommonLog.error("转账操作connection.close()时发生错误:"+ e.toString());
}
}
return rsCnt==1;
}
/**预先插入测试用的日志记录*/
private void insertLog(Account_Info_Tab objFrom, Account_Info_Tab objTo, double money){
QueryRunner qr = new QueryRunner();
try {
// ----以下为故意构建的测试插入转账日志代码
sql = "insert into account_log(A_Id,AL_DateTime,AL_Money,AL_Balance)values('"
+ objFrom.getA_No() + "',now(),-" + money + ",("+objFrom.getA_Money()+"-"+money+"))";// 测试提前插入转账日志记录代码
CommonLog.info("测试提前插入转出账日志记录代码执行结果:"
+ qr.update(connection, sql));
sql = "insert into account_log(A_Id,AL_DateTime,AL_Money,AL_Balance)values('"
+ objTo.getA_No() + "',now()," + money + ",("+objTo.getA_Money()+"+"+money+"))";// 测试提前插入转账日志记录代码
CommonLog.info("测试提前插入转入账日志记录代码执行结果:"
+ qr.update(connection, sql));
} catch (SQLException e) {
CommonLog.error("测试提前插入转出、转入账日志记录代码时发生错误:" + e.toString());
throw new RuntimeException(e.getMessage(),e);
}
}
/**查找转出账号/转入账号及其他信息*/
private Account_Info_Tab findId(String ANo){
Account_Info_Tab obj=null;
List<?> list = null;
QueryRunner qr = new QueryRunner();
MapListHandler mlh = new MapListHandler();
try {
sql="select * from account_info_tab where A_No=?";
list = qr.query(connection, sql, mlh, new Object[] { ANo });
if(list!=null&&list.size()>0){
Map<?, ?> map = (Map<?, ?>) list.get(0);
obj=new Account_Info_Tab();
obj.setA_No(map.get("A_No").toString());
obj.setA_Money(map.get("A_Money").toString());
obj.setA_Name(map.get("A_Name").toString());
obj.setA_Pswd(map.get("A_Pswd").toString());
}
} catch (SQLException e) {
CommonLog.error("根据账号ID查找具体账户信息时发生错误:" + e.toString());
throw new RuntimeException(e.getMessage(),e);
}
return obj;
}
/**具体执行转账的操作*/
private int turn2(Account_Info_Tab objFrom,Account_Info_Tab objTo, double money){
int exeUptResult = 0;
QueryRunner qr=new QueryRunner();
try {
sql = "update account_info_tab set A_Money=("
+ (Double.parseDouble(objFrom.getA_Money()) - money) + ") where A_No='"
+ objFrom.getA_No() + "'";// 转出
exeUptResult = qr.update(connection, sql);
CommonLog.info("转出:exeUptResult="+exeUptResult);
sql = "update account_info_tab set A_Money=("
+ (Double.parseDouble(objTo.getA_Money()) + money) + ") where A_No='"
+ objTo.getA_No() + "'";// 转入
exeUptResult = qr.update(connection, sql);
CommonLog.info("转入:exeUptResult="+exeUptResult);
Account_Info_Tab objFrom1=findId(objFrom.getA_No());
Account_Info_Tab objTo1=findId(objTo.getA_No());
turnResult+=">>转出后账户"+objFrom1.getA_No()+"余额:"+objFrom1.getA_Money();
turnResult+=",<<转入后账户"+objTo1.getA_No()+"余额:"+objTo1.getA_Money();
} catch (Exception e) {
CommonLog.error("具体执行转账操作时发生错误:" + e.toString());
throw new RuntimeException(e.getMessage(),e);
}
return exeUptResult;
}
/** 增加新记录 */
/*public boolean insert(Account_Info_Tab obj) {
// obj.setA_Money("0");
obj.setA_No("" + System.currentTimeMillis());
// obj.setA_Pswd("12345678");
boolean rtnVal = false;
Object[] paramsStrings = { obj.getA_No(), obj.getA_Name(),
obj.getA_Pswd(), obj.getA_Money() };
sql = "insert into account_info_tab(A_No,A_Name,A_Pswd,A_Money)values(?,?,?,?)";
QueryRunner qr = new QueryRunner();
int exeUptResult = 0;
try {
exeUptResult = qr.update(connection, sql, paramsStrings);
connection.commit();// 提交事务
} catch (Exception e) {
try {
connection.rollback();
} catch (SQLException e1) {
CommonLog.error("connection.rollback()增加新记录insert时发生错误:"
+ e1.toString());
}
CommonLog.error("增加新记录insert时发生错误:" + e.toString());
} finally {
try {
connection.close();
} catch (SQLException e) {
CommonLog.error("connection.close()增加新记录insert时发生错误:"
+ e.toString());
}
}
rtnVal = exeUptResult == 1;
return rtnVal;
}*/
}
最后编写测试用servlet(注意:这里用的servlet3.0标准,servlet3.0以前的标准的话,请在web.xml中配置servlet映射访问路径):
package com.test;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.zytk.common.CommonLog;
import com.zytk.db.dal.DalAccount_Info_Tab;
import com.zytk.db.models.Account_Info_Tab;
/**
* Servlet测试用例
*/
@WebServlet("/test")
public class ServletTest extends HttpServlet {
private static final long serialVersionUID = 1L;
int icnt=0;
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
String tmp="";
for(int i=0;i<10;i++){
DalAccount_Info_Tab dal=new DalAccount_Info_Tab();
/*Account_Info_Tab obj=new Account_Info_Tab();
obj.setA_Money("10008.38");
obj.setA_Name("用户名");
obj.setA_Pswd("PSWD"+(icnt++));
CommonLog.info("新增记录执行结果:"+dal.insert(obj));*/
//response.getWriter().print("执行完成。");
CommonLog.info("\n");
CommonLog.info("-----------------------\n");
CommonLog.info("转账操作执行结果:"+dal.transferMoney("1413598616890", "1413598570031", 100));
CommonLog.info("-----------------------\n");
tmp+=i+".<b>数据处理结果:"+dal.turnResult+"</b><br/>";
//response.getWriter().print("执行完成,<br/><b>数据处理结果:"+dal.turnResult+"</b>");
}
response.getWriter().print("执行完成,<br/>:"+tmp);
}
}
然后用多个客户端的浏览器同时访问这个地址来模拟并发:http://192.168.1.101:8080/test
可以看到页面输出结果和数据库记录结果,你会发现可以并发执行了。
注意:以上测试中用到的log4j和dbutils等包需要自己添加。
