概述
在只有一台机器的情况下,我们也会碰到类似的情况,比如在多个线程需要访问某个共享资源的时候,我们就可以采用加锁的形式。在Java中,一个简单的办法就是使用synchronized
关键字来对方法或者变量加锁。
但是,这种加锁方式在进程之间的共享就显得力不从心了起来。为了解决这样的问题,我们必须引入分布式锁。分布式锁一般会用于互斥资源的访问。
在这里,我们将使用MySQL数据库
来实现分布式锁。当然,分布式锁还有其他的实现方式,如通过redis
、zookeeper
等,这些方式可以很方便的在网上找到实现方案。从性能上来说,使用缓存(Redis
) > Zookeeper
> MySQL数据库
。
Redis
固然是可以实现分布式锁,但是在QPS不高的情况下,使用这样的分布式锁会带来复杂性。使用MySQL数据库
能够快速开发。很多时候,先进的方案并不会比老旧的方案带来更多的收益,那这种时候使用简单的方案也是一个很好的方案。
基于乐观锁(CAS)的方案
顾名思义,乐观锁在大多数的情况下不会发生冲突,使用乐观锁为了保证其在不冲突情况下的性能。实际上,乐观锁并不是锁,所以省下了加锁、释放锁带来的时间和资源的消耗。同时,无锁机制自然也就避免了死锁的产生。
乐观锁实际上是一种思想:比较并交换(Compare and swap, CAS),简单来说就是当数据是原来的数据时,我才会进行交换。如下一段Java代码就能够解释这个操作。
public boolean cas(int addr, int oldValue, int newValue) {
if (addr != oldValue) return false;
addr = newValue;
return true;
}
其中,addr
指的是需要进行修改的数据,oldValue
是执行修改之前的数据,newValue
指的是希望修改成的数据。当我们有2个进程共同执行修改操作时,只会有一个能够生效,同时返回true,另外一个将会修改失败,返回false。
需要注意,一次cas操作必须是原子的,否则无法正常使用。
ABA问题
该问题可以简化为以下的时序图
时序 | 目标的值 | 进程1 | 其他进程 |
---|---|---|---|
1 | 0 | 读取0 | - |
2 | 0 | - | 读取0 |
3 | 1 | - | 修改为1 |
4 | 0 | - | 修改为0 |
5 | 1 | 修改为1 | - |
可以注意到其他进程将数据修改为1后又修改回0,那么进程1会以为并没有进行过修改,那么就会执行后续的操作,这可能会引起一些问题。
为了解决这样的问题,可以考虑在字段上面加入一个版本号,每次修改需要比对版本号,同时修改成功了版本号需要+1。当然也可以使用时间戳来实现同样的功能。
具体实现
数据库可以这样建表test_table
Name | Type | Not null | Comment |
---|---|---|---|
id | int | √ | id,唯一 |
value | int | 需要修改的值 | |
update_time | int | 更新时间 |
在使用乐观锁时,需要保证数据库中有数据,随后才能更新数据。
UPDATE
test_table
SET
value = #{ value },
update_time = #{ newUpdateTime }
WHERE
id = #{ id }
AND update_time = #{ oldUpdateTime }
由于mysql中的update会返回更新的行数,所以只需要判断返回的值是否是1即可。
如果为0,需要重新获取一次数据库中的数据,并重新进行一次更新。这里要注意循环次数需要做一个限制,否则可能会导致死循环。