GeekIBLi

mysql乐观锁实现分布式锁

2021-07-28

基于数据表乐观锁实现分布式锁

整体的实际思路

要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们要锁住某个方法或资源的时候,
我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。

基于数据表实现分布式锁的几个要点

1、这把锁依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解决操作失败,就会导致记录一直在数据库中,其他线程无法在获得锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁的操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据库表中数据已经存在了。

当然,我们也可以有其它方式解决上面的问题:

1、数据库是单点?那就搞两个数据库,数据库之前双向同步,一旦挂掉快速切换到备库上。
2、没有失效时间?可以做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3、非阻塞?可以写一个while循环,直到insert成功再返回成功。
4、非重入?可以在数据库表中加一个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库中可以查到的话,就直接把锁分配给它即可。

乐观锁&悲观锁

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,
可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,
每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

乐观锁的实现

实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。

1、使用版本号实现乐观锁

使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新的版本号。

1
2
3
4
5
6
7
1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};

需要注意的是,乐观锁机制往往基于系统中数据存储逻辑,因此也具备一定的局限性。由于乐观锁机制是在我们的系统中实现的,对于来自外部系统的用户数据更新操作不受我们系统的控制,
因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况,并进行相应的调整(如将乐观锁策略在数据库存储过程中实现,
对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。

这一点其实在微服务架构中只要做好数据隔离就可以避免,比如user这张数据表,按照边界划分应该属于用户中心服务的,其他服务比如仓储,物流等需要
用户的信息,应该有用户中心暴露出接口,而不是仓储去数据库查询user这张表的数据,甚至update user的数据。

2、使用时间戳

一般都是使用update_time字段,并且这个字段肯定是跟随数据库时间配置的,即 update on current_timestamp ;

乐观锁的优点与不足

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
能够提升数据库的吞吐量;
但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

参考资料

基于数据库的分布式锁实现
分布式锁方式(一、基于数据库的分布式锁)
分布式锁看这篇就够了
乐观锁与悲观锁深入学习

Tags: MySQL