背景

单机部署环境下,用synchronized关键字和ReentrantLock可重入锁可以控制多线程环境中对共享资源的并发访问。但是随着分布式的发展,本地加锁已经无法满足我们的需要,所以在分布式环境中就需要一种跨JVM的互斥机制来控制共享资源的访问。

CAP理论

一致性(Consistency):写操作之后的读操作,必须返回该值。 可用性(Availability):收到用户的请求,服务器就必须给出回应。 分区容错性(Partition tolerance):多个子网络组成了多个分区。分区容错的意思是,区间通信可能失败。一般无法避免。 一致性与可用性的矛盾:不可能同时成立,因为可能同时存在(出现分区容错)。保证一致性的时候会牺牲可用性,追求可用性则无法满足一致性。 分布式锁的一些特点 互斥性:指的是共享资源只允许一个访问者对其进行访问,具有唯一性和排他性,譬如写数据的操作。分布式环境下还需要保证不同节点的不同线程的互斥。 可重入性:同一节点上的同一个线程获取锁之后可再次获取这个锁。 锁超时:与本地锁一样,支持超时释放,防止死锁。 高效率,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。 支持阻塞和非阻塞:和ReentrantLock一样支持lock和tryLock以及tryLock(long timeOut) 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。 常用分布式锁及其原理

MySQL锁

核心思路:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

CREATE TABLE IF NOT EXISTS `method_lock` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` VARCHAR(45) NOT NULL COMMENT '锁定的方法名称',
  `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据的时间',
  PRIMARY KEY (`id`),
  UNIQUE INDEX `method_name_UNIQUE` (`method_name` ASC) )
ENGINE = InnoDB
COMMENT = '锁定的方法';

优缺点以及解决办法 优点

实现方式简单,代码复杂度低。

缺点

因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换。 不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程信息相同,若相同则直接获取锁; 没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在锁中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据; 不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。

redis

使用原因

Redis性能很高 Redis命令支持较好,实现方便 主要命令

SETNX
SETNX key val: 当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0
EXPIRE
EXPIRE key timeout: 为key设置一个超时时间,单位为秒,超过时间锁自动释放,避免死锁
DELETE
DELETE key:删除key

实现思想:

获取锁的时候,用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。 获取锁的时候还需设置一个获取的超时时间,若超过这个时间则放弃获取锁。 释放锁的时候,通过UUID判断是否是该锁,若是,则执行delete进行锁释放。

redisson
RedissonClient redis = Redisson.create();
RLock lock = redis.getLock("resourceName");
//加锁
lock.lock();
//尝试加锁5s,锁过期时间10s
lock.lock(5, 10, TimeUnit.SECONDS);
//支持非阻塞异步操作
RFuture<Boolean> rFuture = lock.tryLockAsync(5, 10, TimeUnit.SECONDS);
rFuture.whenCompleteAsync((result, throwable) -> {
    System.out.println("当前加锁的情况:" + result + throwable);
});
//解锁
lock.unlock();
//使用
RLock lock = null;
        try {
            lock = distributedLocker.acquireLock(param.getAlarmCode());
            lock.lock(5, TimeUnit.SECONDS);
            ...... 
         } catch (InterruptedException e) {
            return ResultResponse.createFailureResponse(BizCode.SYSTEMERROR);
        } finally {
            if(lock!=null){
                lock.unlock();
            }
        }
// 获取锁的方法
public RLock acquireLock(String resourceName) throws InterruptedException {
        RedissonClient redisson = redissonConnector.getClient();
        String wxb_prefix = active + "_" + LOCKER_PREFIX;
        RLock lock = redisson.getLock(wxb_prefix + resourceName);
        // Wait for 100 seconds seconds and automatically unlock it after lockTime seconds
        boolean success = lock.tryLock(5, 5, TimeUnit.SECONDS);
        return lock;
    }

zookeeper

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。

基于ZooKeeper实现分布式锁的步骤如下:

创建一个目录mylock; 线程A想获取锁就在mylock目录下创建临时顺序节点; 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; 线程B获取所有节点,判断自己是不是最小节点,设置监听比自己次小的节点; 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小节点,如果是则获取锁。 优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。