遇到了一个关于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.defaults.DefaultSqlSession@148c6be]

JDBC Connection[com.mchange.v2.c3p0.impl.NewProxyConnection@f018eb] 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 [org.apache.ibatis.session.defaults.DefaultSqlSession@151125f]

JDBC Connection[com.mchange.v2.c3p0.impl.NewProxyConnection@18d1eb9] 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[org.apache.ibatis.session.defaults.DefaultSqlSession@61b09c] from currenttransaction

==> Preparing: update sign_record setSTATUS=?,POSITION=?,RESULT=?,RESULT_PATH=? where id=?

==> Parameters:1(String), 132,350,212,430@1(String), 0(String),411e31d0b2ec43539cd9f68b43afeca9.pdf(String),411e31d0b2ec43539cd9f68b43afeca9(String)

<==    Updates: 1

Releasingtransactional SqlSession[org.apache.ibatis.session.defaults.DefaultSqlSession@61b09c]

******6完成更新3

<==    Columns: times, result_path

<==        Row: 2,e685c0cf179e45ceb9e536937440f85a.pdf

<==      Total: 1

Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@11cc8d5]

null******1查询得到的数据times:2

*****2之前的数据**2

*****3之后的数据**3

Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@11cc8d5]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 [org.apache.ibatis.session.defaults.DefaultSqlSession@11cc8d5]

1******4插入新纪录times:3

Fetched SqlSession[org.apache.ibatis.session.defaults.DefaultSqlSession@11cc8d5] 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 [org.apache.ibatis.session.defaults.DefaultSqlSession@11cc8d5]

******************5count:1----times3---ttimes3

Transactionsynchronization committing SqlSession[org.apache.ibatis.session.defaults.DefaultSqlSession@61b09c]

Transactionsynchronization closing SqlSession[org.apache.ibatis.session.defaults.DefaultSqlSession@61b09c]

 

于是出现,这个事务中的更改还没有完成,但是,另一个事务已经开始查询得到数据,而不是在等待的状态

==> Preparing: update sign_record setSTATUS=?,POSITION=?,RESULT=?,RESULT_PATH=? where

SqlSession[org.apache.ibatis.session.defaults.DefaultSqlSession@11cc8d5]

******6完成更新3

*************************************************times2的时候

 [org.apache.ibatis.session.defaults.DefaultSqlSession@c140e9]

null******1查询得到的数据times:2

*****2之前的数据**2

*****3之后的数据**3

Transaction synchronization committing SqlSession [org..DefaultSqlSession@11cc8d5]

Transaction synchronization closingSqlSession [oDefaultSqlSession@11cc8d5]

 

如上:查询得到的数据times:2,但是之前明显已经有更新times3。更新操作的事务没有提交,就查询返回了。更新事务@11cc8d5查询所在的事务c140e9,但是c140e9是在11cc8d5中间执行的。

在上面日志之前,有事务c140e9的查询

Registering transaction synchronization forSqlSession [DefaultSqlSession@ c140e9]

JDBC Connection[com.mchange.v2.c3p0.impl.NewProxyConnection@18ca549] 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 查询条件设置为索引,在事务中得到的查询结果出现重复?

将表中主键掉,索引不变,事务中的查询结果正常,没有出现重复?

又搞不懂了,有主键,反而锁不住数据?只有索引才能锁定记录

使用mysql数据库,

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值