目录:
-
如何实现声明式事务
-
@Transactional注解属性
-
事物隔离级别
事务是一组原子操作单元,从数据库角度说,就是一组SQL指令,要么全部执行成功,若因为某个原因其中一条指令执行有错误,则撤销先前执行过的所有指令。更简答的说就是:要么全部执行成功,要么撤销不执行。
一、如何实现声明式事务:
1.添加spring-aspects-4.3.10.RELEASE.jar包
2.在application.xml中添加
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource">
</bean>
<!-- 启用事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager" />
3.在Service层public方法上添加事务注解——@Transactional
二、@Transactional注解属性:
1.rollbackFor
默认情况下,如果在事务中抛出了运行时异常(继承自RuntimeException异常类),则回滚事务;
没有抛出任何异常,或者抛出了检查时异常,则依然提交事务。
如果我们想在抛出某些检查时异常时回滚事务,我们可以样做。(例如:一个人有20元;要买3本,每本10元的书;这种书本 来有10本)
以下为没有rollback的情况:
//立即购买
@Override
@Transactional
public boolean insert(String userId,String bookId, int count) throws MoneyException {
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
这时,虽然报了 ''余额不足,购买失败...'' 。但在数据库中,书变成了7本,但是钱还是20。
这就是因为程序 执行到比较 money是否足够时,没有再往下执行,但是已经完成了上面book的比较和修改。没有回滚。
接下来看看运用了rollback的:
//立即购买
@Override
@Transactional(rollbackFor=MoneyException.class)
public boolean insert(String userId,String bookId, int count) throws MoneyException {
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
这样就很合理。如果金额不足无法购买。就会回滚,撤销对之前的操作。数据库中书的数目和钱都不会变。
2.timeout
设置一个事务所允许执行的最长时长(单位:秒),如果超过该时长且事务还没有完成,则自动回滚事务且出现 org.springframework.transaction.TransactionTimedOutException异常
//立即购买
@Override
@Transactional(timeout=3)
public boolean insert(String userId,String bookId, int count) {
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
// Thread.sleep(4000)会使得当前事务4秒之后结束,该时长超出了所允许的最长时长,因此事务自动回滚
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
注:timeout后跟的单位是s;Thread.sleep()里跟的单位是ms。
3.readOnly
事务只读,指对事务性资源进行只读操作。如果readOnly=true就是只能读不能改。
//立即购买
@Override
@Transactional(readOnly=true)
public boolean insert(String userId,String bookId, int count) {
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
注:@Transactional注解中添加了readOnly=true,但@Transactional注解修饰的方法涉及数据的修改,因此抛出如下异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
4.propagation
指定事务传播行为,一个事务方法被另一个事务方法调用时,必须指定事务应该如何传播,例如:方法可能继承在现有事务中运行,也可能开启一个新事物,并在自己的事务中运行。
REQUIRED:默认值,如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行
REQUIRES_NEW:当前方法必须启动新事务,并在它自己的事务内运行,如果有事务在运行,则把当前事务挂起,直到新的事务提交或者回滚才恢复执行
举个例子1:假如一个人有30元。要买2本A和2本B,A和B都是每本10元。
//立即购买
@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public boolean insert(String userId,String bookId, int count) {
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
会发现,数据库中A或B少了2本,钱变成10元。
举个例子2:假如一个人有30元。要买2本A和2本B,A每本10元,B每本50元。
我们会发现有时两本10元的书A会购买成功;有时不管A还是B都不会购买成功。
//购物车购买
ICarService carService = application.getBean(ICarService.class);
String userId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
Map<String,Integer> commodities = new HashMap<String,Integer>();
commodities.put("4c37672a-653c-4cc8-9ab5-ee0c614c7425",2);
commodities.put("a2f39533-659f-42ca-af91-c688a83f6e49",2);
carService.batch(userId, commodities);
这是因为Map集合中的key是无序的 ,我们无法控制是先买A还是先买B。
三、事物隔离级别
同一个应用程序中的多个事务或不同应用程序中的多个事务在同一个数据集上并发执行时, 可能会出现许多意外的问题,这些问题可分为如下三种类型:
1.脏读(Drity Read): 已知有两个事务A和B, A读取了已经被B更新但还没有被提交的数据,之后,B回滚事务,A读取的数据就是脏数据。
场景:公司发工资了,领导把5000元打到Tom的账号上,但是该事务并未提交,而Tom正好去查看账户,发现工资已经到账,账户多了5000元,非常高兴,可是不幸的是,领导发现发给Tom的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,Tom再次查看账户时发现账户只多了2000元,Tom空欢喜一场,从此郁郁寡欢,走上了不归路…... 分析:上述情况即为脏读,两个并发的事务:“事务B:领导给Tom发工资”、“事务A:Tom查询工资账户”,事务A读取了事务B尚未提交的数据 |
2.不可重复读(Non-repeatable read):已知有两个事务A和B,A 多次读取同一数据,B 在A多次读取的过程中对数据作了修改并提交,导致A多次读取同一数据时,结果不一致,例子:
场景:Tom拿着工资卡去消费,酒足饭饱后在收银台买单,服务员告诉他本次消费1000元,Tom将银行卡给服务员,服务员将银行卡插入POS机,POS机读到卡里余额为3000元,就在Tom磨磨蹭蹭输入密码时,他老婆以迅雷不及掩耳盗铃之势把Tom工资卡的3000元转到自己账户并提交了事务,当Tom输完密码并点击“确认”按钮后,POS机检查到Tom的工资卡已经没有钱,扣款失败,Tom十分纳闷,明明卡里有钱,于是怀疑POS有鬼,和收银小姐姐大打出手,300回合之后终因伤势过重而与世长辞,Tom老婆痛不欲生,郁郁寡欢,从此走上了不归路...... 分析:上述情况即为不可重复读,两个并发的事务,“事务A:POS机扣款”、“事务B:Tom的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新数据并提交了事务,而事务A再次读取该数据扣款时,数据已经发生了改变。
场景:Tom拿着工资卡去消费时,一旦POS机读取工资卡信息(即事务开始),Tom老婆即便进行了转账,待Tom输入密码并点击“确认”按钮后,POS机检查到Tom工资卡上余额没有变化,最终扣款成功。 分析:上述情况即为重复读 |
3.幻读(Phantom Read): 已知有两个事务A和B,A从一个表中读取了数据,然后B在该表中插入了一些新数据,导致A再次读取同一个表, 就会多出几行,简单地说,一个事务中先后读取一个范围的记录,但每次读取的纪录数不同,称之为幻象读,例子:
场景:Tom的老婆工作在银行部门,她时常通过银行内部系统查看Tom的工资卡消费记录。2019年5月的某一天,她查询到Tom当月工资卡的总消费额(select sum(amount) from record where card_id='6226090219290000' and date_format(create_time,'%Y-%m')='2019-05')为80元,Tom的老婆非常吃惊,心想“老公真是太节俭了,嫁给他真好!”,而Tom此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录并提交了事务,沉浸在幸福中的老婆查询了Tom当月工资卡消费明细(select amount from record where card_id='6226090219290000' and date_format(create_time,'%Y-%m')='2019-05')一探究竟,可查出的结果竟然发现有一笔1000元的消费,Tom的老婆瞬间怒气冲天,外卖订购了一个大号的榴莲,傍晚降临,Tom生活在了水深火热之中,只感到膝盖针扎的痛...... 分析:上述情况即为幻读,两个并发的事务,“事务A:获取事务B消费记录”、“事务B:添加了新的消费记录”,事务A获取事务B消费记录时数据多出了一条。 场景:教师A执行SQL语句将数据库中所有学生的成绩从具体分数改为ABCDE等级制,该SQL语句执行后事务提交前,教师B插入了一条具体分数的记录并提交了事务,教师A事务提交并执行查询SQL语句,此时教师A发现还有一条记录没有改过来,就好像发生了幻觉一样。 |
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
解决方案
根据实际需求,通过设置数据库的事务隔离级别可以解决多个事务并发情况下出现的脏读、不可重复读和幻读问题,数据库事务隔离级别由低到高依次为Read uncommitted、Read committed、Repeatable read和Serializable等四种。数据库不同,其支持的事务隔离级别亦不相同:MySQL数据库支持上面四种事务隔离级别,默认为Repeatable read;Oracle 数据库支持Read committed和Serializable两种事务隔离级别,默认为Read committed。
1、Read uncommitted(读未提交):可能出现脏读、不可重复读和幻读。
2、Read committed(读提交):可以避免脏读,但可能出现不可重复读和幻读。大多数数据库默认级别就是Read committed,比如Sql Server数据库和Oracle数据库。注意:该隔离级别在写数据时只会锁住相应的行。
3、Repeatable read(重复读):可以避免脏读和不可重复读,但可能出现幻读。注意:事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
4、Serializable(序列化):可以避免脏读、不可重复读和幻读,但是并发性极低,一般很少使用。注意:该隔离级别在读写数据时会锁住整张表。
MySQL的事务隔离级别:
查看:输入以下语句执行 select @@global.tx_isolation,@@tx_isolation;
修改:
1.全局修改
①、在my.ini配置文件最后加上如下配置:
#可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
[mysqld]
transaction-isolation = READ-UNCOMMITTED
②、重启MySQL服务
2.当前session修改,登录MySQL数据库后执行如下命令:
set session transaction isolation level read uncommitted;
另外:用java代码实现,在@Transactional() 的()里面加上属性,如图所示:
如果()里面的事务隔离等级和是数据库中的不一致,那么在执行Java程序时,会按照()里面的事务隔离等级,相当于一次性的,暂时的。事务结束,还是按照数据库中的事务隔离等级。而且数据库中的事务隔离等级还是全局的,java代码中的只是局部的。