数据库锁与幂等性

dblock
今天跟同事讨论了数据库加锁和幂等性的话题,本文记录一下

乐观锁和悲观锁

在并发条件下,需要对关键数据的更新加上防并发操作,在应用里加锁是没用的,在集群环境下并不能确保不并发提交。在数据库层面加锁才是最保险的方式,常见的做法有乐观锁和悲观锁。

乐观锁

乐观锁只在update之前做检查。常见的实现方式有2种,版本字段或者时间戳字段。

比如:

1
update users set name = 'kyfxbl', version = 2 where id = 1 and version = 1;

如果有另外一个线程已经更新了id为1的数据,那么where中的version条件就不会满足,本条update语句就会失败,从而避免了并发更新。时间戳也是类似的思路。

悲观锁

悲观锁是在事务开始前就加锁,事务提交或回滚后才释放。一般用select for update来实现。

比如:

1
2
3
4
5
6
7
8
9
begin

select * from users where id = 1 for update

update users set name = 'kyfxbl' where id = 1

commit

end

此时如果另一个线程也执行select for update或者update语句,就会阻塞,也避免了并发更新

另外注意一点,在select for update的时候,必须明确指定主键条件,才会加行锁。否则会加表锁,这样会带来更大的开销,降低并发性,而且一般是不必要的。

利用锁的特性防止应用并发

利用乐观锁和悲观锁,都可以实现防止应用并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 利用乐观锁示意,如果已经被锁,则update不会成功
private boolean lockByOptimisticLock(Long id){

String lockSql = "update users set lock = 1 where id = xxx and lock = 0";
return jdbc.exec(lockSql, id);
}

// 利用悲观锁示意,如果已经被锁,select for update会被阻塞
private boolean lockByPessimisticLock(Long id){

String lockSql = "select id from users where id = xxx for update";
return jdbc.exec(lockSql, id);
}

public void doSomething(){

boolean flag = lockByOptimisticLock(id);// 或者调悲观锁

if(!flag){
return;// 拿不到锁则返回
}

// 业务逻辑

unlock();// 乐观锁的方案需要解锁,悲观锁方案只需要提交或回滚事务
}

幂等性

为了防止重复数据插入,还要考虑幂等性。

唯一索引

比如在业务上,A、B、C的组合是唯一的,那么就应该用这3列做成组合唯一索引,这样就可以保证重复的数据绝不会插入。

在应用层面做校验,在并发条件下也是不能保证绝对正确的,数据库层面的唯一索引才是最保险的。

软删除的情况

但是如果不允许硬删除数据,加唯一索引就会有问题。比如A、B、C,还有一列deleted,用0表示正常状态,用1表示已删除。在这种情况下,就不能给A,B,C加唯一索引了,因为如果一条数据删除了,应该允许再次插入。

今天同事想到了一个很好的方案,deleted还是用0表示正常状态,删除的情况则是deleted = id,然后把A,B,C,deleted作为组合唯一索引