什么时候回滚事务?
在spring的事务管理中我们首先要明白这个问题,一般是在抛出运行期异常的时候会进行事务的回滚。而spring的声明式事务管理只接受运行期异常。
异常通常分为运行期异常和编译期异常。
在java中常见的运行期异常有:
NullPointerException - 空指针引用异常
ClassCastException - 类型强制转换异常。
IndexOutOfBoundsException - 下标越界异常
NumberFormatException - 数字格式异常
IllegalArgumentException - 传递非法参数异常。
java.lang.NoSuchMethodError-方法不存在异常
java.sql.SQLException -Sql语句执行异常
java.io.IOException -输入输出异常
在java中对异常的处理方式通常有两种:
A:当前方法明确知道该如何处理该异常,程序应该使用try-catch来捕捉该异常,然后再抵押的catch块中修补该异常。
B:当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。
但是运行期异常比较灵活,不需要手动的去try-catch,如果程序需要捕捉运行时异常,也可以通过try-catch块来捕捉。
下面的一段代码就是我们自定义一个运行时异常:
package cn.codingxiaxw.exception;
/**
* 重复秒杀异常,是一个运行期异常,不需要我们手动try catch
* Mysql只支持运行期异常的回滚操作
*/
public class RepeatKillException extends SeckillException {
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
可以看出这个重复秒杀的异常继承了SeckillException,这个SeckillException也是我们自定义的异常:
package cn.codingxiaxw.exception;
/**
* 秒杀相关的所有业务异常
* Created by codingBoy on 16/11/27.
*/
public class SeckillException extends RuntimeException {
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
SeckillException 异常继承了RuntimeException ,表明他是一个运行时异常,这里定义了两个构造方法,可以在抛出异常时说明错误的信息。
这里使用RepeatKillException 继承SeckillException 的好处就是,如果遇到重复秒杀的行为,我们可以抛出RepeatKillException ,其他的异常可以抛出SeckillException 。这样对于重复秒杀这些需要特别注意的操作,我们抛出异常时可以知道是因为重复秒杀而造成的。
再来看声明式事务管理,他有三种使用方式:
① ProxyFactoryBean+XML的方式:这是早期的使用方式,现在使用的比较少。
② Tx:advice+aop命名空间,这种配置一次配置永久生效
③ 通过注解@Transactional 。
这里我们是通过注解的方式, 使用注解控制事务方法的优点:
1.开发团队达成一致约定,明确标注事务方法的编程风格
2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制
这里我们通过sql插入一条数据。
INSERT ignore INTO success_killed(seckill_id,user_phone,state)
VALUES (#{seckillId},#{userPhone},0)
当出现主键冲突时(即重复秒杀时),会报错;不想让程序报错,加入ignore。
我们可以根据执行insert语句返回的int类型的值进行异常处理。
@Transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException {
if (md5==null||!md5.equals(getMD5(seckillId)))
{
throw new SeckillException("seckill data rewrite");//秒杀数据被重写了
}
//执行秒杀逻辑:减库存+增加购买明细
Date nowTime=new Date();
try{
//否则更新了库存,秒杀成功,增加明细
int insertCount=successKilledDao.insertSuccessKilled(seckillId,userPhone);
//看是否该明细被重复插入,即用户是否重复秒杀
if (insertCount<=0)
{
throw new RepeatKillException("seckill repeated");
}else {
//减库存,热点商品竞争
int updateCount=seckillDao.reduceNumber(seckillId,nowTime);
if (updateCount<=0)
{
//没有更新库存记录,说明秒杀结束 rollback
throw new SeckillCloseException("seckill is closed");
}else {
//秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit
SuccessKilled successKilled=successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS,successKilled);
}
}
}catch (SeckillCloseException e1)
{
throw e1;
}catch (RepeatKillException e2)
{
throw e2;
}catch (Exception e)
{
logger.error(e.getMessage(),e);
//所以编译期异常转化为运行期异常
throw new SeckillException("seckill inner error :"+e.getMessage());
}
}
最后执行插入的方法,返回值如果错误就会抛出异常,从而事务回滚,实现对事务的控制。