本文的主角是mysql InnoDB的写锁,即排他锁(for update)
使用他最好的方式就是理解他:
- 排他锁不能与其他锁共存
- 一个事务获取了某行的排他锁,其他事务就不能再获取该行的锁
- 获取排他锁的当前事务内可以对数据进行读取和修改
- 不开启事务,FOR UPDATE 不会锁数据
- FOR UPDATE 是写锁,读操作不会锁住
- 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=0
CREATE 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 = Exception.class)
public class ScoreBSImpl implements ScoreBS {
@Autowired
private ScoreMapper scoreMapper;
@Override
public void A(){
}
@Override
public void B(){
}
}
需求:甲乙两个用户分别操作A、B方法,两个方法需要先根据id查询出数据,并对score的值进行追加修改
普通写法如下:
@Override
public void A(){
Score score = scoreMapper.selectById(1);
score.setScore(score.getScore()+10);
scoreMapper.updateScore(score);
}
@Override
public void B(){
Score score = scoreMapper.selectById(1);
score.setScore(score.getScore()+5);
scoreMapper.updateScore(score);
}
<select id="selectById" parameterType="java.lang.Integer" resultType="com.tobemn.project.entity.Score">
select * from score
where id = #{id,jdbcType=INTEGER}
</select>
<update id="updateScore" parameterType="com.tobemn.project.entity.Score">
update score
set score = #{score,jdbcType=INTEGER}
where id = #{id,jdbcType=INTEGER}
</update>
普通写法自然只适用于普通操作:用户甲调用A方法之后,用户乙再调用B方法
最终结果:score=15,没毛病,但是如果是如下场景呢
甲乙几乎同时发起调用,AB方法同时查询出score的值,此时A方法还未更新,B方法先更新完了,而后A方法完成更新,结果会是怎么样的
修改A方法
@Override
public void A(){
Score score = scoreMapper.selectById(1);
score.setScore(score.getScore()+10);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
scoreMapper.updateScore(score);
}
最终结果:socre=10
结果跟我们想要的正常流程结果不符,原因就是AB两个方法同时获取了score=0的初始值,而后B将数据库修改为0+5=5,最后A将数据库修改为0+10=10,导致出现了A覆盖了B结果的现象,而且在这种场景下,由于AB是两个独立的方法,没办法通过加可重入锁或synchronize来实现同步,此时FOR UPDATE就派出用场了
修改A方法:
@Override
public void A(){
Score score = scoreMapper.selectByIdLock(1);
score.setScore(score.getScore()+10);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
scoreMapper.updateScore(score);
}
添加带有FOR UPDATE 的sql
<select id="selectById" parameterType="java.lang.Integer" resultType="com.tobemn.project.entity.Score">
select * from score
where id = #{id,jdbcType=INTEGER}
for update
</select>
最终结果:score=5
结果依然不是理想中的score=15,这是因为上面总结的第5点:FOR UPDATE 是写锁,读操作不会锁住
所以当A方法调用for update时,B方法的查询依然生效,而更新被阻塞,等到A方法执行更新完毕,数据库值是score=10,B的更新方法才开始执行,但是B的更新方法前面获取到的查询值还是初始值0,所以最终结果是score=5
综上所述,想要保障获取到的结果是理想的值,就得当A方法调用for update的时候其它方法在查询是就被阻塞
这就使用到了上面总结的第2点:一个事务获取了某行的排他锁,其他事务就不能再获取该行的锁
修改B方法
@Override
public void B(){
Score score = scoreMapper.selectByIdLock(1);
score.setScore(score.getScore()+5);
scoreMapper.updateScore(score);
}
B方法也使用for update进行查询,由于其他事物不能获取已有排他锁的锁,因此会等待当前数据的排他锁被释放才能后进行查询,也就是所谓的,在查询时就进行阻塞
最终结果:score=15
另外一种解决思路就是执行A方法时,同时调用了B方法,不进行阻塞,直接抛出异常,通知用户“服务器繁忙请重试”等,此方法的目的是通过用户重新发起调用来避免并发情况的出现,使用的是关键词FOR UPDATE NOWAIT,此方法暂不推荐在高并发的情况下使用,即影响用户体验,也影响执行速度,仅当做一个思路