1 顶层方法@Transactional
开启事务的方法A调用方法B,方法B中插入一条数据,并立刻查询返回,结果Mybatis打印插入成功,但是查询失败,返回为null
@Transactional
public void A(){
B();
}
public TeamConsumeAmountRecord B(String uid){
Date beginOfMonth = DateUtils.beginOfMonth();
TeamConsumeAmountRecordExample e = new TeamConsumeAmountRecordExample();
e.createCriteria().andUserIdEqualTo(uid).andDateEqualTo(beginOfMonth);
List<TeamConsumeAmountRecord> list = selectByExample(e);
int returnId=0;
//找不到记录或者和今天不是同一天,插入新记录
if(list.size()==0 || !DateUtils.isSameMonth(list.get(0).getDate())) {
returnId= insertOneNewTeamRecommendRecord(uid);
log.debug("返回自增长id:{}",returnId);
TeamConsumeAmountRecord res = selectByPrimaryKey(returnId + "");
log.debug("搜索结果:{}",res);
return res;
}
return list.get(0);
}
/**
* 插入一个空团队推荐的记录,返回自增长id
*/
public int insertOneNewTeamRecommendRecord(String uid){
TeamConsumeAmountRecord record = new TeamConsumeAmountRecord();
record.setAmount(new BigDecimal(0));
record.setUserId(uid);
record.setDate(DateUtils.beginOfMonth());
teamConsumeAmountRecordMapper.insertSelectiveAndReturn(record);
return record.getId();
}
插入成功,但是查询失败,返回为null:
2021-02-12 23:03:22.116 -> [http-nio-8080-exec-4] -> DEBUG c.f.v.m.a.T.insertSelectiveAndReturn - ==> Preparing: insert into c_team_consume_amount_record ( user_id, amount, date ) values ( ?, ?, ? )
2021-02-12 23:03:22.120 -> [http-nio-8080-exec-4] -> DEBUG c.f.v.m.a.T.insertSelectiveAndReturn - ==> Parameters: 12(String), 0(BigDecimal), 2021-02-01 00:00:00.0(Timestamp)
2021-02-12 23:03:22.178 -> [http-nio-8080-exec-4] -> DEBUG c.f.v.m.a.T.insertSelectiveAndReturn - <== Updates: 1
2021-02-12 23:03:22.182 -> [http-nio-8080-exec-4] -> DEBUG com.fc.v2.service.TeamConsumeAmountRecordService - 返回自增长id:1
2021-02-12 23:03:22.183 -> [http-nio-8080-exec-4] -> DEBUG c.f.v.m.a.T.selectByPrimaryKey - ==> Preparing: select id, user_id, amount, date from c_team_consume_amount_record where id = ?
2021-02-12 23:03:22.183 -> [http-nio-8080-exec-4] -> DEBUG c.f.v.m.a.T.selectByPrimaryKey - ==> Parameters: 1(Integer)
2021-02-12 23:03:22.213 -> [http-nio-8080-exec-4] -> DEBUG c.f.v.m.a.T.selectByPrimaryKey - <== Total: 0
2021年2月14日勘误:这里其实可能是因为返回的自增长id不对,所以查不到。。。实际上一个事务里就算是可重复读,自己的插入操作是可以查询到的。
2 PROPAGATION_REQUIRES_NEW 开启独立事务
查询过后,建议对B方法的插入开启独立事务,也就是在插入方法insertOneNewTeamRecommendRecord
上新加@Transactional
注解,同时传播级别为PROPAGATION_REQUIRES_NEW
:
//无论当前事务是否存在,都会创建新事务运行方法,这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立 REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
参考:事务隔离级别和传播行为以及@Transactional使用规范
/**
* 插入一个空团队推荐的记录,返回自增长id
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int insertOneNewTeamRecommendRecord(String uid){
TeamConsumeAmountRecord record = new TeamConsumeAmountRecord();
record.setAmount(new BigDecimal(0));
record.setUserId(uid);
record.setDate(DateUtils.beginOfMonth());
teamConsumeAmountRecordMapper.insertSelectiveAndReturn(record);
return record.getId();
}
但是无效,记录没有被插入,其他操作被回滚,因此也看不到变化。
3 service自己注入自己,再调用内部方法
查询到,如果在一个类里调用这个类的方法,其上的@Transactional
会无效,因为不会对这个方法进行代理。
参考:spring 事务REQUIRES_NEW 不起作用的解决方法 spring 的事务传播这边就不提了,各种可百度到。但在用REQUIRES_NEW的时候,发现没有起作用。 原因:spring的事务管理通过切面实现,如果直接使用this.方法()或者方法(),不会触发切面中对事务的管理。应使用该方法所在的类的实例.方法()。 解决方案1:需要将两个方法分别写在不同的类里。 解决方案2:方法写在同一个类里,但调用B方法的时候,将service自己注入自己,用这个注入对象来调用B方法。 使用方案2解决了我遇到的问题 **
参考:事务的传播无效,required_new无效,动态代理给spring事务传播留下的坑 / required_new spring事务传播行为无效碰到的坑! 只有代理对象proxy直接调用的那个方法才是真正的走代理的,嵌套的方法实际上就是 直接把嵌套的代码移动到代理的方法里面。 所以,嵌套的事务都不能生效。
使用方案2,发现插入新数据成功,**但是查询仍旧失败,其他操作回滚。
2021-02-13 21:12:28.081 -> [http-nio-8080-exec-2] -> DEBUG c.f.v.m.a.T.insertSelectiveAndReturn - ==> Preparing: insert into c_team_consume_amount_record ( user_id, amount, date ) values ( ?, ?, ? )
2021-02-13 21:12:28.083 -> [http-nio-8080-exec-2] -> DEBUG c.f.v.m.a.T.insertSelectiveAndReturn - ==> Parameters: 15(String), 0(BigDecimal), 2021-02-01 00:00:00.0(Timestamp)
2021-02-13 21:12:28.139 -> [http-nio-8080-exec-2] -> DEBUG c.f.v.m.a.T.insertSelectiveAndReturn - <== Updates: 1
2021-02-13 21:12:28.162 -> [http-nio-8080-exec-2] -> DEBUG com.fc.v2.service.TeamConsumeAmountRecordService - 返回自增长id:21
2021-02-13 21:12:28.163 -> [http-nio-8080-exec-2] -> DEBUG c.f.v.m.a.T.selectByPrimaryKey - ==> Preparing: select id, user_id, amount, date from c_team_consume_amount_record where id = ?
2021-02-13 21:12:28.165 -> [http-nio-8080-exec-2] -> DEBUG c.f.v.m.a.T.selectByPrimaryKey - ==> Parameters: 21(Integer)
2021-02-13 21:12:28.173 -> [http-nio-8080-exec-2] -> DEBUG c.f.v.m.a.T.selectByPrimaryKey - <== Total: 0
2021-02-13 21:12:28.174 -> [http-nio-8080-exec-2] -> WARN com.fc.v2.service.TeamConsumeAmountRecordService - null:获取团队消费业绩记录失败
4 设置未提交读
查询以为是隔离级别的问题,在A方法上设置未提交读,这样可以读取到未提交的数据:
2.隔离级别详解
我们 主要针对第二种丢失更新,为了压制丢失更新,数据库标准提出了4类不同的隔离级别,分别为未提交读(read uncommitted)、读写提交(read commited)、可重复读和串行化。提出4种不同的隔离级别是出于性能的考虑 未提交读(read uncommitted)会产生脏读 未提交读是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。未提交读是一种危险的隔离级别,所以一般在我们实际的开发中应用不广 , 但是它的优点在于并发能力高,适合那些对数据一致性没有要求而追求高并发的场景 ,它的最大坏处是出现脏读 。 读写提交(read committed)会产生不可重复读 读写提交隔离级别,是指一个事务只能读取另一个事务已经提交的数据,不能读取未提交的数据。 可重复读 会产生幻读 可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能出现一些值的变化, 影响当前事务的执行,如上述的库存是个变化的值,这个时候数据库提出 了可重复读的隔离级别 串行化(Serializable) 串行化(Serializable)是数据库最高的隔离级别,它会要求所有的 SQL 都会按照顺序执行,这样就可以克服上述隔离级别出现的各种问题,所以它能够完全保证数据的一致性 。 参考:Springboot中的数据库事务
A方法修改如下:
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void A(){
B();
}
但是无效。
**
怀疑事务中的查询操作没法查询到未提交的插入数据。但是前面的操作都无效,可能是配置有问题。
2021年2月14日更新:
如果想要查询到,除了设置A为未提交读,查询操作应该放在
5 罪魁祸首:快照读
MySQL的innodb引擎默认采用可重复读隔离级别,Spring的事务管理采用默认DEFAULT隔离级别时自动获取为可重复读
。为了实现可重复读,mysql使用两种技术:第一个是mvcc(多版本控制)
,第二个是next-key技术(间隙锁)
。
可以简单理解为mvcc实现快照读,next-key实现当前读。使用的语句不同,采用技术不同:
- 快照读(snapshot read) 简单的select操作(不包括 select … lock in share mode, select … for update) 2.当前读(current read)
select … lock in share mode
select … for update
insert
update
delete 参考:当前读和快照读
spring隔离级别
DEFAULT
:默认值,表示使用底层数据库的默认隔离级别。大部分数据库为READ_COMMITTED(**MySql默认隔离级别为REPEATABLE**)
回到上文,REQUIRES_NEW
会开启新事务,挂起旧事务,旧事务抛出异常时,新事务不会回滚,因此可以看到插入的新纪录。
但是由于旧事务是RR(可重复读)级别,第一次普通查询后生成快照,之后的普通查询都是按照快照查询,因此看不到新插入的数据,返回null。
参考:mysql事务提交后为什么看不到最新的记录 这个问题的原因是因为select是属于快照查询,当你开启一个事务之后,进行了第一次查询,这时候mysql就会记录下当前的事务版本,后面的查询都会只查询这个版本的数据,而其他事务里面进行了insert,update,delete操作会产生一个新的版本数据,所以才会查不出来
解决方法:使用当前读而不是快照读
插入新记录后的查询操作,修改为:
TeamConsumeAmountRecord res = selectByPrimaryKeyOnLock(returnId + "");
selectByPrimaryKeyOnLock
的sql语句改为:
select * from c_team_consume_amount_record
where id = #{id,jdbcType=INTEGER}
FOR UPDATE
最重要的是在最后加上了FOR UPDATE
,那么这个select就是当前读,可以获取当前数据库的真实数据。
最后操作成功!
贴上全部代码:
//自己注入自己
@Autowired
private TeamConsumeAmountRecordService teamConsumeAmountRecordService;
//对A所有数据库操作开启一个事务
@Transactional
public void A(String uid){
B(uid);
}
//B方法
public TeamConsumeAmountRecord B(String uid){
Date beginOfMonth = DateUtils.beginOfMonth();
TeamConsumeAmountRecordExample e = new TeamConsumeAmountRecordExample();
e.createCriteria().andUserIdEqualTo(uid).andDateEqualTo(beginOfMonth);
List<TeamConsumeAmountRecord> list = selectByExample(e);
int returnId=0;
if(list.size()==0 || !DateUtils.isSameMonth(list.get(0).getDate())) {
//开启独立事务的插入方法
returnId = teamConsumeAmountRecordService.insertOneNewTeamRecommendRecord(uid);
log.debug("返回自增长id:{}",returnId);
}
if(list.size()!=0){
return list.get(0);
}else{
//使用当前读的查询方法
TeamConsumeAmountRecord res = selectByPrimaryKeyOnLock(returnId + "");
log.warn("null:获取团队消费业绩记录失败");
return res;
}
}
/**
* 插入一个空团队推荐的记录,返回自增长id,这里开启新事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int insertOneNewTeamRecommendRecord(String uid){
TeamConsumeAmountRecord record = new TeamConsumeAmountRecord();
record.setAmount(new BigDecimal(0));
record.setUserId(uid);
record.setDate(DateUtils.beginOfMonth());
teamConsumeAmountRecordMapper.insertSelectiveAndReturn(record);
return record.getId();
}
selectByPrimaryKeyOnLock(id)执行的sql:
select * from c_team_consume_amount_record
where id = #{id,jdbcType=INTEGER}
FOR UPDATE
实验
开启两个查询,先执行:
查询1:
begin;
select * from t_test;
结果为:
再执行:
查询2:
begin;
UPDATE t_test set name="b" where id = 1;
select * from t_test;
结果为:更新成功,但是没提交。
这时候再执行查询1的select * from t_test;
语句,结果为:
和之前查询1的结果一样。证明实现了可重复读。
但是如果查询和更新(插入)放在同一个事务,那么怎么样都是可以获取最新结果的。
思考
mysql快照生成时间?
RR级别的事务第一次普通查询后生成快照。
参考:Mysql可重复读(1) —— 快照何时创建
因此事务中,先普通查询一次,再进行插入操作,之后的普通查询都是快照读,无法获取数据库真实数据(看不到刚刚插入的数据)。
解决方法:第二次查询使用当前读,最简单的是select ... for update
。
如果需要某个数据库操作不会被其他错误回滚,那么可以开启新事务@Transactional(propagation = Propagation.REQUIRES_NEW)