分布式锁解决之道

在涉及资源共享的问题上,传统应用的解决方法是利用synchronized或者Lock来实现线程锁,从而达到资源访问控制的目的。但是当资源的访问控制涉及到多个进程时,情况就有了变化。
下面就介绍几种常见的解决方案:

数据库

1. 悲观锁 – 基于事务

CREATE TABLE `base_admin`.`lock_t`  (
  `id` tinyint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `lock_name` varchar(36) NOT NULL COMMENT '锁实例',
  `lock_desc` varchar(64) NULL COMMENT '描述',
  `create_time` timestamp NULL COMMENT '创建lock_t时间',
  PRIMARY KEY (`id`),
  UNIQUE INDEX `uq_lock_name`(`lock_name`)
);

对lock_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功。业务处理完后delete这条数据

INSERT INTO lock_t(lock_name, desc) VALUES ('lock_name', 'desc');

当然,我们推荐使用select for update 来处理
select * from lock_t for update 会等待行锁释放之后,返回查询结果。
select * from lock_t for update nowait 不等待行锁释放,提示锁冲突,不返回结果
select * from lock_t for update wait 5 等待5秒,若行锁仍未释放,则提示锁冲突,不返回结果
select * from lock_t for update skip locked 查询返回查询结果,但忽略有行锁的记录

不过锁定(Lock)的数据是判别就得要注意一下了。以mysql来说,由于InnoDB 预设是Row-Level
Lock,所以只有「明确」的指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

// 明确指定主键,并且有此数据,row lock
SELECT * FROM lock_t WHERE id='3' FOR UPDATE;
// 明确指定主键,若查无此数据,无lock
SELECT * FROM lock_t WHERE id='-1' FOR UPDATE;
// 无主键,table lock
SELECT * FROM lock_t WHERE lock_name='lock_name' FOR UPDATE;
// 主键不明确,table lock
SELECT * FROM lock_t WHERE id<>'3' FOR UPDATE;
// 主键不明确,table lock
SELECT * FROM lock_t WHERE id LIKE '3' FOR UPDATE;

悲观锁的实现比较简单但是性能比较低,也容易出现死锁情况,对于高并发下场景不推荐使用

2. 乐观锁 – 基于版本号

CREATE TABLE `base_admin`.`lock_t`  (
  `id` tinyint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `lock_name` varchar(36) NOT NULL COMMENT '锁实例',
  `lock_desc` varchar(64) NOT NULL COMMENT '描述',
  `create_time` timestamp NOT NULL COMMENT '创建时间',
  `version` tinyint NOT NULL COMMENT '版本号',
  PRIMARY KEY (`id`),
  UNIQUE INDEX `uq_lock_name`(`lock_name`)
);
// 先获取id和版本号
select id, version from method_lock where lock_name='lock_name';
// 再更新
update lock_t set version=version + 1 where id = #{id} and version = #{version};

乐观锁的性能高于悲观锁,并不容易出现死锁。但是对于高并发下场景也不推荐使用
基于数据库实现需要考虑单点的问题

Memcached

Memcached 可以使用 add 命令,该命令只有KEY不存在时,才进行添加,或者不会处理。Memcached
所有命令都是原子性的,并发下add 同一个KEY ,只会一个会成功。
利用这个原理,可以先定义一个 锁 LockKEY ,add 成功的认为是得到锁。并且设置[过期超时] 时间,保证宕机后,也不会死锁。
在具体操作完后,判断是否此次操作已超时。如果超时则不删除锁,如果不超时则删除锁。

   // 得到锁
  if (mc.Add("LockKey", "Value", expiredtime)){
   try{
       doSomething();
       // 检查超时
       if (!CheckedTimeOut()){
           mc.Delete("LockKey");
       }
   }catch (Exception e){
       mc.Delete("LockKey");
   }
}

Redis

1. setnx [key] [value]

setnx [key] [value] 如果名称为[key]的字符串不存在的话,生成一个key为[key],值为[value]的字符串,返回结果为1;如果字符串已经存在,则什么都不做,返回0。setnx即为set if not exists的意思。
当多个进程竞争同一个锁时,只需要执行 setnx [key] [value],根据命令的返回结果判断锁的归属。获取锁的进程执行完业务逻辑后,执行del命令删除字符串,之后其他进程就能继续获取锁。
在这个过程中,存在一个问题,就是当获取锁的进程在释放锁之前,因为某种原因挂掉了,导致锁没有释放,也就是redis中一直存在某个字符串,其他进程去获取某个锁时,发现这个锁对应的字符串已经存在,之后不停地等待,造成死锁。于是,要给锁引入一个过期时间,超过某个时间后,锁自动释放,即代表锁的字符串被删除。

2. set key value [ex seconds] [px milliseconds] [nx|xx]

举个例子:set name suo ex 100 nx 的意思即是设置key为name值为suo的字符串,字符串的过期时间(ex代表过期时间)为100s,不存在时才创建(nx);
由于redis的操作具有原子性,可以保证我们在获取锁的同时一定会设置过期时间。如果分成setnx设置字符串与expire设置过期时间两条命令,在极端情况下会发生进程获取到锁,但是在用expire命令给锁设置过期时间之前,突然挂掉了的情况,造成死锁。
为保证服务器资源的高利用效率,可以不用等到锁自动过期才删除,删除需要用lua脚本保证原子性
为保证其他客户端不会删除另外一个客户端持有的锁,建议key是一个随机生成的值

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

redis在进行主从复制时是异步完成的,比如在clientA获取锁后,主redis崩溃了,数据还没有复制到从redis中,然后从redis选举出一个升级为主redis,造成新的主redis没有clientA 设置的锁,这时clientB尝试获取锁,并且能够成功获取锁,导致互斥失效

3. redlock算法(Redisson支持)

请看官网https://redis.io/topics/distlock
个人觉得不太适用,需要N个独立的master节点,这些节点完全互相独立,不存在主从复制或者其他集群协调机制,redis主要用途是做缓存,搭建的集群模式,如果为了满足redlock,个人觉得还是用ZK实现更好,虽然ZK性能不一定很高。

In the distributed version of the algorithm we assume we have N Redis masters. Those nodes are totally independent, so we don’t use replication or any other implicit coordination system. We already described how to acquire and release the lock safely in a single instance. We take for granted that the algorithm will use this method to acquire and release the lock in a single instance. In our examples we set N=5, which is a reasonable value, so we need to run 5 Redis masters on different computers or virtual machines in order to ensure that they’ll fail in a mostly independent way.

In order to acquire the lock, the client performs the following operations:

  1. It gets the current time in milliseconds.
  2. It tries to acquire the lock in all the N instances sequentially, using the same key name and random value in all the instances. During step 2, when setting the lock in each instance, the client uses a timeout which is small compared to the total lock auto-release time in order to acquire it. For example if the auto-release time is 10 seconds, the timeout could be in the ~ 5-50 milliseconds range. This prevents the client from remaining blocked for a long time trying to talk with a Redis node which is down: if an instance is not available, we should try to talk with the next instance ASAP.
  3. The client computes how much time elapsed in order to acquire the lock, by subtracting from the current time the timestamp obtained in step 1. If and only if the client was able to acquire the lock in the majority of the instances (at least 3), and the total time elapsed to acquire the lock is less than lock validity time, the lock is considered to be acquired.
  4. If the lock was acquired, its validity time is considered to be the initial validity time minus the time elapsed, as computed in step 3.
  5. If the client failed to acquire the lock for some reason (either it was not able to lock N/2+1 instances or the validity time is negative), it will try to unlock all the instances (even the instances it believed it was not able to lock).

Redisson的用法:

Config config1 = new Config();
config1.useSingleServer().setAddress("redis://ip:port1").setPassword("xx").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://ip:port2").setPassword("xx").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://ip:port3").setPassword("xx").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String resourceName = "REDLOCK_KEY";
RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);
// 向3个redis实例尝试加锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
    // isLock = redLock.tryLock();
    // 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    System.out.println("isLock = "+isLock);
    if (isLock) {
       doSomething()
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    redLock.unlock();
}

Zookeeper

基于zookeeper临时有序节点可以实现的分布式锁
每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下(这里的是一个持久节点),生成一个唯一的临时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机(watch机制临时节点会删除)导致的锁无法释放,而产生的死锁问题。
可以解决缓存的

Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。

InterProcessMutex interProcessMutex= new InterProcessMutex(client,"/d_lock");

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {    
    try {        
        return interProcessMutex.acquire(timeout, unit);    
    } catch (Exception e) {        
        e.printStackTrace();    
    }    
    return true; 
} 
 
public boolean unlock() {    
    try {        
        interProcessMutex.release();    
    } catch (Throwable e) {        
        log.error(e.getMessage(), e);    
    } finally {        
        executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);    
    }    
    return true; 
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值