分布式锁的实现- mysql

前言

大家好,我是飓风,今天我们来聊聊分布式锁的原理、以及基于 mysql 怎么来实现分布式锁。

那么大家现在能不能想一想,分布式锁的使用场景都有哪些呢?下面我列举一些分布式锁的场景:

记住一点,一定是在分布式的环境下,所以肯定是多个服务,或者多个进程来操作一个共享资源。

  1. 扣减库存
  2. 订单支付,检查订单是否进行了重复支付的操作
  3. 缓存击穿/缓存雪崩,防止大并发对 DB 的操作

上面的场景大家有没有发现一个共同点,那就是在分布式的环境下,需要一种机制,来保证对共享资源【库存、支付状态、db 中的某个数据】的互斥访问,那么怎么来达到互斥访问的目的呢,那么就需要分布式锁来实现了。

分布式锁的特性

  • 互斥性

必须,即保证不同节点不同线程的互斥访问。

  • 超时机制

必须,即锁拥有的时长,也就是锁要有超时机制,防止其中一个节点一直拥有该锁造成死锁,那么其他节点,将永远也获取不到锁了,另一种情况就是设置了锁的超时时间,但是业务执行了的时间超过了设置的超时时间,那么锁自动释放了,那么此时互斥性就没了。

  • 提供阻塞和非阻塞接口

必须,所谓阻塞接口,就是没有获取到锁,那么就会一直等待锁的到来,不能中断。

非阻塞接口,就是尝试获取锁,锁没有获取到,就立即返回没有获取到锁,需要自己来重试,同时还可以支持一个获取锁的超时时间,在这个超时时间内没有获取到,那么还是需要自己重试的,减少 cpu 的浪费。

  • 可重入性

可选,当同一个节点同一个线程,想再次获取到该锁的时候,应该直接获取,不用在重新竞争了。

  • 公平锁和非公平锁

可选,这个其实很好理解,获取锁的优先级问题,公平就是排队,非公平就是有优先级,级别高的可以插队。

  • 其他

最好是高可用的锁方案和高性能的锁接口

实现--mysql 唯一索引

利用 mysql 唯一索引的特性,这个唯一的索引列就是分布式环境下互斥的资源,如果某个节点先插入了这个唯一索引对应的列值,那么其他节点就会插入失败,也就是获取锁失败了,也就达到了互斥性。

创建表信息

表信息用户维护一些锁的信息。

CREATE TABLE distributed_lock  (
  id  bigint(11) NOT NULL AUTO_INCREMENT,
  lock_resource  varchar(100) NOT NULL DEFAULT '' COMMENT '共享资源,可能某个方法或者数据行',
  lock_desc  varchar(100) NOT NULL DEFAULT '' COMMENT '锁描述',
  update_time  datetime NOT NULL,
  create_time  datetime NOT NULL,
  PRIMARY KEY (id ),
  UNIQUE KEY unique_idx_lock_resource (lock_resource )
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

互斥性的实现

利用 mysql 唯一索引来实现,我们要库某个 sku 的扣减库存操作。

获取锁,此时要对 sku=10000 的商品进行扣减库存的 SQL,只要执行成功,那么就获取锁成功了。

INSERT INTO distributed_lock(lock_resource,lock_count,lock_desc,update_time,create_time )VALUES ('10000',1,'库存锁',NOW(),NOW());

释放锁:

DELETE FROM distributed_lock WHERE lock_resource  = '10000';

大家想一想这样,如果按照上面的方式释放锁,会有什么问题?

如果其他节点由于自己的错误原因,误删了这个锁,会怎么样,那么我的任务还没有执行完,我的锁没了,那么其他节点就能获取到该锁,那么互斥性就没了。

所以解决这个问题,就是在删除删除的锁的时候,要知道这是自己的锁,才能删除,如果不是,不会进行锁的删除,也就是锁的释放。

此时 需要给表 加一列为 ' lock_owner' ,也就是锁的拥有者。

DELETE FROM distributed_lock WHERE lock_resource  = '10000' and lock_owner = '';

阻塞等待实现

什么是阻塞呢? 其实阻塞的概念很简单,比如你在进行 io 操作,还没有准备好读或者写操作,那么就需要一直等待,等待就绪,才能开始相应的读写操作,锁也是一样的,如果没有在进行获取锁的时候,获取锁的方法一直没有返回你想要的锁,那么此时你的的程序或者线程需要一直等待,阻塞这里,等待锁的返回。下面我们来看看阻塞等待的代码实现:

@Override
public void lock(String resource) {
    // 这里通过一个死循环,来模拟了阻塞的实现,lockResource 进行资源的锁定方法 while (!lockResource(resource)){
        //等待一会再去获取
        LockSupport.parkNanos(WAIT_TIME);
    }
}

非阻塞的实现

什么是非阻塞呢?所谓非阻塞,就是你获取锁的时候,没有获取到,也就是获取失败了,不用在一直傻傻等待了,获取锁的方法,会直接告诉你获取锁失败了,要不要继续获取锁,你自己来决定,下面来看下非阻塞的代码实现:

//lockResource 进行资源的锁定方法
@Override
public boolean nonLock(String resource) {
    return lockResource(resource);
}

//加了一个等待获取锁的超时时间
@Override
public boolean nonLock(String resource,long timeout) {
    long lastTime = System.currentTimeMillis() + timeout;
    while (!lockResource(resource)){
        if (System.currentTimeMillis() > lastTime) {
            return false;
        }
    }
    return true;
}

可重入性的实现

这里的可重入性和你在 java 中的锁 synchronized 和 lock 接口的可重入性的意思是一样,就是如果你已经获取该锁了,如果还需要该锁,那么不需要重新获取,直接获取就可以了,下面是代码的实现:

// 判断是不是自己的锁,如果是更新库内对应 count 的值 + 1
if (Objects.equals(uniqueLock.getLockOwner(),LockUtil.getLockOwner())){
   return uniqueLockService.incCount(uniqueLock.getId());
}

释放锁的实现

记得一定要释放的是自己的锁哦

@Override
public boolean unLock(String resource) {
    //这里要注意,释放锁,要保证是自己的锁才可以
    final UniqueLock uniqueLock = uniqueLockService.findByResource(resource);
    if (uniqueLock == null) return false;
    // 是自己的锁,在进行操作 if (Objects.equals(uniqueLock.getLockOwner(),LockUtil.getLockOwner())){
        // 如果大于 0 说明 count 要减 1if (uniqueLock.getLockCount() > 0){
          return   uniqueLockService.deCount(uniqueLock.getId());
        }else {
            // ==0 ,删除记录就可以了
          return   uniqueLockService.deleteById(uniqueLock.getId());
        }
    }
    return false;
}

超时时间的实现

锁拥有的最大时间,防止程序出现意外,比如释放锁失败了,而一直拥有该锁,那么其他进程也就无法获取到该锁,那么业务也就无法进行下去了,这里我们可以通过一个定时任务来实现,判断表内,每个锁执行的时间,是否超过该锁设置的最大持有的时长,如果超过了这个时长,就认为锁释放失败了,此时要删除这个锁。

自动续期

没执行完,只要是自己拥有了该锁,那么可以自动续期,如果设置锁的超时时间是 10s 超时,如果 10s 还没有执行完成,那么自动续期的任务,就会检查到超时了,那么就自动续期一个时长如 30s,这里我们可以更新表中该条锁的时长+30s。

只要该锁没有被释放,那么就可以自动续期,所以一定要在你的 finally 中释放锁,来保证锁释放一定会执行。

此时你可能还会疑问,如果在释放锁本身的动作,出现了异常呢?我们该怎么实现呢?

其实也很简单,如果释放锁本身动作出现了异常或者返回失败了,只要我们捕获到该异常或者根据返回标识,然后通知自动续期的任务锁释放失败了,不要续期了,同时也可以帮助我删除掉该锁吧。

总结

今天我们了解了分布式锁以及它的特性。

  • 分布式锁一定是在分布式环境下,对相同的资源进行操作,来会起作用。
  • 分布式锁一定是互斥
  • 分布式锁要提供阻塞和非阻塞实现
  • 分布式锁要有超时时间,不能造成死锁
  • 分布式锁的自动续期,防止业务还没有执行完,锁超时了,结果锁被强制释放了

说明:

这里在说下锁超时和自动续期

自动续期的任务监听到锁没有释放,那么会自动续期锁的超时时间,那么如果还没来的急续期,判断锁超时的任务,判断锁已经超时了,就会强制释放该锁,也就是删除该锁 ,实际上,业务还没有执行完成,所以判断锁超时的任务,可以在锁超时的时长上+5s,一个延迟时间,那么在这个延迟时间内,自动续期的任务就会得到执行,接着更新表中锁的超时时间。

思考:

这里我们没有给锁超时的任务检查代码 和 自动续期的代码实现,大家可以想想怎么去实现?

还有一个思考题,如果没有锁超时的任务检查,只有自动续期的检查,如果释放锁失败了,通知自动续期任务不要进行续期了,同时自动续期任务来释放锁,也就是删除锁,是否可以呢?

源码

快来下载吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飓风zj

感谢打赏,thanks

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值