2023年8月3日发(作者:)

⽤最简单的springboot+mybatis解释forupdate的使⽤场景正经学徒,佛系记录,不搞事情本⽂的主⾓是mysql InnoDB的写锁,即排他锁(for update)使⽤他最好的⽅式就是理解他:1. 排他锁不能与其他锁共存2. ⼀个事务获取了某⾏的排他锁,其他事务就不能再获取该⾏的锁3. 获取排他锁的当前事务内可以对数据进⾏读取和修改4. 不开启事务,FOR UPDATE 不会锁数据5. FOR UPDATE 是写锁,读操作不会锁住6. FOR UPDATE 即可能是⾏锁也可能是表锁假设有个表单products ,⾥⾯有id跟name⼆个栏位,id是主键。例1: (明确指定主键,并且有此笔资料,row lock)SELECT * FROM wallet WHERE id=’3′ FOR UPDATE;例2: (明确指定主键,若查⽆此笔资料,⽆lock)SELECT * FROM wallet WHERE id=’-1′ FOR UPDATE;例2: (⽆主键,table lock)SELECT * FROM wallet WHERE name=’Mouse’ FOR UPDATE;例3: (主键不明确,table lock)SELECT * FROM wallet WHERE id<>’3′ FOR UPDATE;例4: (主键不明确,table lock)SELECT * FROM wallet WHERE id LIKE ‘3’ FOR UPDATE;带着上⾯的总结,开始试验:建⽴⼀张最基本的表,数据库增加数据id=1,score=0CREATE TABLE `score` ( `id` int(6) NOT NULL AUTO_INCREMENT COMMENT '主键', `score` int(6) DEFAULT NULL COMMENT '分数', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;java部分代码,主要是A、B两个⽅法进⾏操作,之所以分成两个⽅法就是为了测试不同操作同时修改同⼀条数据会引发的情况,如果是调⽤的痛⼀个⽅法,那直接使⽤可重⼊锁或synchronize等处理并发问题就可以解决了@RestController@RequestMapping(value = "/score")public class ScoreController { @Autowired private ScoreBS scoreBS; @PutMapping(value = "/A") public void A(){ scoreBS.A(); } @PutMapping(value = "/B") public void B(){ scoreBS.B(); }}@Service@Transactional(rollbackFor = )public class ScoreBSImpl implements ScoreBS { @Autowired private ScoreMapper scoreMapper; @Override public void A(){ } @Override public void B(){ }}需求:甲⼄两个⽤户分别操作A、B⽅法,两个⽅法需要先根据id查询出数据,并对score的值进⾏追加修改普通写法如下:@Overridepublic void A(){ Score score = ById(1); re(re()+10); Score(score);}@Overridepublic void B(){ Score score = ById(1); re(re()+5); Score(score);} update score set score = #{score,jdbcType=INTEGER} where id = #{id,jdbcType=INTEGER}普通写法⾃然只适⽤于普通操作:⽤户甲调⽤A⽅法之后,⽤户⼄再调⽤B⽅法最终结果:score=15,没⽑病,但是如果是如下场景呢甲⼄⼏乎同时发起调⽤,AB⽅法同时查询出score的值,此时A⽅法还未更新,B⽅法先更新完了,⽽后A⽅法完成更新,结果会是怎么样的修改A⽅法@Overridepublic void A(){ Score score = ById(1); re(re()+10); try { (5000); } catch (InterruptedException e) { tackTrace(); } Score(score);}最终结果:socre=10结果跟我们想要的正常流程结果不符,原因就是AB两个⽅法同时获取了score=0的初始值,⽽后B将数据库修改为0+5=5,最后A将数据库修改为0+10=10,导致出现了A覆盖了B结果的现象,⽽且在这种场景下,由于AB是两个独⽴的⽅法,没办法通过加可重⼊锁或synchronize来实现同步,此时FOR UPDATE就派出⽤场了修改A⽅法:@Overridepublic void A(){ Score score = ByIdLock(1); re(re()+10); try { (5000); } catch (InterruptedException e) { tackTrace(); } Score(score);}添加带有FOR UPDATE 的sql最终结果:score=5结果依然不是理想中的score=15,这是因为上⾯总结的第5点:FOR UPDATE 是写锁,读操作不会锁住所以当A⽅法调⽤for update时,B⽅法的查询依然⽣效,⽽更新被阻塞,等到A⽅法执⾏更新完毕,数据库值是score=10,B的更新⽅法才开始执⾏,但是B的更新⽅法前⾯获取到的查询值还是初始值0,所以最终结果是score=5综上所述,想要保障获取到的结果是理想的值,就得当A⽅法调⽤for update的时候其它⽅法在查询是就被阻塞这就使⽤到了上⾯总结的第2点:⼀个事务获取了某⾏的排他锁,其他事务就不能再获取该⾏的锁修改B⽅法@Overridepublic void B(){ Score score = ByIdLock(1); re(re()+5); Score(score);}

B⽅法也使⽤for update进⾏查询,由于其他事物不能获取已有排他锁的锁,因此会等待当前数据的排他锁被释放才能后进⾏查询,也就是所谓的,在查询时就进⾏阻塞最终结果:score=15另外⼀种解决思路就是执⾏A⽅法时,同时调⽤了B⽅法,不进⾏阻塞,直接抛出异常,通知⽤户“服务器繁忙请重试”等,此⽅法的⽬的是通过⽤户重新发起调⽤来避免并发情况的出现,使⽤的是关键词FOR UPDATE NOWAIT,此⽅法暂不推荐在⾼并发的情况下使⽤,即影响⽤户体验,也影响执⾏速度,仅当做⼀个思路

2023年8月3日发(作者:)

⽤最简单的springboot+mybatis解释forupdate的使⽤场景正经学徒,佛系记录,不搞事情本⽂的主⾓是mysql InnoDB的写锁,即排他锁(for update)使⽤他最好的⽅式就是理解他:1. 排他锁不能与其他锁共存2. ⼀个事务获取了某⾏的排他锁,其他事务就不能再获取该⾏的锁3. 获取排他锁的当前事务内可以对数据进⾏读取和修改4. 不开启事务,FOR UPDATE 不会锁数据5. FOR UPDATE 是写锁,读操作不会锁住6. FOR UPDATE 即可能是⾏锁也可能是表锁假设有个表单products ,⾥⾯有id跟name⼆个栏位,id是主键。例1: (明确指定主键,并且有此笔资料,row lock)SELECT * FROM wallet WHERE id=’3′ FOR UPDATE;例2: (明确指定主键,若查⽆此笔资料,⽆lock)SELECT * FROM wallet WHERE id=’-1′ FOR UPDATE;例2: (⽆主键,table lock)SELECT * FROM wallet WHERE name=’Mouse’ FOR UPDATE;例3: (主键不明确,table lock)SELECT * FROM wallet WHERE id<>’3′ FOR UPDATE;例4: (主键不明确,table lock)SELECT * FROM wallet WHERE id LIKE ‘3’ FOR UPDATE;带着上⾯的总结,开始试验:建⽴⼀张最基本的表,数据库增加数据id=1,score=0CREATE TABLE `score` ( `id` int(6) NOT NULL AUTO_INCREMENT COMMENT '主键', `score` int(6) DEFAULT NULL COMMENT '分数', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;java部分代码,主要是A、B两个⽅法进⾏操作,之所以分成两个⽅法就是为了测试不同操作同时修改同⼀条数据会引发的情况,如果是调⽤的痛⼀个⽅法,那直接使⽤可重⼊锁或synchronize等处理并发问题就可以解决了@RestController@RequestMapping(value = "/score")public class ScoreController { @Autowired private ScoreBS scoreBS; @PutMapping(value = "/A") public void A(){ scoreBS.A(); } @PutMapping(value = "/B") public void B(){ scoreBS.B(); }}@Service@Transactional(rollbackFor = )public class ScoreBSImpl implements ScoreBS { @Autowired private ScoreMapper scoreMapper; @Override public void A(){ } @Override public void B(){ }}需求:甲⼄两个⽤户分别操作A、B⽅法,两个⽅法需要先根据id查询出数据,并对score的值进⾏追加修改普通写法如下:@Overridepublic void A(){ Score score = ById(1); re(re()+10); Score(score);}@Overridepublic void B(){ Score score = ById(1); re(re()+5); Score(score);} update score set score = #{score,jdbcType=INTEGER} where id = #{id,jdbcType=INTEGER}普通写法⾃然只适⽤于普通操作:⽤户甲调⽤A⽅法之后,⽤户⼄再调⽤B⽅法最终结果:score=15,没⽑病,但是如果是如下场景呢甲⼄⼏乎同时发起调⽤,AB⽅法同时查询出score的值,此时A⽅法还未更新,B⽅法先更新完了,⽽后A⽅法完成更新,结果会是怎么样的修改A⽅法@Overridepublic void A(){ Score score = ById(1); re(re()+10); try { (5000); } catch (InterruptedException e) { tackTrace(); } Score(score);}最终结果:socre=10结果跟我们想要的正常流程结果不符,原因就是AB两个⽅法同时获取了score=0的初始值,⽽后B将数据库修改为0+5=5,最后A将数据库修改为0+10=10,导致出现了A覆盖了B结果的现象,⽽且在这种场景下,由于AB是两个独⽴的⽅法,没办法通过加可重⼊锁或synchronize来实现同步,此时FOR UPDATE就派出⽤场了修改A⽅法:@Overridepublic void A(){ Score score = ByIdLock(1); re(re()+10); try { (5000); } catch (InterruptedException e) { tackTrace(); } Score(score);}添加带有FOR UPDATE 的sql最终结果:score=5结果依然不是理想中的score=15,这是因为上⾯总结的第5点:FOR UPDATE 是写锁,读操作不会锁住所以当A⽅法调⽤for update时,B⽅法的查询依然⽣效,⽽更新被阻塞,等到A⽅法执⾏更新完毕,数据库值是score=10,B的更新⽅法才开始执⾏,但是B的更新⽅法前⾯获取到的查询值还是初始值0,所以最终结果是score=5综上所述,想要保障获取到的结果是理想的值,就得当A⽅法调⽤for update的时候其它⽅法在查询是就被阻塞这就使⽤到了上⾯总结的第2点:⼀个事务获取了某⾏的排他锁,其他事务就不能再获取该⾏的锁修改B⽅法@Overridepublic void B(){ Score score = ByIdLock(1); re(re()+5); Score(score);}

B⽅法也使⽤for update进⾏查询,由于其他事物不能获取已有排他锁的锁,因此会等待当前数据的排他锁被释放才能后进⾏查询,也就是所谓的,在查询时就进⾏阻塞最终结果:score=15另外⼀种解决思路就是执⾏A⽅法时,同时调⽤了B⽅法,不进⾏阻塞,直接抛出异常,通知⽤户“服务器繁忙请重试”等,此⽅法的⽬的是通过⽤户重新发起调⽤来避免并发情况的出现,使⽤的是关键词FOR UPDATE NOWAIT,此⽅法暂不推荐在⾼并发的情况下使⽤,即影响⽤户体验,也影响执⾏速度,仅当做⼀个思路