暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Mysql实现分布式锁的的方式

IT枫斗者 2023-05-11
389

逐一执行、唯一执行。这是分布式锁使用的大致场景。

逐一执行

逐一执行顾名思义,就是多台服务器同一时间只有一台服务器执行,其他服务器等待,当执行服务器执行完成后其他服务器会继续抢占执行。

唯一执行

唯一执行类似于leader的概念,多台服务器同一时间只有一台执行,其他服务器放弃执行。

基于记录

适用于唯一执行场景

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

为了更好的演示,我们先创建一张数据库表,参考如下:

    CREATE TABLE `database_lock` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `resource` int NOT NULL COMMENT '锁定的资源',
    `description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',
    `updatetime` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uiq_idx_resource` (`resource`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';


    其中update_time是用来判断加锁时间,用于后续定时任务解决使用!

    当我们想要获得锁时,可以插入一条数据:

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


      注意:在表database_lock中,resource字段做了唯一性约束,这样如果有多个请求同时提交到数据库的话,数据库可以保证只有一个操作可以成功(其它的会报错:ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘uiq_idx_resource’),那么那么我们就可以认为操作成功的那个请求获得了锁。

      当需要释放锁的时,可以删除这条数据:

        DELETE FROM database_lock WHERE resource=1;

        这种实现方式非常的简单,但是需要注意以下几点:

        乐观锁

        适用于唯一执行场景

        为了更好的理解数据库乐观锁在实际项目中的使用,这里就列举一个典型的电商库存的例子。一个电商平台都会存在商品的库存,当用户进行购买的时候就会对库存进行操作(库存减1代表已经卖出了一件)。我们将这个库存模型用下面的一张表optimistic_lock来表述,参考如下:

          CREATE TABLE `optimistic_lock` (  `id` BIGINT NOT NULL AUTO_INCREMENT,  `resource` int NOT NULL COMMENT '锁定的资源',  `version` int NOT NULL COMMENT '版本信息',  `created_at` datetime COMMENT '创建时间',  `updated_at` datetime COMMENT '更新时间',  `deleted_at` datetime COMMENT '删除时间',   PRIMARY KEY (`id`),  UNIQUE KEY `uiq_idx_resource` (`resource`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';

          其中:id表示主键;resource表示具体操作的资源,在这里也就是特指库存;version表示版本号。

          在使用乐观锁之前要确保表中有相应的数据,比如:

          如果只是一个线程进行操作,数据库本身就能保证操作的正确性。主要步骤如下:

          • STEP1 - 获取资源:SELECT resource FROM optimistic_lock WHERE id = 1

          • STEP2 - 执行业务逻辑

          • STEP3 - 更新资源:UPDATE optimistic_lock SET resource = resource -1 WHERE id = 1


            然而在并发的情况下就会产生一些意想不到的问题:比如两个线程同时购买一件商品,在数据库层面实际操作应该是库存(resource)减2,但是由于是高并发的情况,第一个线程执行之后(执行了STEP1、STEP2但是还没有完成STEP3),第二个线程在购买相同的商品(执行STEP1),此时查询出的库存并没有完成减1的动作,那么最终会导致2个线程购买的商品却出现库存只减1的情况。

          在引入了version字段之后,那么具体的操作就会演变成下面的内容:

          • STEP1 - 获取资源:SELECT resource, version FROM optimistic_lock WHERE id = 1

          • STEP2 - 执行业务逻辑

          • STEP3 - 更新资源:UPDATE optimistic_lock SET resource = resource -1, version = version + 1 WHERE id = 1 AND version = oldVersion

          其实,借助更新时间戳(updated_at)也可以实现乐观锁,和采用version字段的方式相似:更新操作执行前线获取记录当前的更新时间,在提交更新时,检测当前更新时间是否与更新开始时获取的更新时间戳相等。

          缺点:不适合大量请求,会涉及大量事务回滚从而导致性能瓶颈

          悲观锁

          除了可以通过增删操作数据库表中的记录以外,我们还可以借助数据库中自带的锁来实现分布式锁。在查询语句后面增加FOR UPDATE,数据库会在查询过程中给数据库表增加悲观锁,也称排他锁。当某条记录被加上悲观锁之后,其它线程也就无法再改行上增加悲观锁。

          这样在使用FOR UPDATE获得锁之后可以执行相应的业务逻辑,执行完之后再使用COMMIT来释放锁。

          我们不妨沿用前面的database_lock表来具体表述一下用法。假设有一线程A需要获得锁并执行相应的操作,那么它的具体步骤如下:

          • STEP1 - 获取锁:SELECT * FROM database_lock WHERE id = 1 FOR UPDATE;。
          • STEP2 - 执行业务逻辑。
          • STEP3 - 释放锁:COMMIT。

          如果另一个线程B在线程A释放锁之前执行STEP1,那么它会被阻塞,直至线程A释放锁之后才能继续。注意,如果线程A长时间未释放锁,那么线程B会报错,参考如下(lock wait time可以通过innodb_lock_wait_timeout来进行配置):

          上面的示例中演示了指定主键并且能查询到数据的过程(触发行锁),如果查不到数据那么也就无从“锁”起了。

          如果未指定主键(或者索引)且能查询到数据,那么就会触发表锁,比如STEP1改为执行(这里的version只是当做一个普通的字段来使用,与上面的乐观锁无关):

          或者主键不明确也会触发表锁,又比如STEP1改为执行:

          缺点也很明显,要考虑上锁超时所导致的报错问题。

          最近在更新过关斩将系列——java面试题,全力帮助小白突破面试大关,目前已经更新至第44篇感兴趣可以关注公众号IT枫斗者追更

          往期分享:

          java之yield(),sleep(),wait()区别详解

          天涯倒闭后,是一家独霸,还是诸子百家。

          Spring AOP的理解

          Rocket MQ延迟消息

          Redis的持久化

          文章转载自IT枫斗者,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论