正在等待缓存锁:无法获得锁_漫谈分布式锁

f49b8c48f4a5aaaba6e0d2987dfd6281.png 笔耕墨耘,深研术道。 7bdf3a578fdd07b0eda024c948c7e08d.png 01为什么需要分布式锁 在日常开发中,我们经常会用到一些锁,比如Java的语言提供的同步关键字:synchronized,Jdk提供的Lock接口;这些同步机制帮我们解决了单机情况下的资源抢占问题。但在实际应用中,往往我们的服务都是集群部署,是分布式的,此时单机的解决方案已经不再适用。如下图所示场景:

4baed7e841e72f0be8196c26d2c0360a.png

在分布式场景上:

  • 线程A和线程B同时读到临界资源Redis中的stock,其值为10;

  • 线程A对其进行stock + 20的操作,并将修改结果更新回Redis;线程B同理,也进行类似操作;

  • 在线程A读取数据还未完成更新之间,线程B抢先完成了修改,此后线程A也完成了修改。这将导致数据产生错乱。

显然,在这种业务场景下,我们需要保证每个线程的读取和修改是原子操作。当然这里的原子操作是广义上的,并不是针对Redis如何保证一系列命令的原子性的讨论。保证原子性操作的有效方法之一是:加锁,一把分布式的锁。0 2分布式锁的特点
  • 互斥性:锁的基本特性,保证线程操作的原子性;

  • 可重入性:同一节点的同一个线程能够对一个临界资源(共享资源)重复加锁;

  • 支持阻塞和非阻塞:和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut);

  • 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级;

  • 锁超时:支持锁自动过期释放,防止死锁;

  • 支持公平锁和非公平锁(可选):公平锁是指多个线程按照申请锁的顺序来获取锁;非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。

03分布式锁的实现思路 基于上述对分布式锁特性的了解,业界常见的实现方式如下:
  • 基于数据库,如MySQL;

  • 基于缓存,如Redis;

  • 基于Zookeeper;

  • 自研分布式锁:如谷歌的Chubby。

其详细实现可以概览如下:

47ab0461f8c5a738125b45507b598725.png

本文首先基于数据库实现,基于缓存和分布式协调器的实现在后续篇章将会介绍。04基于MySQL的实现

基于数据库(MySQL)的方案,一般分为3类:基于表记录、乐观锁和悲观锁;

第1类:基于表记录(唯一索引)

我们知道,数据库的唯一索引约束可以保证有且仅有一条记录可以插入成功,基于此,我们可以将临界资源(共享资源)的标识设置为唯一索引,插入数据表成功,即代表获取到锁,删除记录代表释放锁。

数据库表设计如下:

CREATE TABLE `distributed_lock` (  `id` BIGINT NOT NULL AUTO_INCREMENT,  `resource` int NOT NULL COMMENT '锁定的资源',  `node_info` varchar(128) NOT NULL DEFAULT NULL COMMENT '机器节点信息,可由机器IP和线程名组合',  `reentry_count` int NOT NULL COMMENT '锁次数统计,统计可重入锁的加锁次数',  `description` varchar(1024) NOT NULL DEFAULT NULL COMMENT '描述',  PRIMARY KEY (`id`),  UNIQUE KEY `uiq_idx_resource` (`resource`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';

加锁:

INSERT INTO distributed_lock(resource, description) VALUES (1, 'lock');

解锁:

DELETE FROM distributed_lock WHERE resource = 1;
上述方式实现简单,但也面临一些问题:
  • 锁没有过期失效时间:一旦释放锁的操作失败就会导致锁记录一直在数据库中,其它线程无法获得锁。这个缺陷也很好解决,比如可以做一个定时任务去定时清理;

  • 可靠性依赖于数据库:建议设置备库,避免单点,进一步提高可靠性;

  • 非阻塞:因为插入数据失败之后会直接报错,想要获得锁就需要再次操作。如果需要阻塞式的,可以弄个for循环、while循环之类的,直至INSERT成功再返回;

  • 性能低:在分布式场景中,往往需要高性能地加锁和解锁,数据库很容易成为性能瓶颈。

第2类:乐观锁 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。乐观锁大多数是基于数据版本(version)的记录机制实现的。

在基于数据库表的版本解决方案中,乐观锁可以分为如下几个步骤:

  • 数据库表字段设计上新增version字段,以用于标识数据版本;

  • 查询数据时候,读出version字段,更新时候核对版本号,并一同更新version标识;

  • 更新过程中,会核对版本号,如果是一致的,则会成功执行本次操作;如果版本号不一致,则会更新失败。

核心实现:
UPDATE xxx_table SET resource = resource - 1, version = version + 1 WHERE id = 1 AND version = oldVersion;
乐观锁的优点比较明显,由于在检测数据冲突时并不依赖数据库本身的锁机制,不会影响请求的性能,当产生并发且并发量较小的时候只有少部分请求会失败。

缺点是:

  • 数据表新增version字段,增加数据库表的冗余;

  • 过高并发情况下,请求不断尝试更新数据,导致数据库压力增大,甚至导致系统不可用;

综合数据库乐观锁的优缺点,乐观锁比较适合并发量不高,并且写操作不频繁的场景。

第3类:悲观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。我们知道,共享锁和排他锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。值得庆幸的是,MySQL帮我们实现了,只需要编写相应SQL语句即可。

要使用悲观锁,我们必须关闭MySQL数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。

对于排他锁:

set autocommit = 0;# 设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:# 1. 开始事务begin;/begin work;/start transaction; (三者选一就可以)# 2. 查询表信息select status from TABLE where id = 1 for update;# 3. 插入一条数据insert into TABLE (id,value) values (2,2);# 4. 修改数据为update TABLE set value = 2 where id = 1;# 5. 提交事务commit;/commit work;

对于共享锁:

共享锁又称读锁 read lock,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获得共享锁的事务只能读数据,不能修改数据。

set autocommit = 0;# 开始事务begin;/begin work;/start transaction;  (三者选一就可以)# 开启共享锁select * from TABLE where id = 1  lock in share mode;

当一个事务对该行数据加上共享锁后,其他事务若要进行修改,需等待所有共享锁被释放,这个等待超时会抛出异常:

Lock wait timeout exceeded; try restarting transaction

lock wait time可以通过innodb_lock_wait_timeout来进行配置。

在查询语句后面增加 LOCK IN SHARE MODE,MySQL会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。加上共享锁后,对于update,insert,delete语句会自动加排它锁。

悲观锁注意事项:

  • 要使用悲观锁,我们必须关闭MySQL数据库的自动提交属性;

  • MySQL InnoDB引擎在加锁的时候,只有明确地指定主键(或索引)的才会执行行锁 (只锁住被选取的数据),否则MySQL 将会执行表锁(将整个数据表单给锁住)。

至此,基于数据库的实现思路已经梳理完毕。

05引用

[1] https://juejin.im/post/6844903688088059912

[2] https://blog.csdn.net/u013256816/article/details/92854794

[3] https://segmentfault.com/a/1190000015815061

[4] https://tech.meituan.com/2018/11/15/java-lock.html

发现“在看”和“赞”了吗,戳我试试吧 9166344910ca16aa4f9c43ad3e54643e.gif
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值