写这个文章的原因是在面试阿里二面的过程中被问到过如何在不同机器(jvm)之间同步变量,当时对分布式锁不是很了解,就随便打了使用序列化传递对象,其实是答非所问。后来的工作学习中接触了分布式锁的不同实现,以及了解到在分布式系统中的应用,在此做个记录。

前言

在单机程序中,我们如果要实现不同线程的变量同步,并发量不大的情况下可以使用乐观锁(unsafe的cas,sql中带版本号set操作),并发较大时可以使用悲观锁(lock,synchronize,sql中的行锁(select for update)以及表锁(串行))。对于分布式系统而言,由于已经是不同的jvm,jdk的锁方法无法起到作用,因此只能借助第三方的锁来实现两台机器中的同步操作。目前主流的分布式锁实现主要有三种,数据库存储锁,缓存数据库存储锁,zookeeper存储锁。

数据库存储锁

首先要把本文的应用于变量同步的数据库存储锁所与数据库事务锁区分开来,前者是通过多应用连接同一个数据库共享资源,后者是数据库的事务隔离级别。使用数据库进行变量同步有两种方式,

数据库锁竞争

第一种是在数据中显示地表达出某资源的持有人是谁,其他竞争者轮询这条记录,等待持有人使用资源完成任务后释放锁(更新记录,比如ower_id设为null),再进行锁竞争。
resource_id|owner_id
–|:–:
1|2|

在事务中锁定数据

这种同步方式和数据库事务锁有关,在数据库设置中将隔离级别设为repeatable-read,在事务中使用select for update语句后可以保证当前事务更新过程中其他事务无法更新同一列数据,可以用于不同机器中的变量累加计数。

使用数据库锁的优势与劣势
  • 掌握数据库是编程的基本技能之一,所以使用数据库来同步变量对于开发者而言实现简单,不用依赖其他中间件,开发者也会信赖从数据库层面保证的一致性。
  • 由于数据库是使用文件存储,变量同步的过程除了网络消耗,还有磁盘io的消耗,轮询的cpu消耗,因此对于高频率同步的变量(比如秒杀,累加),同步消耗很会成为一个瓶颈,这种场景一般使用下一段提到的缓存数据库锁。

缓存数据库存储锁

缓存数据库和普通数据库的一大区别就是运行在内存上,就个人使用电脑而言一块ddr3内存条的读速度可以达到5G,而一块7200r/s的机械硬盘的读速度只有300M,可见缓存数据库具有天然的io优势。目前网上大多秒杀场景所设计的架构都使用的redis来进行库存计数。虽然redis中的事务操作和普通数据库不同(不会保证事务子操作都会被成功执行,这个可以去网上找一下相关资料),但是redis用单线程非常简单地解决了外部看来十分复杂的并发处理。redis的使用就相当简单了,直接在事务中进行cas操作或者使用watch命令即可。

set num 0

incre num

使用缓存数据库的做分布式锁优势与劣势
  • 使用redis做分布式锁能够轻松处理并发问题,并且是面对高并发场景的主要处理手段。
  • 缓存数据库本身也要做成集群实现高可用,有运维难度。

zookeeper实现的分布式锁

zk最开始是hadoop的子项目,主要是为了解决分布式系统中的信任问题(拜占庭将军问题),现已成为apache的顶级项目。zookeeper本身也是运行在内存上的中间件,并且可以将数据持久化到磁盘。zk本身是一套节点通知系统,各follower节点在master节点下建立临时目录并通过心跳机制保持可以实现节点之间的发现和数据共享。通过这种特性可以实现节点选举(使用在分布式系统节点故障时进行切换的场景)、负载均衡、发布订阅、集群管理、分布式锁。
可以将要共享的资源作为一个路径放在zookeeper上,并且把数据设置成持有该资源的节点id,以此达到资源持有和互斥的目的

/resource/node-id-5

使用zookeeper做分布式锁优势与劣势
  • 通常在集群管理的场景会使用zk,分布式锁只是其一种应用场景,但并不清楚通过节点共享数据的性能相较于两种数据库如何,建议可以在并发量不高的业务场景中尝试。
  • zk作为分布式协调中间件,在许多分布式中间件中为实现高可用使用到(如kafka,hadoop,dubbo)。但是对于大多数非一线开发者来说并不容易接触到,并且集群运维保障也有难度。因此对于公司而言的使用成本,对于开发者来说的学习成本都不低