一、思考

1、为何要有分布式锁

分布式系统中对于共享资源的控制不像单机应用那么方便,多台机器的数据一致性显得尤为重要且难搞,比如一致性要求严格的支付业务或者下单业务,多台机器之前如何保证支付的一致性和下单库存等的一致性是大问题。因而在多台机器之间需要有一个全局且唯一的东西来当锁,拿到锁的才能获取资源,如此来解决并发问题,解决下单库存此类业务出现超卖的情况。

2、实现方式

锁分乐观锁和悲观锁,区别是线程之间是否相互阻塞。分布式锁常用的实现方式有三种,分别是基于数据库、基于redis的实现、基于zookeeper的实现。而锁的实现中最重要需要考虑以下几点问题:

  • 死锁问题(失效时间)
  • 是否为阻塞锁
  • 是否可重入
  • 单点问题
  • 性能问题

3、实现思路

1)数据库

基于数据库的实现是利用数据库唯一索引来进行控制,获取锁时向数据库插入一条记录,由于唯一索引所以其他线程无法再插入记录,等到拥有锁的线程释放锁,也就是删除该条记录。

2)redis,更详细可看 

redis实现锁是基于set命令,该命令是当key不存在时才进行set操作,所以当多个线程同时set相同的key,只有最先的一个可以成功,因此也就获得了锁。旧版本(2.6.12以前)需要先setNX、然后再设置过期时间expire,但是后续的版本set命令增加很多选项(set [key] NX/XX EX/PX [expiration]),一次性设置。

3)zookeeper有两种思路,更详细可看 

①保持占用:每个客户端都尝试创建同一个 znode,但只有一个可以成功创建,创建成功的客户端即获取到锁,当该客户端处理完业务删除该节点,释放锁,其他客户端观察 znode的删除,又回到刚开始一样,抢占创建同个 znode,最终获取锁

②控制时序:给需要得到锁的客户端各自创建一个临时有序的顺序znode,作为子节点,即顺序号最小的客户端便持有锁(之所以要临时是防止出现客户端宕机而该节点没有删除,一直持有锁又没有用,像废弃的节点一直拿锁但其他节点一直得不到锁形成了死锁)当删除了前面顺序号小的znode节点,则释放锁,锁将留给后续排队的节点客户端观察znode删除,判断自己是不是列表中序号最小,是则获得锁,不是则继续监听

4、三种实现的对比

1)数据库

①优点

  • 使用较方便,不需要再额外增加其他中间件

②缺点

  • 容易造成死锁,因为没有失效时间,解锁失败时则造成其他线程都用不了该资源
  • 增加了数据库的负担,数据库性能会影响锁的过程,且数据库坏了也就加不了锁,三种方式中性能最低
  • 锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作
  • 锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁

2)redis

①优点

  • 性能较高,实现也比较方便
  • 引入超时时间,避免死锁
  • 可重入,重入时记录锁的次数
  • 业务处理时间过长时,redisson框架提供续租功能

②缺点

  • 需部署redis中间件,多了维护成本
  • 没得到锁的客户端是不断轮询去尝试获取锁,性能上很消耗

3)zookeeper

①优点

  • 可靠性较高,因为不依赖于超时时间,而是利用监听来决定是否释放锁

②缺点

  • 同样需要部署zookeeper中间件
  • 性能相比redis较低

4)对比

  • 从理解的难易程度角度(从低到高):数据库 > 缓存 > Zookeeper
  • 从实现的复杂性角度(从低到高):Zookeeper >= 缓存 > 数据库
  • 从性能角度(从高到低):缓存 > Zookeeper >= 数据库
  • 从可靠性角度(从高到低):Zookeeper > 缓存 > 数据库

5、拓展

1、针对上面所说的数据库实现方式中的缺点其实也有曲线救国的方式,如下

  • 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
  • 数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
  • 非阻塞的?搞一个while循环,直到insert成功再返回成功。
  • 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以