一、前言
首先简单介绍一下悲观锁和乐观锁: 悲观锁: 比较悲观,一旦加锁,自身增删查改,其他线程无法任何操作,不能与其他锁并存。加锁方式 for update 乐观锁: 比较乐观,认为其他线程不会修改数据,一旦加锁自身可以增删查改,其他线程只能读。加锁方式 lock in share mode 两种锁的的释放都在 commit或者rollback 之后,否则就会一直持有。
场景:并发查询签到时,导致一个用户可以签到多次
解决办法:for update 来解决并发重复查询,保证每次只有只能一个线程执行查询
二、MySQL测试 开启两个查询窗口,开启手动提交事务,先后执行一下加锁查询语句
代码语言:javascript复制set autocommit=0;
BEGIN;
SELECT
id,
uid,
diamond
FROM
reward
WHERE
uid = '2300220816449831'
AND type IN ('SIGNIN')
AND ctime >= '2022-10-17'
AND ctime < '2022-10-19')
for update;
COMMIT;
其中一个窗口执行完成之后,只要不执行 commit 操作,另一个窗口就会一直阻塞着。这样就可以说明 for update 避免并发重复查询,每一次只允许一个线程查询。 这时候其实可以去修改或者查询跟查询条件无关的数据,发现是可以修改成功的,但是如果是同种类型的数据,就会被阻塞,说明for update 加的是行锁。
三、java代码测试
根据上面签到重复问题,可以在查询的时候,增加 for update,其实也就是步骤一中的sql语句,不过注意需要在方法上加事务注解 @Transactional(rollbackFor = Exception.class)
Jmeter 测试配置:100个线程同时访问
1.Jmeter 测试——查询无加for update
发现同个用户id,同个时间点会有多条数据
2.Jmeter 测试——查询加for update,无@Transactional
结果发现也会重复插入数据
3.Jmeter 测试——查询加for update,加@Transactional
同个用户id,同个时间点只有一条数据
四、加的什么锁
如果查询条件用了索引/主键,那么select ..... for update就会进行行锁。
如果是普通字段(没有索引/主键),那么select ..... for update就会进行锁表。
但是如果select没有数据为空情况,会怎样呢?
其实也会,变成锁表,这时候,如果有insert或者Update操作,就会出现死锁情况。所以for Update的查询结果,应该作为判空处理,而不是判断非空。
这种情况其实很好验证,只要包含where条件的查询数据清空了,然后用jmeter并发请求,就可以重现:Deadlock found when trying to get lock; try restarting transaction
五、总结
1. for update可以加锁解决并发问题,并且还能作为分布式锁的一种实现方式,但是如果没有在事务内释放掉锁,就会导致死锁。 2. for update使用必须在事务内,也就是必须加注解@Transactional
我正在参与 腾讯云开发者社区数据库专题有奖征文。