先把base放在这里
设计时我们应当考虑到分布式锁需要有的一些特性:

  • 互斥性:和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。
  • 可重入性:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
  • 锁超时:和本地锁一样支持锁超时,防止死锁。
  • 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
  • 支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。
  • 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。

以我们常用的mysql为例,来实现基于数据库的分布式锁。一般情况下,mysql有两种锁:乐观锁和悲观锁
乐观锁:利用版本号实现
悲观锁:行锁与表锁(一般用行锁,有排他锁 select for update,共享锁 select in share mode)

先创建一个锁表

image.png
前面我们说的lock(),tryLock(long timeout),trylock()这几个方法可以用下面的伪代码实现

lock()

lock一般是阻塞式的获取锁,意思是不获取到锁,就一直阻塞,可以写一个死循环来执行操作:

  1. public void lock(){
  2. while(true){
  3. if(mysqlLock.lock(resource)){
  4. return;
  5. }
  6. // 休眠后重试
  7. LockSupport.parkNanos(1000*1000*3);
  8. }
  9. }

mysqlLock.lock内部是一个sql,为了达到可重入锁的效果,我们应该先进行查询,如果有值,需要比较node_info是否一致。
这里的node_info可以用机器IP和线程名字来表示,如果一致就加可重入锁,如果不一致,就返回false,如果没有值,就直接插入一条数据。

image.png
需要注意的是,这段代码需要加事务,必须要保证这一系列操作的原子性。

tryLock()和tryLock(long timeout)

tryLock()是非阻塞获取锁,如果获取不到就马上返回,代码如下:
image.png
tryLock(long timeout) 实现如下:
image.png
mysqlLock.lock 和上面一样,但是要注意的是 select … for update 这个是阻塞的获取行锁,如果同一个资源并发量较大还是有可能会退化成阻塞的获取锁。

unlock()

unlock 的话如果这里的 count 为 1 那么可以删除,如果大于 1 那么需要减去 1。
image.png

锁超时

我们有可能会遇到我们的机器节点挂了,那么这个锁就不会得到释放,我们可以启动一个定时任务,通过计算一般我们处理任务的时间。
比如是 5ms,那么我们可以稍微扩大一点,当这个锁超过 20ms 没有被释放我们就可以认定是节点挂了然后将其直接释放。

MySQL 小结:

  • 适用场景:MySQL 分布式锁一般适用于资源不存在数据库,如果数据库存在比如订单,可以直接对这条数据加行锁,不需要我们上面多的繁琐的步骤。 比如一个订单,我们可以用 select * from order_table where id = ‘xxx’ for update 进行加行锁,那么其他的事务就不能对其进行修改。
  • 优点:理解起来简单,不需要维护额外的第三方中间件(比如 Redis,ZK)。
  • 缺点:虽然容易理解但是实现起来较为繁琐,需要自己考虑锁超时,加事务等等。性能局限于数据库,一般对比缓存来说性能较低。对于高并发的场景并不是很适合

乐观锁

前面我们介绍的都是悲观锁,这里想额外提一下乐观锁,在我们实际项目中也是经常实现乐观锁,因为我们加行锁的性能消耗比较大,通常我们对于一些竞争不是那么激烈。
但是其又需要保证我们并发的顺序执行使用乐观锁进行处理,我们可以对我们的表加一个版本号字段。
那么我们查询出来一个版本号之后,update 或者 delete 的时候需要依赖我们查询出来的版本号,判断当前数据库和查询出来的版本号是否相等,如果相等那么就可以执行,如果不等那么就不能执行。
这样的一个策略很像我们的 CAS(Compare And Swap),比较并交换是一个原子操作。这样我们就能避免加 select * for update 行锁的开销。