Java技术栈各种风骚的分布式锁
Posted 西二旗阵地上的Tsinghua兵
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java技术栈各种风骚的分布式锁相关的知识,希望对你有一定的参考价值。
关键词:Java锁、分布式锁
在多线程/多进程编程场景下,锁是实现对共享资源"独占"的一种常用工具。本文不打算吹逼经典JVM的线程模型:Java的线程如何和不同操作系统OS级的进程/线程通过JNI和OS的C动态库挂接、线程队列、线程调度模型、JVM级的自旋锁/偏向锁/重入锁、CAS等等,诸如此类。打算在应用实操层面,从Java单机(单实例)的内存锁,扩展到分布式多实例部署下的分布式锁浪一发。
1. Java单实例内的内存锁
(1)吾辈已X到想吐的synchronized(Monitor Object)
Java基于Monitor Object模式,将锁的语义内置到语言内Object上的关键词synchronized。示例如下:
这是一个典型的基于用户请求建立缓存的姿势(本示例用的EhCache本地内存缓存)。当从指定的缓存key取不到数据时,需要从底层的Presto查取数据,如果不加锁,此时大量的请求过来,查询会全部落到Presto(这里Presto通过catalog配置Hive连接器连接了对应的Hive表,若干Hive表存了亿量级数据),同时不断重复刷新对应key的缓存,浪费时间和资源,引起性能恶化。
synchronized具体咋实操就毋庸赘言了。需要说明的是箭头处为啥取的是url.intern()的对象锁。这里Presto的查询url其实就是sql语句,我们目的是为了对相同sql查询的结果集进行缓存。但是如果直接锁url对象,就算相同值的url输入字符串,其都是不同的对象。也就是说,相同值的url查询(sql)其实没法锁住。这里使用字符串的intern()方法巧妙解决这个问题。intern()的语义见jdk doc:
(2)更轻量级的ReentrantLock,提供了tryLock、Condition等更丰富的api。同时,jdk还提供诸如CountDownLatch、BlockingQueue、AtomicXX、CyclicBarrier等等丰富的并发sync工具类。
2. 分布式锁
分布式锁,乍一看好NB,分布式的!其实,大多成熟稳定的单点资源只要保证原子性都可以实现分布式锁,比如文件、mysql数据库、ZooKeeper、Redis等等。
为啥需要分布式锁?简单列两个场景:a. 高可用要求场景下,我们经常会把比如定时任务等重要服务部署相同的2套以上,并且大部分任务并不幂等,需要保证正常的跑且只能跑一次。一般我们都会让这些多套相同的服务去抢一个分布式锁,谁抢到谁执行;挂了的某个服务自然不会去抢了,剩余任一活着的服务抢到后执行一次,其他服务也就热备着,进而实现高可用。b. 多个进程并行的读写某个资源文件,如何保证不把该文件写乱了?诸如此类,都是跨进程需要独占共享资源的问题。传统的jdk基于进程内的内存锁机制失效了。
(1)基于文件的分布式锁
Java的NIO提供的FileLock就是mapping了文件系统的文件锁。通过FileLock,多个JVM实例进程即可实现对同一文件的读写不乱序,或者做些进程间互斥的控制逻辑。样例如下:
需要注意的是,既然是NIO文件锁,必然和操作系统、文件系统强相关,有些跨平台的坑。使用时注意看看FileLock的jdk doc说明。
(2)基于ZooKeeper的分布式锁
几乎是入门zk必操。样例如下:
通过Apache curator封装的zk client获取InterProcessMutex,调用其acquire方法抢锁。需要小心,如上图红框所示,记得在finally块中release锁,否则业务异常抛了,锁不释放就杯具鸟~
(3)基于MySQL行锁的分布式锁
经验丰富的老司机看到上述(2)中的Quartz分布式锁居然用zk搞,肯定开喷:艹,Quartz不是既支持RAM jobStore又支持jdbcjobstore么,jdbcjobstore不是基于数据库行锁就可以实现分布式锁么?yes!但是,换着姿势折腾新技能,解锁新**,其乐无穷
基于Spring Boot整合的Quartz配置如下:
Quartz如何通过数据库行锁实现的分布式锁?如下为quartz的MySQL中的锁表结构:
在需要进行Job任务互斥控制的地方,使用MySQL的InnoDB引擎,在开启事务后,执行select xx from table_name where 主键 = xx for update加行锁(悲观锁,其他线程/事务对锁定的行既不可写也不可读,阻塞等待锁。比如:
select * from QRTZ_LOCKS where lock_name = 'TRIGGER_ACCESS' for update)的特性,实现分布式锁功能。
需要注意的是,MySQL行锁的几个重要条件(否则会锁全表):InnoDB、select xx from table_name where 主键 = xx for update中的where条件必须为主键且该行记录要存在。
(4)基于Redis的分布式锁
这个就比较好理解了:Redis在处理数据读写时是单进程单线程顺序处理的,高qps只是在I/O层面通过epoll、存储层纯内存读写实现。所以,对于redis客户端而言,每条数据读写命令是原子操作。因此通过redis 2.6.12以上的版本封装的SET resource-name anystring NX EX max-lock-time
即可简单有效实现一把分布式锁。
而在Redis 2.6.12之前,没有这个原子命令,只有通过SETNX key value和set expiretime至少两条命令组合实现,2条命令夹杂着别的逻辑且需保证原子性,在实现分布式锁时很容易掉坑。
libo09@mails.tsinghua.edu.cn
以上是关于Java技术栈各种风骚的分布式锁的主要内容,如果未能解决你的问题,请参考以下文章