分布式锁的三种实现方式 学习总结

Java 分布式锁的三种实现方案

数据库:唯一约束
基于缓存:redis 等
zookeeper:基于zookeeper

1. 基本原理和依据

数据库实现
主要利用了数据库表中的唯一约束,唯一约束本身就无法创建相同的,也就保证了唯一性,我们可以将锁定义一个名字或其他标识存储到某张我们定义的用于管理锁的表(表id 自增,单独定义一个字段设定为唯一约束,另设置一个字段用于存储加锁的对象,用于后续同一对象解锁和排除其他对象加相同的锁)。
同一个锁由于唯一约束所以不可能存在两条锁一样的数据。

缓存redis实现
缓存redis 等基本都是内存操作,从速度上来讲可以说是最快的。
redis 主要是通过利用 Redis 的 SETNX key value 这个命令,只有当 key 不存在时才会执行成功,如果key已经存在则命令执行失败。
即将锁和操作锁的对象分别设置为 key 和 value 以此来控制锁的加入和解锁。

zookeeper实现
zookeeper 一般用于分布式的注册配置中心,通过zookeeper 操作锁实际上是在 zookeeper 创建瞬时节点,由于节点本身不能重名,所以可以利用这一点做锁操作。

但是实际要考虑的问题远不止这些,例如服务宕机,超时,锁的释放时机等等。

2. 实现个别细节

2.1.数据库实现

建表时 主要有两个字段来控制锁,id 为常规自增主键可以不用关心,然后需要定义两个额外的字段,一个字段是用于存放锁的(即定义的名称,用于区分不同的锁),同时定义唯一约束,物理上防止多次插入相同数据。
另一个字段则用于存储当前加锁成功的调用者,即如果有多个操作人操作同一个锁,那么每次都只有一个人能加锁成功。

加锁成功:即在数据库中插入一条数据,记录当前已加锁成功的锁和操作人,其他人也想加锁时会发现已经有人操作了,那么就不能操作。

解锁成功:如果操作完成需要解锁,那么就将对应的记录删除。其他操作人抢占后再次加锁。
(单纯额外添加字段管理状态依旧有可能存在被两个链接读到然后被覆盖的问题)

特殊情况:假使一个锁被某一个操作人创建成功了,但是中间可能操作人被废弃或者无法操作了等状况,那么为了释放锁给其他人用,可以额外比较当前库中的锁数据是否存在超过了一定的时间,如果超过了这个超时时间那么也解锁该锁将数据删除给其他人使用。

如果在有效时间内同一操作人再次操作那么也认为加锁成功。

如果数据库整个陷入不可用了,那么锁也就相当于不可用了,这样可以考虑数据库高可用方案,例如MySQL的MHA高可用解决方案。

2.2.redis 实现

redis 实现时大致会使用如下方法:

stringRedisTemplate.opsForValue().setIfAbsent(锁关键词key, 操作人id即value, 60, TimeUnit.SECONDS);

第一个入参就是锁的key ,相当于数据库中的加了唯一约束的锁名
第二个入参则是锁的value,即当前操作人的id 或者竞争的业务id 等
第三个和第四个是这个值的过期时间和数值的单位。

方法含义
即如果redis 中没有对应的锁的键那么就加入对应键值,否则什么都不做。而且一旦超时后,会自动释放该键值。

主动释放锁
这里手动写入了redis 的脚本去操作是为了保证操作的原子性,即一次就完成全部操作,保证操作的对象只有一个。

	private static final Long RL_SUCCESS = 1L;

    public static boolean releaseDistributedLock(StringRedisTemplate stringRedisTemplate, String key, String value) {
    
    
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(lockKey), requestId);
        return RL_SUCCESS.equals(result);
    }

如果分为两步代码操作可能会出现两个操作都通过了第一个的键判断,然而最终结果被后面的操作覆盖。

为了应对这个,其实已经提供了完善的redis 锁的依赖 Redisson
Redisson 内部实现实际也是通过 手写 脚本来保证原子性的。

Redisson 实现分布式锁
依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.6.5</version>
</dependency>

初始化Bean:

@Bean
public RedissonClient redisson(){
    
    
    // 单个
    Config config = new Config();
    config.useSingleServer().setAddress("redis://xxx.xxx.xx.xxx:6379").setDatabase(0);
    return Redisson.create(config);
}

加锁与解锁

	@Autowired
    private RedissonClient redisson;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

	
		String lockKey = "key_lock001";
        // 获取锁
        RLock redissonLock = redisson.getLock(lockKey);
        try{
    
    
            // 加锁
            redissonLock.lock();  // 等价于 setIfAbsent(key, value, 10,TimeUnit.SECONDS);
            // 从redis 中拿当前值
            int business1 = Integer.parseInt(stringRedisTemplate.opsForValue().get("business1"));
            if(business1 > 0){
    
    
                int realStock = business1 - 1;
                stringRedisTemplate.opsForValue().set("business1",realStock + "");
                System.out.println("减少1,剩余:" + realStock);
            }else{
    
    
                System.out.println("减少失败,已用完");
            }
        }finally {
    
    
            // 释放锁
            redissonLock.unlock();
        }

参考资料 https://blog.csdn.net/qq_42764269/article/details/122412431

2.3.zookeeper实现

zookeeper 的数据存储方式是类似树形的,所有的节点被称为 Znode。
Znode 的大致分两类 持久节点以及临时节点,每一类又分为顺序和无顺序,所以一共四种
zookeeper 分布式锁利用的是 临时顺序节点,临时节点在创建后如果不再使用则会被删除,加入顺序后如果连续创建会自动添加名称编号用于区分。

实际实现时,每次请求创建锁都是在某一指定根节点下创建临时顺序节点,创建后如果有多个则会按顺序排序下去,我们认为如果自己是最小的节点即按顺序节点排序创建后我们之前没有其他节点只有自己,那么就认为当前的这个请求创建的节点获得了锁。

其他临时节点排序后会依次向自己的前一个节点注册watcher(监控),例如,第二个会监控第一个,第三个会监控第二个。

释放锁:当最小的节点因为业务结束而主动删除锁或者超时,或者链接断开崩溃了都会将临时节点删除,使得下一个排队的或者全部丢失后重新排队获得锁。

另外由于zookeeper 是集群部署,只要一半以上的机器存活就依旧可以保证正常使用。

zookeeper 第三方客户端 curator 中已经实现了基于 zookeeper 的分布式锁。

加锁和解锁大致如下:
curator 实现

@Autowired
private CuratorFramework curatorFramework;

// 加锁,支持超时,可重入
public boolean setLock(long timeout, TimeUnit unit) throws InterruptedException {
    
    
    //
    InterProcessMutex interProcessMutex= new InterProcessMutex(curatorFramework, "/ParentNodeLock");
    try {
    
    
        return interProcessMutex.acquire(timeout, unit);
    } catch (Exception e) {
    
    
        e.printStackTrace();
    }
    return true;
}
// 解锁
public boolean releaseLock() {
    
    
InterProcessMutex interProcessMutex= new InterProcessMutex(curatorFramework,  "/ParentNodeLock");
    try {
    
    
        interProcessMutex.release();
    } catch (Throwable e) {
    
    
        log.error(e.getMessage(), e);
    } finally {
    
    
        executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
    }
    return true;
}

常用锁
InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁

参考学习来源:https://blog.csdn.net/qq_42764269/article/details/122435977

猜你喜欢

转载自blog.csdn.net/weixin_44131922/article/details/131727746