遇到了一个关于for update用索引锁行的问题,悬而未解
又是一次曲线救国。求解答。
一个在事务里面的for update ,可以被多个事务获取返回值,而不是等待上个事务结束,才获取for update查询的结果。
for update在事务中为什么没有锁住根据索引字段作为条件查询得到数据,一个事务没有结束别的事务获取上个事务for update得到返回结果。表已将查询条件添加到索引。
业务代码放到spring的编程式事务里。具体流程是,1、根据索引for update查询结果 2、根据查询结果插入一条新记录 3、更新刚插入的记录。出现问题,当插入更新记录成功之后,但是事务还没有提交,另一个事务查询返回结果(导致没有得到新插入的数据),之后前面的事务才提交,由于两个事务的查询结果一样,因此出现插入数据重复。
----------------------------------------------------
遇到了一个事务和锁的问题,百思不得其解,各种尝试测试,都没能彻底的解决问题,甚至都怀疑造成这个问题的根本原因。但是项目上,还是以解决问题为重,于是只能绕开根本原因,根据现象处理问题,基本上留有隐患的暂时把问题处理了,都算不得解决。根本问题,应该在业务环节,不然实在不应该。
以前用for update 和事务是没什么问题的啊。
这次,应是在事务配置上,业务逻辑上有问题。业务上是,先使用for update查询,然后插入、再无for update 查询、更新。
具体问题:为了锁定几条记录,在当前事务处理完成之后,其他事务不能查询。
实现方式:在查询条件上添加索引,在事务中使用for update ,然后执行插入,更新操作。下个事务,for update查询应该是等待状态,直到当前事务完成。
事务,使用spring的编程式事务,数据库是mysql,使用jmeter进行的多线程测试。
其中jmeter测试webservice接口的时候,需要在线程组下添加SOAP/XML-RPC Request,然后设置请求地址,请求参数
测试:在程序在事务代码中debug,然后在数据库中使用for update查询,处于等待状态,没毛病。
但是,当使用压力测试jemter,多个线程组的时候,出现问题:在一组事务完成commit之前,夹杂着其他的事务查询结果的返回。
怀疑是索引的问题,for update 没能锁住记录,于是给for update查询的所有where条件的列都设置了索引
没解决
然后怀疑事务级别,原来的事务级别是READ_COMMIT,可能出现不可重复读,于是修改为REPEATABLE_READ,可避免不可重复读,然后没有重复数据了,但是数据缺少了,后台报错Deadlock found when trying to get lock; try restarting transaction
但是不能让数据就这样丢失啊,于是修改代码逻辑,当报错时,再执行一遍,直到执行了指定次数之后,记录数据,通过定时任务再去执行。
基本解决了这个问题,但是却还是不明白for update在事务中为什么没有锁住行,别的事务还能执行。
具体代码日志如下:
Object signResult = false;
try {
signResult = transactionTemplate.execute(newTransactionCallback<Object>(){
@Override
public ObjectdoInTransaction(TransactionStatusarg0) {
String id = UUID.randomUUID().toString().replaceAll("-","");
//使用forupdate查询,条件是索引字段
SignRecordsign=signRecordMapper.selectMaxSignByPdf(spdfpath);
if(null!=sign) {
System.out.println(sign.getTspdfPath()+"******1查询得到的数据times:"+sign.getTimes());
}
Stringtemppdf=spdfpath;//记录初始签名pdf文件
inttimes = 1;
if(null!=sign) {
//查询最近一次签名成功的结果文件作为源文件
StringresultPath=sign.getResultPath();
temppdf = resultPath;
times = sign.getTimes();
System.out.println("*****2之前的数据**"+times);
times +=1;
System.out.println("*****3之后的数据**"+times);
finalintttimes =times ;
sign.setTimes(ttimes);//设置签名次数
}else {
//插入新记录
sign = new SignRecord();
finalintttimes =1 ;
sign.setTimes(ttimes);//设置签名次数
}
sign.setStatus("0");//0正签名状态
sign.setKeyword(skeyword);
sign.setDocId(sdocId);
sign.setCreateTime(new Date());
sign.setSpdfPath(spdfpath);
sign.setPage(tpage);
//先插入一条记录
sign.setId(id);
sign.setTspdfPath(temppdf);
signRecordMapper.insertSignRecord(sign);
System.out.println(sign.getDocId()+"******4插入新纪录"+"times:"+times);
//当同一pdf文件,同一关键字,需要多次签名的时候,根据要签名的次数,将初始位置后延,指定长度的倍数
intcount =signRecordMapper.selectCountByPdfKW(sign);
System.out.println("******************5count:"+count+"----times"+times+"---ttimes"+sign.getTimes());
//确定文件的根目录
final StringPath=TZGMBatchSignServiceImpl.class.getResource("/").toString().substring(6)+"PDFSign/";
String resultPath =Path+"/"+id+".pdf";
//开始执行具体操作
try {
Map<String,Object> re = doSignByKW(resultPath,Path+temppdf,Path+pfxpath,pfxPwd,skeyword,Path+sealPath,count,tpage);
//更新结果
sign.setStatus("1");
if(null!=re.get("position")){
sign.setPosition(re.get("position").toString());
}
sign.setResult(re.get("result").toString());
sign.setResultPath(id+".pdf");
signRecordMapper.updateSignRecord(sign);
System.out.println("******6完成更新"+sign.getTimes());
}catch(Exceptione) {
System.out.println(e.getMessage());
returnfalse;
}
returntrue;
}
});
}catch(Exceptione) {
// 打印报错信息
}
索引字段
事务级别
<propertyname="isolationLevelName"value="ISOLATION_READ_COMMITTED"/>
具体:
<!-- 事务管理器 -->
<beanname="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<propertyname="dataSource"ref="dataSource"/>
</bean>
<beanid="transactionTemplate"class="org.springframework.transaction.support.TransactionTemplate">
<propertyname="transactionManager"ref="transactionManager"/>
<!-- <propertyname="isolationLevelName" value="ISOLATION_READ_COMMITTED"/>-->
<propertyname="isolationLevelName"value="ISOLATION_REPEATABLE_READ"/>
<propertyname="timeout"value="30"/>
</bean>
Jmeter测试start之后查看后台日志、数据库信息:
数据库出现重复数据
日志:
其中紫色部分是在一个事务的语句,绿色部分是另一个事务的语句。在紫色部分事务没有commit之前,就夹杂着则绿色部分事务的语句。
Registering transaction synchronization forSqlSession [org.apache.ibatis.session.[email protected]]
JDBC Connection[[email protected]] will be managed by Spring
==> Preparing: select a.times,a.result_path from(select times,result_path from sign_record t where spdf_path= ? and status='1'and result='0' for update) a order by a.times DESC limit 1
……(省略部分日志)
Creating a new SqlSession
Registering transaction synchronization forSqlSession [[email protected]]
JDBC Connection[[email protected]] will be managed by Spring
==> Preparing: select a.times,a.result_path from (select times,result_pathfrom sign_record t where spdf_path= ? and status='1' and result='0' for update)a order by a.times DESC limit 1
==> Parameters: 1emr.pdf(String)
SHA1
Fetched SqlSession[[email protected]] from currenttransaction
==> Preparing: update sign_record setSTATUS=?,POSITION=?,RESULT=?,RESULT_PATH=? where id=?
==> Parameters:1(String), 132,350,212,[email protected](String), 0(String),411e31d0b2ec43539cd9f68b43afeca9.pdf(String),411e31d0b2ec43539cd9f68b43afeca9(String)
<== Updates: 1
Releasingtransactional SqlSession[[email protected]]
******6完成更新3
<== Columns: times, result_path
<== Row: 2,e685c0cf179e45ceb9e536937440f85a.pdf
<== Total: 1
Releasing transactional SqlSession [[email protected]]
null******1查询得到的数据times:2
*****2之前的数据**2
*****3之后的数据**3
Fetched SqlSession [[email protected]]from current transaction
==> Preparing: insert intosign_record(ID,DOC_ID,SPDF_PATH,TSPDF_PATH,TIMES,STATUS,CREATE_TIME,keyword,page)values(?,?,?,?,?,?,?,?,? )
==> Parameters:b44e721f49b540dd9c951ff1a95a9702(String), 1(String), 1emr.pdf(String),e685c0cf179e45ceb9e536937440f85a.pdf(String), 3(Integer), 0(String), 2017-11-1509:14:18.113(Timestamp),医生签名三(String),1(Integer)
<== Updates: 1
Releasingtransactional SqlSession [[email protected]]
1******4插入新纪录times:3
Fetched SqlSession[[email protected]] from currenttransaction
==> Preparing: select count(c.rownum) count from( select (@i := case when @pre_times=times then @i + 1 else 1 end) rownum,b.*,@pre_times:=times from ( select t.spdf_path,doc_id,page,keyword,t.times fromsign_record t where spdf_path=? and keyword=? and page=? and result='0' groupby id order by times) b,(SELECT @i := 0, @pre_times:='') AS a ) c wherec.rownum=1;
==> Parameters:1emr.pdf(String), 医生签名三(String),1(Integer)
<== Columns: count
<== Row: 1
<== Total: 1
Releasing transactional SqlSession [[email protected]]
******************5count:1----times3---ttimes3
Transactionsynchronization committing SqlSession[[email protected]]
Transactionsynchronization closing SqlSession[[email protected]]
于是出现,这个事务中的更改还没有完成,但是,另一个事务已经开始查询得到数据,而不是在等待的状态。
==> Preparing: update sign_record setSTATUS=?,POSITION=?,RESULT=?,RESULT_PATH=? where
SqlSession[org.apache.ibatis.session.defaults[email protected]]
******6完成更新3
*************************************************times2的时候
[[email protected]c140e9]
null******1查询得到的数据times:2
*****2之前的数据**2
*****3之后的数据**3
Transaction synchronization committing SqlSession [[email protected]]
Transaction synchronization closingSqlSession [[email protected]]
如上:查询得到的数据times:2,但是之前明显已经有更新times3。更新操作的事务没有提交,就查询返回了。更新事务@11cc8d5,查询所在的事务c140e9,但是c140e9是在11cc8d5中间执行的。
在上面日志之前,有事务c140e9的查询
Registering transaction synchronization forSqlSession [[email protected] c140e9]
JDBC Connection[[email protected]] will be managed by Spring
==> Preparing: select a.times,a.result_path from (select times,result_pathfrom sign_record t where spdf_path= ? and status='1' and result='0' for update)a order by a.times DESC limit 1
但是程序里面如果在事务中有操作,使用数据库for update查询的时候,事务没提交,是得不到查询结果的?
但是程序里面如果在事务中有操作,程序里另一个事务却能查询得到数据?如日志打印结果。
不知道怎么处理了。。。。。。。
测试:将查询签名的部分独立成一个方法,当执行报错的时候,重新执行;这样的话应该设置重新执行的次数,如果超过指定的次数,那么记录数据不再执行。。。。
不过修改了事务级别,修改为REPEATABLE_READ,不可重复读。
<propertyname="isolationLevelName"value="ISOLATION_REPEATABLE_READ"/>
修改成REPEATABLE_READ的时候,出现报错。如果在事务中的时候,for update 查询会报错。Deadlock found when trying to get lock; try restarting transaction
测试:
<propertyname="isolationLevelName"value="ISOLATION_READ_COMMITTED"/>
在29次中出现一次重复了,即存在一个事务没有提交,另一个就查询出结果,导致两次结果一样。
测试
<propertyname="isolationLevelName"value="ISOLATION_REPEATABLE_READ"/>
29次,没有出现重复的times。同时查看日志,发现出现了错误次数有超过两次的时候。
曲线救国的处理方式:
由于使用REPEATABLE_READ会报错,但是不出现重复数据,那就就将报错的数据再执行一遍,直到不报错为止。
1、事务级别设置成REPEATABLE_READ,不可重复读。
2、代码中,如果执行的时候出现错误,那么重新执行一次,如果还错,那么再执行,最大执行5次,如果5次之后还报错,那么记录当期签名操作。后期使用定时任务执行。
在事务处理的代码后添加
if(null!=signResult) {
result = Boolean.parseBoolean(signResult.toString()) ;
if(!result&&count<=5) {
System.out.println("******这里报错了*******报错执行次数"+count);
count++;
result = (Boolean) toGetSign(pfxpath,pfxPwd,sealPath,tpage,spdfpath,sdocId,skeyword,count);
}else if(count>5) {
//记录没有签名成功的请求,是通过定时任务处理。超过太多。。。。。这是临时解决方法。
System.out.println("*****这是报错的数据:");
}
}
---------------------------------------我是分割线-----------------------------------------------------------------------
打算单独测试forupdate在事务中查询,然后插入的情况。
添加数据库表test,两个字段id,times ,设置id为索引
CREATE TABLE `TEST` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`times` int(2) NULL DEFAULT NULL ,
INDEX `test_index` USING BTREE (`id`)
)
具体代码实现:
Object testResult = transactionTemplate.execute(new TransactionCallback<Object>(){
@Override
public Object doInTransaction(TransactionStatusarg0) {
String uuid = UUID.randomUUID().toString().replaceAll("-","");
inttimes =signRecordMapper.getTestTimes(tid);
if(times==0) {
times = 1;
}else {
times +=1;
}
Map<String, Object> map = new HashMap<String,Object>();
map.put("id",tid);
map.put("times",times);
signRecordMapper.insertTest(map);
}
});
其中for update 查询:inttimes =signRecordMapper.getTestTimes(tid);
select if(max(t.times)is null,0,max(t.times)) times from test t
where id=#{id} order bytimes DESC limit 1 for update
插入:signRecordMapper.insertTest(map);
insert intotest(id,times) values(#{id},#{times})
通过jmeter测试,线程设置100,
当事务级别是REPEATABLE_READ的时候,在insert 的时候报错,被锁定的错误。但是在修改成READ_COMMIT的时候,不报错而且数据库也没有出现重复。
这就是正常现象的。
所有在加上具体的业务就出现问题了?
于是又将这段代码copy到之前业务代码之前。
//添加的测试代码段
int times = signRecordMapper.getTestTimes(spdfpath);
if(times==0) {
times = 1;
}else {
times +=1;
}
Map<String,Object> map = new HashMap<String, Object>();
map.put("id",spdfpath);
map.put("times",times);
signRecordMapper.insertTest(map);
//为了得到作为初始文件的pdf文件和成功次数---之前的正式代码段
SignRecord sign = signRecordMapper.selectMaxSignByPdf(spdfpath);
……
于是毁三观的事情发生了,用jmeter再次测试30次,没有问题,又试了一次还是没有问题,于是将线程数加到190,执行了多一会,但是依旧没有问题,times无重复的到了191,由于初始有一条数据。
不信啊,把刚添加了测试代码段去掉,再次测试。
//添加的测试代码段
/*int times =signRecordMapper.getTestTimes(spdfpath);
if(times==0) {
times = 1;
}else {
times +=1;
}
Map<String,Object> map = new HashMap<String, Object>();
map.put("id",spdfpath);
map.put("times",times);
signRecordMapper.insertTest(map);*/
//为了得到作为初始文件的pdf文件和成功次数---之前的正式代码段
int times = 1;
SignRecord sign = signRecordMapper.selectMaxSignByPdf(spdfpath);
……
再次测试,线程数190,果断出现重复了,times没有累计到191,说明中间有重复。
本来就是字段测试代码,为什么添加之后整个流程正常了,难道是因为测试代码的for update查询正常锁定了记录,然后代码块进入了等待其实事务完成么?于是只保留测试代码的查询
//添加的测试代码段
int times = signRecordMapper.getTestTimes(spdfpath);
/*if(times==0) {
times = 1;
}else {
times +=1;
}
Map<String,Object> map = new HashMap<String, Object>();
map.put("id",spdfpath);
map.put("times",times);
signRecordMapper.insertTest(map);*/
//为了得到作为初始文件的pdf文件和成功次数---之前的正式代码段
//int times =1;
SignRecord sign = signRecordMapper.selectMaxSignByPdf(spdfpath);
……
再次使用lmeter测试,这次测试线程100,得到的结果是正常的。
看来就应该是for update 没用好。。。
看一下写的forupdate查询语句
select times,result_path from sign_record t
where spdf_path= #{spdfPath} and status='1' and result='0'
order by times DESC limit 1 for update
也没有看出什么问题
应该就是forupdate的原因,那么语句看不出来问题,于是又看了一下索引,去掉其他的只保留spdf_path列作为索引,同时原来索引的名称为pdf感觉怕是什么关键字就改为psd_index。同时查询语句也只保留条件spdf_path。
执行测试,神奇的事情发生了。竟然数据正常。
我以为找到解决办法了,但是当我再次测试,还是出现的重复数据,只是重复的少了而已。但是也就是说还是不行。
但是出现一个另类的处理问题的方式:
在事务的开始,添加了一个使用for update查询临时表的sql,同时临时表锁定唯一的条件和业务表的数据一样,这样可以根据临时表的唯一,确保业务逻辑上的唯一锁定。
鬼使神差的出现的这种解决方案:
测试表的前提约束,测试表for update查询要锁定的行的值,是和业务表中锁定的行的值一样,或者主要列一样(这样可能会多覆盖一些记录)。通过一张表判断另一个表的数据,可能会出现不一致。
由于业务表中数据量变大,但是测试表可以设置要锁定列唯一约束,使得数量不会太多稍微减少多出来的查询消耗的时间。
但是依旧是为什么,业务表的锁定不可以呢?
-------------------------------------------------------------又是一个分割线-------------------------------------------------
查询测试表和业务表的区别,除了将查询条件设置为索引,测试表中没有设置主键字段,但是业务表中设置了主键字段。这个导致的巧合?
发现测试表中,没有设置主键ID,于是将业务表中的主键也去掉,结果厉害了。
第一次测试50的线程,没有问题。数据正常。
第二次测试50的线程,没有问题。数据正常。
第二次测试100的线程,没有问题。数据正常。
然后测试,去掉主键之后,会不会锁定整个表?
在事务代码中添加断点测试,表示事务未完成状态,然后在数据库中使用for update 查询。
结果,按照和业务代码中一样的条件查询出现等待,最后报错超时Lock wait timeout exceeded; try restarting transaction;按照不一样的条件查询直接得到结果。验证锁定有效。
如下图:控制台打印的查询语句,条件是1emr.pdf,其中spdf_path设置为索引
如下图:在mysql中查询,按照事务中查询条件查询的时候,等待,然后报错。更换条件之后直接得到结果。
难道不应该添加主键么?
表中设置主键,将for update 查询条件设置为索引,在事务中得到的查询结果出现重复?
将表中主键掉,索引不变,事务中的查询结果正常,没有出现重复?
又搞不懂了,有主键,反而锁不住数据?只有索引才能锁定记录。