分布式锁实现的几种方式

本文介绍了分布式锁的几种实现方式,包括数据库锁(基于表记录、乐观锁、悲观锁)、基于Redis实现分布式锁,以及基于zookeeper实现分布式锁(排他锁和共享锁),还给出了基于Redis实现的代码,最后提醒转载需指明出处。

分布式锁实现的几种方式

写在前面:本篇文章主要搬自本人的云笔记,不当之处,欢迎指正,共同学习;

1、数据库锁的方式

1.1、基于表记录的实现方式

准备条件:创建表ABC,并做唯一索引version;
//1、线程A加锁:
    insert into ABC(version)values (versionID);
//2、线程A释放锁:
    delete from ABC where version =versionID;
如果数据库中有该锁记录,线程B再保存数据进行加锁就会发生唯一索引异常,这时就成功实现了分布式锁;
缺点:只适用于并发量不大的业务场景,这里xxl-job的管理后台上使用了这种方式;
    容易发生死锁,但是可以起一个定时任务去清理它;

1.2、基于乐观锁的实现方式

乐观锁主要根据加版本号的方式实现,原来如下:A、B线程执行更新操作之前先查询版本号,然后根据版本号进行更新,如果版本号被线程A更新了,
    这时线程B再用该版本号执行更新就会更新失败,从而成功实现加锁;这里需要注意此种锁为不可重入锁,只能重新发起业务请求;

//1、查询出数据库中的版本号
    select ID,version from ABC;//获取到versionID
//2、对该条记录进行更新
    update ABC set business = business-1 , version = versionID+1 where version=versionID and id =ID;
这时如果线程B再用原来的versionID进行更新,就失败了;
缺点:需要维护版本字段,造成数据库表结构的冗余;
    只适用于并发不高,写操作少的情况;

1.3、基于悲观锁的实现方式

//1、由于MySQL是默认自动事务提交的,这里需要先关闭事务自动提交
    SET AUTOCOMMIT = 0;(有没人知道使用spring事务的时候,怎么不关闭自动提交呢?)
//2、对于InnoDB的数据表来说,支持表级锁和行级锁,当查询时不走索引的时候加的就是表级锁;对于MYISAM(不支持事务)的数据表,只支持表级锁;
    select * from ABC where a.version =versionID for update;//行级锁
    select * from ABC where a.version >versionID for update;//表级锁
    select * from ABC where a.business =businessID for update;//表级锁
//3、读锁属于共享锁,A线程对该条记录上共享锁后,B线程也可以上共享锁,但不能上排它锁;
    写锁属于排它锁,A线程对该条记录上排它锁后,B线程不能再上任何锁;
//4、这里多补充一点Oracel中有for update nowait语法,使用该语法不会像for update当该行被锁定时一直等待,而是直接报错;
    MySQL中可以是通过配置 innodb_lock_wait_timeout的时间指定该查询会话等待的时间,超时就直接报错

2、基于Redis实现分布式锁

优点:redis是单线程执行的,所以它天然的支持分布式锁
缺点:超时时间的设置不当,会对服务性能有较大影响(我们的项目设置的8s)

实现代码如下:

//1、加锁基于set key value [EX seconds|PX milliseconds] [nx|xx];
    EX 表示超时时间精度是秒
    PX 表示超时时间精度是毫秒
    NX 表示只有当该key 不存在时才保存
    xx 表示只有当该key存在时才覆盖
public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }
}
2、解锁使用Lua脚本,保证操作的原子性
public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

3、基于zookeeper实现分布式锁

3.1、排他锁的原理,主要应用zk上到临时节点

1、多个client共同去zk上预定义好的/lock节点下创建一个相同的临时节点/abc;
2、哪个节点创建成功之后,就说明该会话拿到了锁;
3、其他创建失败的client,继续保持对该节点的监听;
4、当之前拿到锁的client会话结束或者主动释放了锁,这时其他的client再次去/lock下创建/abc,以便获取锁

缺点:每一次只有一个client注册成功,其他的client都会失败,然后触发监听时还要通知每一个client,再次去竞争锁,这里如果请求量大的话,会引发羊群效应,这里可以尝试使用共享锁

3.2、共享锁的原理,主要应用zk上的临时有序节点

1、当多个请求到来时,同样去zk上预定义好的/lock节点下创建节点,但是是临时有序节点/abc0000001,这样每个请求都可以创建成功
2、当创建成功后,并不是每个节点都能够拿到锁,只有当前有序节点之前没有其他节点,即当前节点的序号是最小的时候,才说明了该节点拿到了锁;
3、拿到锁的请求,会话结束后,删除掉对应的临时节点即可;
4、其他没有拿到的锁的请求只需要保持对它前一个节点的监听,当它前面的节点不存在了,自己变成了最小的序号了,这就说明自己拿到了锁

未完待续。。。。。。

总结

整理不宜,转载请指明出处,共同进步吧!

Redisson 是一个用于 Redis 的 Java 客户端,它提供了许多高级功能,其中最引人注目的就是其对分布式锁的支持。Redisson 的分布式锁机制不仅限于基本的锁功能,还扩展出了多种实现方式以适应不同的应用场景。以下是 Redisson 实现分布式锁几种主要方式: ### 可重入锁(Reentrant Lock) 可重入锁是最常见的锁类型之一,它允许同一个线程多次获取同一个锁而不会发生死锁。这种锁机制对于递归调用或者在同一个线程中多次需要获取锁的情况非常有用。Redisson 的可重入锁实现了 `java.util.concurrent.locks.Lock` 接口,提供了与 Java 标准库中 `ReentrantLock` 类似的 API,但其作用范围是跨 JVM 的,这意味着它可以协调不同节点上的线程对共享资源的访问[^1]。 ### 公平锁(Fair Lock) 公平锁确保了锁的获取遵循请求的顺序,即先请求锁的线程会比后请求的线程优先获得锁。这种方式可以有效防止某些线程长时间得不到锁而导致的饥饿现象。Redisson 的公平锁同样实现了 `java.util.concurrent.locks.Lock` 接口,并通过 Redis 的发布/订阅功能来维护锁请求的顺序,从而保证了锁分配的公平性。 ### 联锁(MultiLock) 联锁是一种特殊的锁,它由多个锁组成,这些锁可以属于不同的 Redis 实例。当使用联锁时,所有涉及的锁必须同时被获取成功,否则整个获取操作失败。这种方式适用于需要跨多个服务或数据源保持一致性的场景。Redisson 的 `RLock` 接口提供了一个 `multiLock` 方法来创建联锁实例,这使得开发者能够轻松地在分布式环境中实现复杂的同步需求。 ### 红锁(RedLock) 红锁是 Redisson 提供的一种高级分布式锁算法实现,它基于 Redis 的多个独立实例来提高锁服务的可用性和可靠性。红锁算法的核心思想是在多个 Redis 节点上尝试获取锁,只要大多数节点成功获取了锁,则认为整个锁操作成功。这种方法不仅提高了系统的容错能力,还增强了锁服务的整体稳定性。Redisson 的红锁实现简化了开发者在构建高可用分布式系统时的复杂度,使得即使在部分节点故障的情况下也能安全地管理分布式资源。 ### 读写锁(ReadWriteLock) 读写锁允许多个读操作并发执行,但写操作与其他所有操作互斥。这种锁机制特别适合于读多写少的应用场景,因为它可以在不影响数据一致性的前提下显著提升系统性能。Redisson 的读写锁同样遵循了 `java.util.concurrent.locks.ReadWriteLock` 接口的设计理念,但在实现上考虑到了网络延迟等因素的影响,确保了在分布式环境下的高效性和正确性。 ### 信号量(Semaphore) 虽然严格意义上不属于锁的范畴,但信号量也是一种重要的同步工具,它可以控制同时访问的线程数量。Redisson 的信号量实现允许设置许可的数量,线程可以通过 `acquire` 和 `release` 方法来获取和释放许可。这对于限制对有限资源的并发访问非常有用,例如控制数据库连接池的大小等场景。 ### 闭锁(CountDownLatch) 闭锁是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。Redisson 的闭锁实现支持初始化一个计数器,每当某个事件发生时,计数器减一,直到计数器达到零,等待的线程才会继续执行。这种机制非常适合用于协调多个任务之间的启动或完成条件[^1]。 ### 示例代码 以下是一个使用 Redisson 获取可重入锁的基本示例: ```java Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("myLock"); try { // 尝试加锁,最多等待100秒,上锁以后10秒钟自动解锁 boolean isLocked = lock.tryLock(100, 10, TimeUnit.SECONDS); if (isLocked) { // 执行业务逻辑 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } ``` 通过上述各种锁机制的介绍可以看出,Redisson 不仅为开发者提供了丰富的分布式锁实现,还通过简洁易用的 API 设计降低了使用门槛,使得即使是复杂的分布式协调问题也能得到优雅而高效的解决。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值