分布式锁的应用场景和三种实现方式

您所在的位置:网站首页 分布式锁的实际原则有哪些 分布式锁的应用场景和三种实现方式

分布式锁的应用场景和三种实现方式

2024-07-12 02:02| 来源: 网络整理| 查看: 265

应用场景

多线程对同一资源的竞争,需要用到锁,例如Java自带的Synchronized、ReentrantLock。 但只能用于单机系统中,如果涉及到分布式环境(多机器)的资源竞争,则需要分布式锁。

分布式锁的主要作用:

保证数据的正确性: 比如:秒杀的时候防止商品超卖,表单重复提交,接口幂等性。避免重复处理数据: 比如:调度任务在多台机器重复执行,缓存过期所有请求都去加载数据库。

分布式锁的主要特性:

互斥:同一时刻只能有一个线程获得锁。可重入:当一个线程获取锁后,还可以再次获取这个锁,避免死锁发生。高可用:当小部分节点挂掉后,仍然能够对外提供服务。高性能:要做到高并发、低延迟。支持阻塞和非阻塞:Synchronized是阻塞的,ReentrantLock.tryLock()就是非阻塞的支持公平锁和非公平锁:Synchronized是非公平锁,ReentrantLock(boolean fair)可以创建公平锁 分布式锁的知识图谱

在这里插入图片描述

分布式锁的实现方式 使用 MySQL 实现分布式锁

数据库表设计:

CREATE TABLE `distributed_lock` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `resource_name` varchar(200) NOT NULL DEFAULT '' COMMENT '资源名称(唯一索引)', `owner` varchar(200) NOT NULL DEFAULT '' COMMENT '锁持有者(机器码+线程名称)', `lock_count` int NOT NULL DEFAULT '0' COMMENT '加锁次数', `expire_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '锁过期时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_resource_name` (`resource_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分布式锁'; 锁的实现原理: 将用来竞争的资源名称设置为表的唯一索引。 获取锁的时候,就插入一条记录。插入成功就代表获取到锁,插入失败就代表获取锁失败。 释放锁的时候,就删除这条记录。支持阻塞和非阻塞: 可以用while循环直到插入成功,不过自旋也会占用CPU。支持可重入: 使用锁持有者owner和加锁次数字段lock_count实现可重入: 获取锁,次数加一,释放锁,次数减一,次数为零就删除这把锁。支持过期时间 使用过期时间expire_time字段,通过异步任务检测避免因程序异常或机器宕机导致锁无法释放。支持锁续期 获取锁的同时,启动一个异步任务,每当业务执行到三分之一时间,也就是6秒中的第2秒的时候,就自动延长锁过期时间,继续延长到6秒,这样就能保证业务逻辑处理完成之前锁不会过期。 使用 Redis 实现分布式锁

由于MySQL并发性能跟不上,所以还可以使用Redis实现分布式锁。 获取锁,并设置过期时间:

// 1. 获取锁 redis.setnx('resource_name1', 'owner1') // 2. 增加锁过期时间 redis.exprire('resource_name1', 6, TimeUnit.SECONDS)

但是setnx和exprire两条命令不是原子的,可能获取锁之后还没来得及设置过期时间就宕机了。 所以可以使用Redis 2.6.12之后提供的一条复合命令:

redis.set('resource_name1', 'owner1',"NX" "EX", 6)

释放锁:

// 释放锁 if ('owner1'.equals(redis.get('resource_name1'))){ redis.del('resource_name1') }

释放锁时判断锁的持有者,可以避免把其他线程持有的锁给释放掉了。 但是get和del两条命令不是原子操作,需要引入Lua脚本把两条命令打包成一条发给Redis执行:

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redis.eval(script, Collections.singletonList('resource_name1'), Collections.singletonList('owner1'))

此外,实现锁续期的功能,可以使用Redis客户端的Redisson的WatchDog功能,在我们调用lock自动唤醒WatchDog。

使用 Zookeeper 实现分布式锁

zookeeper采用树形节点,类似Linux目录文件结构,同一目录下的节点名称不能重复。 在这里插入图片描述 节点有分为四种类型:

**持久节点:**一旦创建,永久存储在服务器上,除非手动删除。 临时节点:生命周期与客户端绑定,客户端断开连接,节点就被自动删除。 **持久顺序节点:**特性同持久节点,只是在节点名称后面追加自增有序数字。 **临时顺序节点:**特性同临时节点,只是在节点名称后面追加自增有序数字。

zookeeper还有个监听-通知机制,客户端可以在资源节点上创建watch事件。当节点发生变化,会通知客户端,客户端可以根据变化做相应的业务处理。

我们可以利用临时顺序节点的特性创建分布式锁,分以下三步:

在资源/resource1目录下创建临时顺序节点node获取/resource1目录下的所有节点,如果当前节点序号最小,代表加锁成功如果不是,就是watch监听序号最小的节点

实现逻辑很简单,我们来分析一下zookeeper实现分布式锁的优点:

由于创建的临时节点,断开连接后自动删除,所以无需设置锁超时时间,也就不用考虑不释放和锁续期由于节点上存储的创建人信息,锁也就支持可重入由于可以监听节点,也就实现了可阻塞


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3