3、分布式锁

对于分布式锁可以从几个问题入手:

  1. 分布式锁应用的场景是什么样的呢?
  2. 分布式锁的实现是怎么样的呢?
  3. 分布式锁的实现方案应用有什么优劣势?
  4. 分布式锁的方案如何选择?等

一、分布式锁概念

1、什么是分布式锁
  • 分布式锁在分布式环境中,针对多个进程访问共享资源而提供的同步方案。相对于单进程的锁,分布式锁更加复杂,分布式的不可靠性,需要分布式锁考虑更多的东西。
  • 分布式锁作用:保证共享资源的正确性,某一时刻锁定全局资源,让进程串行化执行,能有效避免共享资源被多个进程同时访问时候出现的数据正确性问题。比如防止重复下单、解决业务层幂等问题、解决消息队列重复消费问题、解决数据不一致问题等等
2、分布式锁的设计目标

1、可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
2、这把锁要是一把可重入锁(避免死锁)
3、这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
4、这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
5、有高可用的获取锁和释放锁功能(服务高可用,系统稳健
6、获取锁和释放锁的性能要好
7、锁的自动续约与自动释放
8、代码高度抽象,业务接入非常简单
9、可视化的管理后台,监控与管理

3、常见分布式锁解决方案

MySql
Zk
Redis
自研分布式锁:如谷歌的Chubby。
etcd

二、基于mysql实现分布式锁
1、基于表主键唯一做分布式锁

原理:利用主键唯一的特性,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录即可。(联合主键或者唯一主键但是不能使自增主键)

  • 存在的问题

1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
5、这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁。

  • 当然,我们也可以有其他方式解决上面的问题。

1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3、非阻塞的?搞一个while循环,直到insert成功再返回成功。
4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
5、非公平的?再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁

  • 这种方案总体来说性能依靠于mysql,且会产生锁表等现象,在高并发场景是不适用的。
2、基于数据库排他锁做分布式锁

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意: InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给要执行的方法字段名添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
思路:我们可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,通过connection.commit()操作来释放锁。
示例:

BEGIN;(确保以下2步骤在一个事务中:)
SELECT * FROM tb_product_stock WHERE product_id=1 FOR UPDATE--->product_id有索引,锁行,无索引,表锁(加锁阶段)
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1--->更新库存
COMMIT;( 解锁阶段)

  • 存在的问题

性能依赖于mysql,同时虽然使用了索引,在查询优化的时候未必使用行锁有可能使用了表锁。也有可能引发死锁等其他意外发生

3、基于版本控制实现的乐观锁
  • 这个策略源于 mysql 的 mvcc 机制,使用这个策略其实本身没有什么问题,唯一的问题就是对数据表侵入较大,我们要为每个表设计一个版本号字段,然后写一条判断 sql 每次进行判断,增加了数据库操作的次数,在高并发的要求下,对数据库连接的开销也是无法忍受的。
    思路:我们可以对我们的表加一个版本号字段,那么我们查询出来一个版本号之后,update或者delete的时候需要依赖我们查询出来的版本号,判断当前数据库和查询出来的版本号是否相等,如果相等那么就可以执行,如果不等那么就不能执行。这样的一个策略很像我们的CAS(Compare And Swap),比较并交换是一个原子操作
    示例:

BEGIN;(确保以下2步骤在一个事务中:)
SELECT number FROM tb_product_stock WHERE product_id=1--》查询库存总数,不加锁
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1 AND number=第一步查询到的库存数--》number字段作为版本控制字段(这里版本的控制可以根据业务调整)
COMMIT;

4、mysql作为分布式锁总结
  • 优点:直接借助数据库,容易理解。缺点:会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。操作数据库需要一定的开销,性能问题需要考虑。也就是说在高并发场景数据库作为分布式锁并不适用。
三、redis作为分布式锁
  • redis实现分布式锁原理主要是利用redis中setNX命令的原子性操作来实现的。
1、基于jedis实现分布式锁
/**
 * 基于jedis实现的分布式锁
 */
public class RedisDistributeLockUtils {

    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";

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * @param jedis
     * @param lockKey
     * @param requestId   通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。
     *                    requestId可以使用UUID.randomUUID().toString()方法生成。也就是锁谁加的锁谁来释放
     * @param expiredTime
     * @return
     */
    public boolean getDistributeLock(Jedis jedis, String lockKey, String requestId, Long expiredTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expiredTime);
        if (LOCK_SUCCESS.equalsIgnoreCase(result)) {
            return true;
        }
        return false;
    }

    public boolean releaseDistributeLock(Jedis jedis, String lockKey, String requestId) {
        /**
         * 1、过程:将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,
         * ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
         * 2、原理:首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。
         * 那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。
         */
        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;
    }
}
  • 上面没有考虑使用高可用情况,在通常的互联网架构中都是使用主从或者集群模式来实现redis的高可用。而集群模式是基于redis分片技术,并不适合作为redis分布式锁高可用方案,因此常用的是redis主从模式,在通常99.9999%的情况下面,redis是没问题,但是当主redis挂掉时候,而主从数据异步进行时候,访问此时从服务器中还没有数据,就会存在锁失效问题。也就是说在极端情况下面会存在数据不一致问题。这也和redis集群是AP模型吻合。
  • 在redis官方文档中,推荐使用redlock来保证一致性问题。但是至少需要三个redis主从实例来完成,下面说说redlock算法实现。
2、redlock算法实现分布式锁
  • 在java中,实现了redlock锁的开源框架是redisson,redLock实现分布式锁的原理是:假设我们有N个Redis master节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法。我们已经描述了如何在单节点环境下安全地获取和释放锁。因此我们理所当然地应当用这个方法在每个单节点里来获取和释放锁。在我们的例子里面我们把N设成5,这个数字是一个相对比较合理的数值,因此我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁:

1、获取当前时间(单位是毫秒)。
2、轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
3、客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4、如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
5、如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。

  • springboot+redisson实现分布式锁:
    (1)、导入包
<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.5.0</version>
        </dependency>

(2)、配置文件

spring.redisson.password=123456
spring.redisson.clusters=127.0.0.1:6370,127.0.0.1:6371,127.0.0.1:6372

(3)配置redisson

@Configuration
public class RedissonConfig {
    @Value("${spring.redisson.clusters}")
    private  String cluster;
    @Value("${spring.redisson.password}")
    private String password;
    
    @Bean
    public RedissonClient getRedisson(){
        String[] nodes = cluster.split(",");
        //redisson版本是3.5,集群的ip前面要加上“redis://”,不然会报错,3.2版本可不加
        for(int i=0;i<nodes.length;i++){
            nodes[i] = "redis://"+nodes[i];
        }
        RedissonClient redisson = null;
        Config config = new Config();
        config.useClusterServers() //这是用的集群server
        .setScanInterval(2000) //设置集群状态扫描时间 
        .addNodeAddress(nodes)
        .setPassword(password);
        redisson = Redisson.create(config);
        return redisson;
    }
}

(4)分布式锁使用

@Service
public class DistributeLockServiceImpl{
    @Autowired
    private RedissonClient redissonClient;

private final static String LOCK_KEYP_PREFIX="redisson:lock:test:";
    @overiide
    public void redissonTest(Long userId) {
       //获取key
        String redisLockKey=LOCK_KEYP_PREFIX.concat(String.ValueOf(userId));
       //获取redisson锁
        RLock rlock = redissonClient.getLock(redisLockKey);
  //设置锁超时时间,防止异常造成死锁
        rlock.lock(20, TimeUnit.SECONDS);
        try{
           //todo 执行业务逻辑
        } catch(Exception e){
            //异常处理
        }finally{
            //释放锁
            rlock.unlock();
        }  
    }
}
5、redLock算法实现的分布式锁总结

三、zookeeper实现的分布式锁

四、基于etcd实现的分布式锁

  • etcd和zk一样作为分布式一致性的解决方案,具有zk的所有功能,就是说zk能做的etcd也能做。etcd是基于raft协议的一致性框架,相对paxos算法,更加容易理解,而且etcd提供了http+json的调用格式,更加符合restful风格,还有一点重点的,基于etcd相对zk,性能会更加好。
  • 由于对于etcd不是很熟悉,后续学习了在动手写个demo。

五、几种主流的分布锁对比

  • 在生产上面对高并发基本不会使用数据作为分布式锁的解决方法,绝大数情况是用redis作为分布式锁的解决方案,但是对于一致性要求特别高的比如交易等系统内就要考虑使用zk或者etcd实现的分布式锁。具体三者的比较如下:


    分布式锁实现的主流方案对比

参考:
http://www.importnew.com/27477.html
https://juejin.im/post/59f592c65188255f5c5142d2
https://zhuanlan.zhihu.com/p/42056183
https://juejin.im/post/5bbb0d8df265da0abd3533a5
http://www.spring4all.com/question/158

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • 最近看了极客时间左耳听风的专栏,对于分布式系统的设计有了更深的认识,准备结合陈皓的总结加上自己看过的资料对于分布式...
    仰泳的双鱼阅读 3,670评论 0 23
  • 在很多环境下,多个不同的进程需要以排他的形式使用共享资源,这是使用分布式锁机制是一种传统但有效的方案。 有很多的库...
    BigFish__阅读 1,505评论 0 0
  • 窗外 一缕一缕的春风 一树一树的花开 此时是人间最美的四月天 我的心里 一丝一丝的寒凉 一点一点的死去 此时是世间...
    遇见子鱼阅读 404评论 4 14
  • 一. 小时候,老家院里有颗核桃树,也不知道这树有多大了,只有粗糙而硕大的树干能看出岁月沧桑,春天来了,外婆就催着外...
    xr安稳阅读 417评论 0 1