分布式锁及实现方案

什么是分布式锁?

  锁是在多线程环境下,实现多线程访问同一共享资源时,保证在任何给定时刻只有一个线程可访问共享资源所做的一种标记。而分布式锁是当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

分布式锁的实现方案

  对于分布式锁的实现设计需要考虑以下几个方面

  • 加锁解锁的同源性:A加的锁,不能被B解锁
  • 获取锁是非阻塞的:如果获取不到锁,不能无限期等待
  • 高性能:加锁/解锁是高性能的

基于数据库实现

– 基于数据库表

  这种方式最简单,创建一张锁表,然后通过操作该表中的数据来实现了

CREATE TABLE `RESOURCE_LOCK` (
  `ID` BIGINT NOT NULL AUTO_INCREMENT,
  `KEY_RESOURCE` VARCHAR(45) COLLATE UTF8_BIN NOT NULL DEFAULT '资源主键',
  `STATUS` CHAR(1) COLLATE UTF8_BIN NOT NULL DEFAULT '' COMMENT 'S,F,P',
  `LOCK_FLAG` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '1是已经锁 0是未锁',
  `BEGIN_TIME` DATETIME DEFAULT NULL COMMENT '开始时间',
  `END_TIME` DATETIME DEFAULT NULL COMMENT '结束时间',
  `CLIENT_IP` VARCHAR(45) COLLATE UTF8_BIN NOT NULL DEFAULT '抢到锁的IP',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=UTF8MB4 COMMENT='数据库分布式锁表';

当我们想要获得锁的时候,就可以在该表中增加一条记录,想要释放锁的时候就删除这条记录。

  该方法依赖于数据库,主要有两个缺点:

  • 单点问题:如果数据库不是集群的,一旦数据库不可用,会导致整个系统崩溃
  • 死锁问题:数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。一旦已获得锁的进程挂掉或者解锁操作失败,会导致锁记录一直存在数据库中,其他进程无法获得锁。
– 基于悲观锁

  在对任意记录进行修改前,先尝试为该记录加上排他锁,因为我们都悲观认为会存在资源竞争。

  通过使用了select…for update的方式,开启排他锁的方式实现了悲观锁。当我们使用这种方式时select的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

   这种方式需要注意,MySQL InnoDB默认行级锁,行级锁都是基于索引的。如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住(简单点说就where后面的条件要有索引)。

– 基于乐观锁

  恰好与悲观相反,凡事都想着好的一面。

  我们通过为数据增加版本号来实现,数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作,并判断当前版本号是不是该数据的最新的版本号。

   我们自己实现上面这个操作感觉挺麻烦的,如果你项目中用的是JPA的话,那恭喜你,乐观锁实现非常简单。Spring JPA(Java Persistence API)为开发人员提供了一组强大的工具来管理应用程序中的关系型数据,其中提供了**@Version注解**用于启用实体上的乐观锁。当实体中的某个字段标记为@Version时,JPA 将使用该字段来跟踪更改并确保一次只有一个事务可以更新特定行。

基于redis实现

  Redis 通常可以使用 setnx(key, value) 函数来实现分布式锁,setnx 函数的返回值有 0 和 1。虽然这样可以实现分布式锁,但是我们前面说了分布式锁的实现设计需要考虑几个方面,其中一个方面就是获取锁是非阻塞的。Redis Key超时时间可以利用expire命令来设置,最终通过setnx+expire命令实现。

public boolean tryLock(String key,String requset,int timeout) {

 Long result = jedis.setnx(key, requset);

 // result = 1时,设置成功,否则设置失败

 if (result == 1L) {

 return jedis.expire(key, timeout) == 1L;

    } else {

 return false;

    }

}

  但是这种实现方式是有问题的,setnx和expire是分开的两步操作,不具有原子性,如果执行完第一条指令应用异常或者重启了,锁将无法过期。

  那Redis到底该怎样实现分布式锁呢?改善方案就是使setnx和expire两条指令具有原子性。

– 使用Lua脚本

  我们可以使用Lua脚本来保证原子性(包含setnx和expire两条指令)

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;
– set key value [EX seconds][PX milliseconds][NX|XX] 命令
  • EX seconds: 设定过期时间,单位为秒
  • PX milliseconds: 设定过期时间,单位为毫秒
  • NX: 仅当key不存在时设置值
  • XX: 仅当key存在时设置值

set命令的nx选项,就等同于setnx命令。

  在我们使用种方式时,需要注意value必须要具有唯一性解锁时,我们需要判断锁是否是自己的,基于value值来判断

上述两种基于redis实现分布式锁的方式,会存在锁过期释放,业务没执行完的问题。所以我们需要对锁的过期时间延长,防止锁过期提前释放。

– 基于Redisson框架

  Redisson所有指令都通过lua脚本执行,保证了操作的原子性。设置了watchdog看门狗,保证了没有死锁发生(只有使用默认过期时间30s才生效)。支持Redlock的实现方式。

– 使用Redlock+Redisson

  上述方式,在Redis集群的时候也会出现问题,比如说A客户端在Redis的master节点上拿到了锁,但是这个加锁的key还没有同步到slave节点,master故障,发生故障转移,一个slave节点升级为master节点,B客户端也可以获取同个key的锁,但客户端A也已经拿到锁了,这就导致多个客户端都拿到锁。

Redisson实现了redLock版本的锁,有兴趣的小伙伴,可以去了解一下。

基于zookeeper实现

  基于Zookeeper建立锁就是建立一个文件夹,然后多线程要获取分布式锁的过程中,可以在这个锁(也就是文件夹)下创建一个个有序的文件,代表获取锁的多线程的顺序

  由于序号是有序的,可以规定排号最小的那个获得锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。释放锁的时候,就需要删除抢号的Znode,当前一个Znode 删除的时候通知下一个占有锁,第一个通知第二个、第二个通知第三个,依次向后。

总结

  分布式锁是解决多个进程同时访问临界资源的常用方法,在分布式系统中非常常见,本文主要介绍了三种实现分布式锁的方法,包括基于数据库实现、基于Redis缓存实现,以及基于 ZooKeeper 实现。三种方案,根据不同场景进行选择。

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值