分布式锁原理与实现(数据库、redis、zookeeper)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 分布式锁可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。

分布式锁


分布式锁可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。

分布式锁的实现方式有:

  1. 数据库实现分布式锁:原理简单,性能较差
  2. Redis分布式锁:性能最好
  3. Zookeeper分布式锁:可靠性最好



一、数据库实现分布式锁

数据库实现分布式锁的思路,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。具体实现方式有多种:


当我们要锁住某个方法或资源的时候,就在该表中增加一条记录;想要释放锁的时候,就删除这条记录。

可以基于乐观锁实现。

也可以利用数据库自带的排它锁实现。

由于需要连数据库,适用于对性能要求不高的场景如集群环境下的定时任务等。


参考:《基于数据库的分布式锁实现》https://blog.csdn.net/lmb55/article/details/78495629


二、Redis实现分布式锁


Redis实现分布式锁的思路主要是,获取锁的时候在redis中存储一个特定的key-value,释放锁的时候删除这个key-value。具体实现有多种方式。


1、【setnx】命令实现分布式锁(set if not exist)


一般思路是先用setnx命令设置一个指定的key-value来获取锁(同一业务逻辑获取的分布式锁对应的key固定,value随意),释放的时候用del命令删除这个key-value。这样做可能出现一个问题,如果释放锁(del key)之前系统挂了,redis中的这个key-value会一直存在,也就是会造成死锁。


因此可以用expire命令来给这个key-value加一个有效期,过一段时间即使不删除也自动失效。但由于加锁的时候,setnx和expire是分成两步来执行的,并没有原子性,如果执行expire之前系统挂了,也无法释放锁,造成死锁。当然执行expire需要依赖setnx的执行结果,如果setnx执行不成功(没抢到锁),是不应该执行expire的,所以也无法用redis事务的方式来保证这两个命令的原子性(如果用事务,及时setnx执行失败,也会继续执行expire)。


最终方案:可以通过setnx+getset命令来完美实现redis分布式锁,这种方案可以避免死锁,主要思想就是如果持有锁的线程没有及时释放锁,其他线程可以帮它释放锁。具体做法是:

(1)申请锁的时候用setnx设置key-value,key值固定,value=当前时间戳+过期时间,申请成功则获取锁成功

(2)如果申请锁失败(说明setnx执行失败,redis中已经有对应key了),用getset方法获取之前的值,判断锁是否已过期,如果过期了,判断设置的value


如下是用spring-data-redis实现分布式锁的例子:


public boolean lock(String redisKey,long expireMsecs) {
    try {
        long currentLockValue = System.currentTimeMillis() + expireMsecs + 1;
        boolean lockResult = redisTemplate.opsForValue().setIfAbsent(redisKey, currentLockValue);
        //成功获取得锁
        if(lockResult) {
            return true;
        }
        //如果redisKey存在,但已达到过期时间,则重新进入争抢
        Long lockValue = (Long)this.getRedisTemplate().opsForValue().get(redisKey);  //2019年04月08日12:00:10|000
        long currentTimeMillis = System.currentTimeMillis();  //2019年04月08日12:00:00|100
        if(lockValue != null && lockValue < currentTimeMillis) {
            Long oldLockValue = (Long)redisTemplate.opsForValue().getAndSet(redisKey, currentLockValue);  //2019年04月08日12:00:10|000
            //确保set的时候,没有其它线程进行getset操作
            if(oldLockValue != null && oldLockValue.equals(lockValue)) { 
                return true;
            }
        }
    }catch (Exception e) {
        logException(bizAction, "exception", getLockKey(), e);
    }
    return false;
}
public boolean unLock(){
    redisTemplate.delete(getLockKey());
}

当锁过期重新进入争抢的时候,比如之前redis中存的时间value是5,现在时间currentLockValue是10,所以现在的锁过期了。这时线程A和线程B同时(在同一毫秒)争抢锁,线程A先执行getset,获取到的oldLockValue=5,同时把当前时间currentLockValue 10放到缓存中,线程2再执行getset时,获取到的oldLockValue=10,这时比较线程A获取到的oldLockValue和之前的lockValue值一样,就表示A获取到了锁。

111.png


这种方案还有个小问题就是,需要依赖每个服务器节点的时间,因此需要保证每个服务器的时间一致。


2、用【set key value [EX seconds] [PX milliseconds] [NX|XX]】命令实现分布式锁。


redis2.8之后,扩展了set命令的参数,可以直接执行用一个命令来原子执行set和expire。


3、用lua脚本实现redis分布式锁


4、Redlock算法


三、用Zookeeper实现分布式锁


Zookeeper锁原理:通过Zookeeper上的数据节点来标识一个锁,例如/curator/lock。Zookddper分布式锁与Redis分布式锁相比相比,实现的稳定性更强,这是因为zookeeper的特性所致,在外界看来,zookeeper集群中每一个节点都是一致的。


1、Zookeeper实现分布式锁


下面描述使用zookeeper实现分布式锁的算法流程,假设锁空间的根节点为/curator/lock:


客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/curator/lock/lock-0000000000,第二个为/curator/lock/lock-0000000001,以此类推。

客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/curator/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁;

执行业务代码;

完成业务流程后,删除对应的子节点释放锁。

创建的临时节点能够保证在故障的情况下锁也能被释放,考虑这么个场景:假如客户端a当前创建的子节点为序号最小的节点,获得锁之后客户端所在机器宕机了,客户端没有主动删除子节点;如果创建的是永久的节点,那么这个锁永远不会释放,导致死锁;由于创建的是临时节点,客户端宕机后,过了一定时间zookeeper没有收到客户端的心跳包判断会话失效,将临时节点删除从而释放锁。


对于这个算法有个极大的优化点:假如当前有1000个节点在等待锁,如果获得锁的客户端释放锁时,这1000个客户端都会被唤醒,这种情况称为“羊群效应”;在这种羊群效应中,zookeeper需要通知1000个客户端,这会阻塞其他的操作,最好的情况应该只唤醒新的最小节点对应的客户端。应该怎么做呢?在设置事件监听时,每个客户端应该对刚好在它之前的子节点设置事件监听,例如子节点列表为/lock/lock-0000000000、/lock/lock-0000000001、/lock/lock-0000000002,序号为1的客户端监听序号为0的子节点删除消息,序号为2的监听序号为1的子节点删除消息。调整后的分布式锁算法为:


客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推;

客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;

执行业务代码;

完成业务流程后,删除对应的子节点释放锁。


113.png


如下是用Curator实现分布式锁的例子:

public class ZookeeperDistributeLock{
    private static String lockPath = "/curator/lock";
    private static CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("33.101.98.109:2181")
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
            .build();
    public static void main(String[] args) throws Exception {
        client.start();
        final InterProcessMutex lock = new InterProcessMutex(client, lockPath);
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss|SSS");
        for (int i = 1; i <= 50; i++) {
            final int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        lock.acquire();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    String orderNo = simpleDateFormat.format(new Date());
                    try {
                        lock.release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("生成的第" + (finalI) + "个订单号是:" + orderNo);
                }
            }).start();
        }
        System.out.println("1秒后开始并发生成订单号……");
        Thread.sleep(1000);
        countDownLatch.countDown();
    }
}

2、Zookeeper读写锁


也可以通过Zookeeper来获取分布式读写锁,在获取读写锁时,也是通过数据节点来表示一个锁。请求锁时,在锁节点(比如/lock)下创建格式为“/lock/类型-序号”的临时顺序节点,比如“R-0000001”、“W-0000002”、“R-0000003”:


114.png


获取读写锁流程分析:


在获取读锁时,客户端在/lock节点下创建/R-为前缀的临时顺序节点,比如“R-0000001”、“R-0000003”;在获取写锁时,客户端在/lock节点下创建/W-为前缀的临时顺序节点,比如“W-0000002”。

创建节点后,获取/lock下所有子节点,确定当前节点在所有子节点中的位置,并对最近的子节点设置Watcher监听。

对于读锁请求,如果没有比自己序号小的节点,或者所有比自己序号小的节点都是读请求,则成功获取到读锁,否则进入等待。

对于写请求,如果自己是序号最小的节点,则成功获取到写锁,否则进入等待。

Curator已经为我们实现了多种分布式锁:


InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器


总结


数据库分布式锁、Redis分布式锁、Zookeeper分布式锁的比较


1.理解的难易程度

数据库>Redis>Zookeeper

2.实现的复杂程度

Zookeeper>=Redis>数据库

3.性能高低

Redis>Zookeeper>数据库

4.可靠性

Zookeeper>Redis>数据库


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6天前
|
NoSQL Java 关系型数据库
【Redis系列笔记】分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
136 2
|
6天前
|
存储 监控 NoSQL
【Redis】分布式锁及其他常见问题
【Redis】分布式锁及其他常见问题
32 0
|
6天前
|
NoSQL Java Redis
【Redis】Redis实现分布式锁
【Redis】Redis实现分布式锁
8 0
|
6天前
|
监控 NoSQL 算法
探秘Redis分布式锁:实战与注意事项
本文介绍了Redis分区容错中的分布式锁概念,包括利用Watch实现乐观锁和使用setnx防止库存超卖。乐观锁通过Watch命令监控键值变化,在事务中执行修改,若键值被改变则事务失败。Java代码示例展示了具体实现。setnx命令用于库存操作,确保无超卖,通过设置锁并检查库存来更新。文章还讨论了分布式锁存在的问题,如客户端阻塞、时钟漂移和单点故障,并提出了RedLock算法来提高可靠性。Redisson作为生产环境的分布式锁实现,提供了可重入锁、读写锁等高级功能。最后,文章对比了Redis、Zookeeper和etcd的分布式锁特性。
144 16
探秘Redis分布式锁:实战与注意事项
|
6天前
|
NoSQL Java 大数据
介绍redis分布式锁
分布式锁是解决多进程在分布式环境中争夺资源的问题,与本地锁相似但适用于不同进程。以Redis为例,通过`setIfAbsent`实现占锁,加锁同时设置过期时间避免死锁。然而,获取锁与设置过期时间非原子性可能导致并发问题,解决方案是使用`setIfAbsent`的超时参数。此外,释放锁前需验证归属,防止误删他人锁,可借助Lua脚本确保原子性。实际应用中还有锁续期、重试机制等复杂问题,现成解决方案如RedisLockRegistry和Redisson。
|
6天前
|
存储 供应链 安全
区块链技术原理及应用:深入探索分布式账本技术
【4月更文挑战第30天】区块链,从加密货币的底层技术延伸至多元领域,以其分布式账本、去中心化、不可篡改性及加密技术重塑数据存储与交易。核心组件包括区块、链和节点,应用涵盖加密货币、供应链管理、金融服务等。尽管面临扩展性等挑战,未来潜力无限。
|
6天前
|
存储 算法 搜索推荐
矢量数据库基础:概念、原理与应用场景
【4月更文挑战第30天】矢量数据库,处理高维向量数据的工具,应用于GIS、推荐系统、图像搜索及语义搜索。核心原理是将原始数据嵌入到高维空间,通过索引算法优化搜索性能。现代深度学习模型如Word2Vec提升向量表示准确性,KD-Tree、LSH等算法加速相似性搜索。随着技术发展,矢量数据库在数据科学领域的重要性日益增强。
|
6天前
|
缓存 NoSQL Java
【亮剑】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护,如何使用注解来实现 Redis 分布式锁的功能?
【4月更文挑战第30天】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护。基于 Redis 的分布式锁利用 SETNX 或 SET 命令实现,并考虑自动过期、可重入及原子性以确保可靠性。在 Java Spring Boot 中,可通过 `@EnableCaching`、`@Cacheable` 和 `@CacheEvict` 注解轻松实现 Redis 分布式锁功能。
|
6天前
|
NoSQL Redis 微服务
分布式锁_redis实现
分布式锁_redis实现
|
6天前
|
NoSQL Java Redis
Redis入门到通关之分布式锁Rediision
Redis入门到通关之分布式锁Rediision
21 0