1 事务概述
2 转账案例的实现
2.1 mysql操作事务(了解)
sqlyog会让事务失效,所以试验的时候得用控制台。
sql语句 | 描述 |
---|---|
start transaction; | 开启事务 |
commit; | 提交事务 |
rollback; | 回滚事务 |
mysql事务是默认自动提交。
扩展:Oracle数据库事务不自动提交。
2.2 jdbc操作事务
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import cn.itcast.c3p0utils.C3P0Utils;
/*
使用jdbc完成转账案例。
我们要把加钱和减钱看成是同一组操作,这组操作要么都成功,要么都失败。
可以使用事务去解决这个问题。
步骤:
转账之前开启事务。
如果整个过程没有出现问题,那么就提交事务。
如果整个过程出现了问题,那么就回滚。
如果使用jdbc操作事务,需要使用连接(Connection)对象去操作事务。
void setAutoCommit(boolean autoCommit):如果传递false,就是设置事务手动提交。也就是开启了事务。
void commit():提交事务
void rollback(): 回滚事物。
*/
public class Demo01JDBCTransaction {
public static void main(String[] args) {
Connection con = null;
try {
// 使用工具类获取一个连接
con = C3P0Utils.getConnection();
//通过连接获取到sql语句执行者对象
Statement st = con.createStatement();
//在sql语句执行前开启事务,保证后面的所有的sql语句都是同一组操作。
con.setAutoCommit(false);//false表示手动提交事务,也就是开启了事务
//执行sql语句
//先给jack的钱减去100
int row1 = st.executeUpdate("UPDATE account SET money=money-100 WHERE name='jack'");
System.out.println("row1:" + row1);
//模拟停电
System.out.println(10 / 0);
//给rose的钱加上100
int row2 = st.executeUpdate("UPDATE account SET money=money+100 WHERE name='rose'");
System.out.println("row2: " + row2);
//如果没有问题,就提交事务
con.commit();
System.out.println("转账成功");
} catch (Exception e) {
//如果转账过程出现问题,catch会捕获到这个异常,那么我们就在catch回滚事物
try {
if(con != null) {//非空判断,让程序更加的健壮(鲁棒)
con.rollback();
System.out.println("转账失败");
}
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
}
2.3 dbutils操作事务
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
import cn.itcast.c3p0utils.C3P0Utils;
/*
使用DBUtils完成转账案例。
需要把加钱和减钱看成一组操作,要么都成功要么都失败。
还得用事务。
DBUtils操作事务也是通过连接(Connection)对象去操作。
DBUtils里面的QueryRunner在创建对象的时候一定要使用空参构造去创建对象。(因为另一种拿不到连接,没有连接就操作不了事务)
开启事物,提交事务,回滚事物和jdbc的操作是一样的。
步骤:
1. 创建QueryRunner对象(使用空参构造创建对象)
2. 获取到一个连接。
3. 使用这个连接开启事务。
4. 执行转账的sql语句。
5. 如果整个过程没有遇到问题,那么就提交。
如果整个过程遇到了问题,那么就回滚。
*/
public class Demo01DBUtilsTransaction {
public static void main(String[] args) {
Connection con = null;
try {
//创建QueryRunner对象(使用空参构造创建对象)
QueryRunner qr = new QueryRunner();
//获取到一个连接。
con = C3P0Utils.getConnection();
//使用这个连接开启事务。
con.setAutoCommit(false);
//执行转账的sql语句。
String sqlA = "UPDATE account SET money=money-100 WHERE name='jack'";
qr.update(con, sqlA);
//模拟停电
//System.out.println(10 / 0);
String sqlB = "UPDATE account SET money=money+100 WHERE name='rose'";
qr.update(con, sqlB);
//如果整个过程没有遇到问题,那么就提交。
con.commit();
System.out.println("转账成功");
} catch (Exception e) {
//如果遇到问题,就回滚
try {
if(con != null) {
con.rollback();
}
} catch (SQLException e1) {
e1.printStackTrace();
}
System.out.println("转账失败");//也可以把提示放在这个位置。
//如果需要调试错误,建议加上下面代码
//e.printStackTrace();// 将异常信息使用标准的错误流打印到控制台(由于这个代码异常消耗性能,所以在调试阶段一般用这个直接显示出来,等到运行阶段,要把他换成日志。)
}
}
}
3 三层架构介绍
4 转账案例用三层架构模式改写
4.1 分析
4.2 准备工作
数据库准备
# 创建一个表:账户表.
create database webdb;
# 使用数据库
use webdb;
# 创建账号表
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);
# 初始化数据
insert into account values (null,'jack',10000);
insert into account values (null,'rose',10000);
insert into account values (null,'tom',10000);
4.3 dao层的实现
package cn.myl021.dao;
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
import cn.myl021.utils.C3P0Utils;
/*
转账案例dao层。
这一层我们要对数据库进行增删改查。
这一层被service层调用。
给出两个方法:
出账方法
入账方法
在一组事务操作中,必须要使用的是同一个连接。
可以使用参数的形式把连接传递过来。
在Service层获取连接,然后Service调用dao的时候把连接传递过来,dao层就用Service传递过来的连接就可以了
*/
public class AccountDao {
/*
* 出账方法
* 参数: 出账人姓名, 金额
* 返回值: 不需要
*/
public void outMoney(Connection con, String name, double money) {
try {
//创建一个QueryRunner对象
QueryRunner qr = new QueryRunner();
//执行sql
String sql = "UPDATE account SET money=money-? WHERE name=?";
Object[] params = {money, name};
qr.update(con, sql, params);
} catch (Exception e) {
//手动抛出异常
throw new RuntimeException("出账失败");
}
}
/*
* 入账方法
* 参数: 入账人姓名, 金额
* 返回值: 不需要
*/
public void inMoney(Connection con, String name, double money) {
try {
//创建一个QueryRunner对象
QueryRunner qr = new QueryRunner();
//执行sql
String sql = "UPDATE account SET money=money+? WHERE name=?";
Object[] params = {money, name};
qr.update(con, sql, params);
} catch (Exception e) {
//手动抛出异常
throw new RuntimeException("入账失败");
}
}
}
4.4 service层的实现
package cn.myl021.service;
import java.sql.Connection;
import java.sql.SQLException;
import cn.myl021.dao.AccountDao;
import cn.myl021.utils.C3P0Utils;
/*
转账案例Service层。
这一层用来处理业务上的数据。
这一层会调用dao层,被view层
定义一个转账方法步骤:
1. 获取连接,然后开启事务。
2. 给出账人减去对应的金额(调用dao层)
3. 给入账人加上对应的金额(调用dao层)
4. 如果整个过程没有问题,就提交。
如果真个过程出现问题,那么回滚
*/
public class AccountService {
/*
* 转账方法。
* 参数: 出账人姓名, 入账人姓名, 金额。
* 返回值:void
*/
public void transfer(String outName, String inName, double money) {
Connection con = null;
try {
//获取连接,然后开启事务。
con = C3P0Utils.getConnection();
con.setAutoCommit(false);
//给出账人减去对应的金额(调用dao层)
AccountDao dao = new AccountDao();
dao.outMoney(con, outName, money);
//模拟错误
//System.out.println(10/0);
//给入账人加上对应的金额(调用dao层)
dao.inMoney(con, inName, money);
//如果整个过程没有问题,就提交。
con.commit();
} catch (Exception e) {
//回滚事务
try {
if(con != null) {
con.rollback();
}
} catch (SQLException e1) {
e1.printStackTrace();
}
//如果转账失败,那么就手动抛出一个异常,告诉调用者我出现问题了。
throw new RuntimeException(e.getMessage());
}
}
}
4.5 view层的实现
package cn.myl021.view;
import cn.myl021.service.AccountService;
/*
转账案例的view
用来给用户展示,或者让用户录入信息。
可以让用户录入入账人姓名,出账人姓名, 金额,然后调用Service层把这些数据传递过去,然后Service层进行一个转账处理。
*/
public class AccontView {
public static void main(String[] args) {
String outName = "jack";
String inName = "rose";
double money = 100;
//调用Service层进行转账操作
AccountService service = new AccountService();
try {
service.transfer(outName, inName, money);
System.out.println("转账成功");
} catch (Exception e) {
System.out.println("转账失败");
System.out.println(e.getMessage());
}
}
}
5 ThreadLocal介绍
/*
ThreadLocal 叫做线程局部变量。
作用:可以向当前线程上面绑定数据, 然后在当前线程的所有位置共享。
void set(T value):将数据添加(绑定)到当前线程上面,可以在当前线程的所有位置共享。
T get():获取当前线程上面绑定的数据。 这个方法是通过哪个线程调用的,获取的就是哪个线程上的数据
void remove():移除当前线程上绑定的数据。
一个ThreadLocal对象只能在一个线程上面绑定一个数据。 如果要绑定多个数据,需要创建多个ThreadLocal对象。
在ThreadLocal数据使用完之后要remove掉。否则会引发内存泄漏。因为ThreadLocal内部结构非常复杂,内部是map集合,叫ThreadLocalMap,key是当前的ThreadLocal对象value是线程上面绑定的数据。而ThreadLocalMap里面是一个Entry[]数组,这个Entry是他的一个内部类,继承了WeakRefence是弱引用(比如平常的Student s = new Student(); 是强引用,只要这个对象被强引用关联着,他就永远不会被回收,java中的四种引用的回收时机是不一样的。)
*/
public class Demo01ThreadLocal {
public static void main(String[] args) {
//创建ThreadLocal对象
final ThreadLocal<String> local = new ThreadLocal<>(); //加final的原因是局部内部类访问外部类的成员要加final,而匿名内部类是局部内部类的一种。
//添加数据
local.set("狗蛋"); //这个方法是在main线程中运行的,所以是把这个数据绑定到了main线程上面
//local.remove();
//创建一个新的线程
new Thread() {
@Override
public void run() {
local.set("旺财");
System.out.println("新线程:" + local.get());//获取到的就是新线程上面绑定的数据
}
}.start();
System.out.println("main:" + local.get());
}
}
// 强引用, 软引用, 弱引用, 虚引用
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("新线程执行了");
}
}
6 用ThreadLocal改写转账案例
6.1 添加一个工具类
package cn.myl021.utils;
/*
连接管理类,可以从当前线程上面获取连接。
所以需要ThreadLocal
定义以下方法:
1. 获取连接(从线程上面获取)
2. 开启事务。
3. 提交事务
4. 回滚事务。
*/
import java.sql.Connection;
import java.sql.SQLException;
public class ConnectionManager {
//定义ThreadLocal,用来给每个线程保存对应的数据库连接
private static ThreadLocal<Connection> local = new ThreadLocal<>();
/*
* 获取连接(从线程上面获取)
*/
public static Connection getConnection() throws Exception {
//从当前线程上面获取连接
Connection con = local.get();
//如果之前我们没有往这个线程上面放过连接,那么con就是null
if(con == null) {
//如果之前没有放过连接,那么就往里面添加一个连接。
con = C3P0Utils.getConnection(); //从连接池中获取一个新的连接
local.set(con);//把这个连接绑定到当前线程上
}
return con;
}
/*
* 开启事务。
*/
public static void startTransaction() throws Exception {
getConnection().setAutoCommit(false);//从当前线程上面获取连接,然后开启事务
}
/*
* 提交事务
*/
public static void commitTransaction() throws Exception {
getConnection().commit();
}
/*
* 回滚事物
*/
public static void rollbackTransaction() throws Exception {
getConnection().rollback();
}
}
6.1 service层
package cn.myl021.service;
import java.sql.Connection;
import java.sql.SQLException;
import cn.myl021.dao.AccountDao;
import cn.myl021.utils.C3P0Utils;
import cn.myl021.utils.ConnectionManager;
/*
转账案例Service层。
这一层用来处理业务上的数据。
这一层会调用dao层,被view层
定义一个转账方法步骤:
1. 获取连接,然后开启事务。
2. 给出账人减去对应的金额(调用dao层)
3. 给入账人加上对应的金额(调用dao层)
4. 如果整个过程没有问题,就提交。
如果真个过程出现问题,那么回滚
*/
public class AccountService {
/*
* 转账方法。
* 参数: 出账人姓名, 入账人姓名, 金额。
* 返回值:void
*
* outName:jack
* inName:rose
* money:100
*/
public void transfer(String outName, String inName, double money) {
try {
//直接调用ConnectionManger里面的
ConnectionManager.startTransaction();
//给出账人减去对应的金额(调用dao层)
AccountDao dao = new AccountDao();
dao.outMoney(outName, money);
//模拟错误
System.out.println(10/0);
//给入账人加上对应的金额(调用dao层)
dao.inMoney(inName, money);
//如果整个过程没有问题,就提交。
ConnectionManager.commitTransaction();
} catch (Exception e) {
//回滚事务
try {
ConnectionManager.rollbackTransaction();
} catch (Exception e1) {
e1.printStackTrace();
}
//如果转账失败,那么就手动抛出一个异常,告诉调用者我出现问题了。
throw new RuntimeException(e.getMessage());
}
}
}
6.2 dao层
package cn.myl021.dao;
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
import cn.myl021.utils.C3P0Utils;
import cn.myl021.utils.ConnectionManager;
/*
转账案例dao层。
这一层我们要对数据库进行增删改查。
这一层被service层调用。
给出两个方法:
出账方法
入账方法
在一组事务操作中,必须要使用的是同一个连接。
可以使用参数的形式把连接传递过来。
在Service层获取连接,然后Service调用dao的时候把连接传递过来,dao层就用Service传递过来的连接就可以了
*/
public class AccountDao {
/*
* 出账方法
* 参数: 出账人姓名, 金额
* 返回值: 不需要
*/
public void outMoney(String name, double money) {
try {
//创建一个QueryRunner对象
QueryRunner qr = new QueryRunner();
//执行sql
String sql = "UPDATE account SET money=money-? WHERE name=?";
Object[] params = {money, name};
qr.update(ConnectionManager.getConnection(), sql, params);
} catch (Exception e) {
//手动抛出异常
throw new RuntimeException("出账失败");
}
}
/*
* 入账方法
* 参数: 入账人姓名, 金额
* 返回值: 不需要
*/
public void inMoney(String name, double money) {
try {
//创建一个QueryRunner对象
QueryRunner qr = new QueryRunner();
//执行sql
String sql = "UPDATE account SET money=money+? WHERE name=?";
Object[] params = {money, name};
qr.update(ConnectionManager.getConnection(), sql, params);
} catch (Exception e) {
//手动抛出异常
throw new RuntimeException("入账失败");
}
}
}
7 事务的四个特性
原子性<br /> 事务里面所有的操作是不可分割的,并且这组操作要么都成功,要么都失败。<br /> 一致性<br /> 事务操作前后数据要是一致的。<br /> 隔离性<br /> 多个事务之间应该互不干扰。<br /> 持久性<br /> 事务一旦提交,就永远的生效了。
8 并发访问问题以及隔离级别
8.1 如果不考虑隔离性,会有3种并发访问问题
如果不考虑隔离性,事务存在3中并发访问问题。
1. 脏读:一个事务读到了另一个事务未提交的数据.
2. 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务在事务中的多次查询结果不一致。
3. 虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事在事务中多次查询的结果不一致。
8.2 数据库的4种隔离级别
(1)read uncommitted 读取未提交
什么都不能解决。会引发脏读,不可重复读、幻读。
1)所有事务都可以看到其他未提交事务的执行结果
2)本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少
3)该级别引发的问题是——脏读(Dirty Read):读取到了未提交的数据
(2)read committed(oracle默认)读取已提交
能解决脏读。会引发不可重复读、幻读。
1)这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)
2)它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
3)这种隔离级别出现的问题是——不可重复读(Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一 样的结果。导致这种情况的原因可能有:
a.有一个交叉的事务有新的commit,导致了数据的改变;
b.一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit
(3)repeatable read(mysql默认)可重复读
能解决脏读、不可重复读。会引发幻读。
1)这是MySQL的默认事务隔离级别
2)它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
3)此级别可能出现的问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行
(4)serializable 串行化
能解决所有问题。但是同时只能执行一个事务,相当于事务的单线程。效率最低。
(1)这是最高的隔离级别
(2)它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。
(3)在这个级别,可能导致大量的超时现象和锁竞争
演示
读未提交:read uncommitted
A窗口设置隔离级别
AB同时开始事务
A 查询
B 更新,但不提交
A 再查询?— 查询到了未提交的数据
B 回滚
A 再查询?— 查询到事务开始前数据
读已提交:read committed
A窗口设置隔离级别
AB同时开启事务
A查询
B更新、但不提交
A再查询?—数据不变,解决问题【脏读】
B提交
A再查询?—数据改变,存在问题【不可重复读】
可重复读:repeatable read
A窗口设置隔离级别
AB 同时开启事务
A查询
B更新, 但不提交
A再查询?—数据不变,解决问题【脏读】
B提交
A再查询?—数据不变,解决问题【不可重复读】
A提交或回滚
A再查询?—数据改变,另一个事务
串行化:serializable
A窗口设置隔离级别
AB同时开启事务
A查询
B更新?—等待(如果A没有进一步操作,B将等待超时)
A回滚
B 窗口?—等待结束,可以进行操作