集群部署下的分布式锁:[【SpringBoot电商项目实战-Curator分布式锁实现方案】

一、引言:

网上的分布式锁文章千篇一律,而此文从实际高并发场景深入浅出,缘由剖析,不管是应对面试官的层层"逼问",还是实际项目,相信都能游刃有余,你学会了吗?还不会建议请先去看下哦[美女开头,让你一饱眼福....]。

                       

分布式锁用途:在分布式环境下协同共享资源的使用。

2、分布式锁思路分析

锁特点:

  • 排他性:同一时间,只有一个线程能获得;

  • 阻塞性:其它未抢到的线程阻塞等待,直到锁被释放,再继续抢;

  • 可重入性:线程获得锁后,后续是否可重复获取该锁(避免死锁)。

当然,还要考虑性能开销等问题。

3、常规的分布式锁解决方案有哪几种:

  • 文件系统:同一个目录下,不能存在同名文件

  • 数据库锁:主键 、 唯一约束  、for  update

  • 基于Redis的分布式锁:setnx、set、Redisson

  • 基于ZooKeeper的分布式锁:类似文件系统

对比分析:

  1. 使用数据库锁会有单机性能、单机故障等问题,锁没有失效时间,容易出现死锁,当然可以部署群集,也会出现各种各样的问题,性能开销高,这里不详细介绍。

  2. Redis缓存实现分布式锁,相对复杂,因为没有类似zk的watch监听通知机制,需要自己另外实现;而且Redis可能会出现死锁(或短时间内死锁),比如,获取到锁的线程挂了,必须等到该节点过期时间到了,才能删除。

  3. 而Zookeeper分布式锁可靠性比Redis好,实现相对简单,但由于需要创建节点、删除节点等,所以效率相比Redis要低。

那我们在实际项目中如何选择呢?

原则上如果并发量不是特别大,追求可靠性,那么首选zookeeper。而Redis实现的分布式锁响应更快,对并发的支持性能更好,如果为了效率,首选redis实现。

本文将讲解使用现成的框架Curator实现的分布式锁方案。


二、Curator简介

Zookeeper已经流行了这么多年,实际上基于zk的分布式锁目前已经有现成的实现框架,Curator就是Netflix开源的一套ZooKeeper客户端框架,它提供了zk场景的绝大部分实现,使用Curator就不必关心其内部算法,Curator提供了来实现分布式锁,用方法获取锁,以及用方法释放锁,同其他锁一样,需要放在finally代码块中,确保锁能正确释放。

ZooKeeper可以被用来实现分布式锁,具体是使用“临时顺序节点”实现(假如使用“临时节点”将会出现惊群效应,上篇有介绍)。

Curator提供了四种分布式锁,分别是:

  • InterProcessMutex:分布式可重入排它锁

  • InterProcessSemaphoreMutex:分布式排它锁

  • InterProcessReadWriteLock:分布式读写锁

  • InterProcessMultiLock:将多个锁作为单个实体管理的容

获取锁

我们可以在Zookeeper下创建一个指定的父节点作为分布式锁,每个zk客户端尝试连接zk服务获取分布式锁时,都将在此父节点下创建一个临时顺序节点,分两种情况:

  • 如果创建的临时顺序节点是父节点下的首个子节点(最小),则获取锁成功,执行相应业务逻辑,然后释放锁。

  • 如果创建的临时顺序节点并不是该父节点下最小的子节点,则去对比比自己小的节点注册watcher监听,只监听比自己小的上一个节点,进入阻塞等待。当前一个节点被删除时会触发Watch事件,进而唤醒当前阻塞线程。

如果前一个节点对应的客户端崩溃了,则节点对应的Watch事件也会触发,也会唤醒后一个节点对应的客户端线程,此时仍需要判断当前节点是第一个节点之后才能获取锁,否则继续进入阻塞并Watch前一个节点。

重入性

只考虑同一个客户端、同一个线程获取同一个分布式锁的可重入性,第一次获取锁成功之后,在JVM内存中的一个ConcurrentMap中存储当前线程对应的锁路径及重入次数,后面同一个线程再次获取锁时,先检查该Map中当前锁是否已被当前线程占用即可,如果已占用,则只需要递增重入次数即可。

因为重入性只考虑同一个客户端、同一个JVM、同一个线程,所以可以不用考虑判断ConcurrentMap中的Owner线程的并发问题。

释放锁

释放锁时,对应可重入分布式锁,首先重入次数减一,然后判断重入次数是否已经为0:

  • 如果重入次数为0,则删除当前客户端线程对应的临时顺序节点,删除操作会触发次节点的Watch事件,如果有别的客户端线程正在阻塞等待,则会通过Watch机制唤醒。

  • 如果重入次数非0,则说明还未完全释放锁,直接返回即可。

1、pom引入如下curator依赖

<!-- curator:zk客户端 -->        <dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.3.0</version>
</dependency><dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.3.0</version>
</dependency>

2、yml配置文件

curator:  connectionTimeoutMs: 8000  # 连接超时时间  elapsedTimeMs: 8000   #重试间隔时间  retryCount: 4  #重试次数  sessionTimeoutMs: 30000  # session超时时间  connectString: 192.168.227.132:2181   # zookeeper 地址

3、curator配置类,读取配置属性,并注册bean到spring ioc容器

CuratorFrameworkFactory类提供了两个方法,一个工厂方法newClient,一个构建方法build。使用工厂方法newClient可以创建一个默认的实例, 而build构建方法可以对实例进行定制。当CuratorFramework实例构建完成, 紧接着调用start()方法。

 

@Configuration@ConfigurationProperties(prefix = "curator")@Datapublic class CuratorConfig {    private int retryCount;    private int elapsedTimeMs;    private String connectString;    private int sessionTimeoutMs;    private int connectionTimeoutMs;        @Bean(initMethod = "start")    public CuratorFramework curatorFramework() {        RetryPolicy retryPolicy = new ExponentialBackoffRetry(elapsedTimeMs, retryCount);        CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()                .connectString(connectString)                .sessionTimeoutMs(sessionTimeoutMs)                .retryPolicy(retryPolicy)                .build();        return curatorFramework;    }}

4、新建一个订单服务实现类

@Slf4j@Servicepublic class CuratorDisLockOrderServiceImpl implements OrderService {    private static OrderCodeGenerator codeGenerator = new OrderCodeGenerator();    private static String LOCK_PATH = "/distribute-lock";    @Autowired    private CuratorFramework curatorFramework;
    @Override    public String createOrder() {        String orderCode = "";        InterProcessMutex lock = new InterProcessMutex(curatorFramework, LOCK_PATH);        try {            lock.acquire();            //生成订单编号            orderCode = codeGenerator.getOrderCode();            log.info(Thread.currentThread().getName()+"-->获取锁成功-->生成订单编号:{}",orderCode);        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                lock.release();                log.info(Thread.currentThread().getName() + "-->释放锁成功。");            } catch (Exception e) {                e.printStackTrace();            }        }        // TODO 具体写自己的生成订单业务        return orderCode;    }}

注:InterProcessMutex通过在zookeeper的某路径节点下创建临时顺序节点来实现分布式锁,即每个线程(跨进程的线程)获取同一把锁前,都需要在同样的路径下创建一个节点,节点名字由uuid + 递增序列组成。而通过对比自身的序列数是否在所有子节点的第一位,来判断是否成功获取到了锁。当获取锁失败时,它会添加watcher来监听前一个节点的变动情况,然后进行等待状态。直到watcher的事件生效将自己唤醒,或者超时时间异常返回。

5、新建一个controller,提供一个下单http接口方法

@RestController@Slf4jpublic class OrderController {    @Autowired    private  OrderService orderService;    /**     * 模拟高并发场景,多线程,并发下单     * @return     */    @RequestMapping("/order")    public String createOrdertTest(){        //并发线程数        int count = 20;        //循环屏障        CyclicBarrier cb  = new CyclicBarrier(count);        //模拟高并发场景,多线程,创建订单        for(int i=0; i<count; i++){            new Thread(new Runnable() {                @Override                public void run() {                    log.info(Thread.currentThread().getName()+"--我已经准备好了");                    try {                        //等待所有线程启动准备好,才一起往下执行                        cb.await();                    } catch (InterruptedException | BrokenBarrierException e) {                        e.printStackTrace();                    }                    //创建订单                    orderService.createOrder();                }            }).start();        }        return "ok";    }}

6、运行应用,访问 http://localhost:8888/order ,观察控制台,订单编号没有重复。

-->是不是感觉比Zookeeper原生实现的分布式锁简单0.0

                      

                           

参考资料:

https://blog.csdn.net/xuefeng0707/article/details/80588855

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值