为了账号安全,请及时绑定邮箱和手机立即绑定

分布式锁

标签:
Java

基于数据库排他锁

在查询语句后面增加for update(行级锁)

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

这里还可能存在另外一个问题,虽然我们对method_name 使用了唯一索引,并且显示使用for update来使用行级锁。但是,MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。如果发生这种情况就悲剧了。。。

基于Zookeeper实现分布式锁

Zookeeper简介

中间件,提供协调服务

作用于分布式系统,发挥其优势,可以为大数据服务

https://img1.sycdn.imooc.com//5bcfd1a200011d2405580304.jpg

https://img1.sycdn.imooc.com//5bcfd1b40001fca305580310.jpg

https://img1.sycdn.imooc.com//5bcfd1bd00014a8905580272.jpg

https://img1.sycdn.imooc.com//5bcfd2cd00010dba05580298.jpg

https://img1.sycdn.imooc.com//5bcfd1cd000106b705580245.jpg

zookeeper 的作用体现

  1. 节点选取,主节点挂掉,从节点就可以接受工作,并且保证这个节点 是唯一的,这也是所谓的首脑模式从而保证我们的集群是高可用的。

  2. 统一配置文件管理,既只需要部署一台服务器,则可以把相同配置文件同步到更新到其他所有服务器。

  3. 发布于订阅,类似消息队列mq    ,dubbo发布者把    数据存放在znode上,订阅者会读取数据。

  4. 提供分布式锁,分布式环境中不通进程之间争夺资源,类似于多线程中的锁

  5. 集群管理,集群中保证数据的强一致性。

    zookeeper 常用客户端

  • zk原生

  • zkclient

  • apache curator

    https://img1.sycdn.imooc.com//5bcfd5320001d36d05580386.jpghttps://img1.sycdn.imooc.com//5bcfd54a00015b1807390507.jpg

    https://img1.sycdn.imooc.com//5bcfd55b0001e3ad08860542.jpg

curator与spring的整合 实现分布式锁

首先配置文件

zk与spring容器结合, 启动项目加载的时建立与zk的连接

首先我们在创建CuratorFrameworkFactory的时候我们必须要设置一个重试的机制。

https://img1.sycdn.imooc.com//5bcfd6350001b75805580173.jpg

https://img1.sycdn.imooc.com//5bcfd63e00016a6e05580174.jpg

下面配置的Curator的client,  有四个构造参数

1,zk服务地址,集群用","分隔

2. session timeout 会话超时时间

3. connectionTimeoutMs 创建连接超时时间

4. 重试策略

取分布式锁的流程

任何一个进程访问资源之前,需要先判断,分布式锁是否被占用,如果锁被占用说明资源正在被使用,所以进程等待释放锁,如果没有被占用就可以直接创建zk节点(就是获得一把锁),一,所有系统的每个进程创建这样一把锁,取名相同。二,创建这个节点,不可能是一个持久性的锁(释放就是删除节点)。所以这个节点就是临时性的,创建节点之后,当前线程就获得了锁,此时就可以创建订单,减库存等操作,操作完毕就需要释放锁(就是手动删除)。

https://img1.sycdn.imooc.com//5bcfd6770001fe5b05580319.jpg

开发分布式锁

首先在zk的xml 添加分布式锁的工具类,并且把client传进去

https://img1.sycdn.imooc.com//5bcfd68e0001ffb205580064.jpg

这个类就是用来实现分布式锁的工具类。

如果等待释放锁,需要挂起请求,所以需要工具类CountDownLatch

https://img1.sycdn.imooc.com//5bcfd6b00001346505580350.jpg

https://img1.sycdn.imooc.com//5bcfd7ce0001a1ca05580353.jpg

https://img1.sycdn.imooc.com//5bcfd7d60001672605580066.jpg

这是总结点,在设置分布式锁的名字(订单的 都用一个)

https://img1.sycdn.imooc.com//5bcfd7e90001e76605580079.jpg

接下来是构造函数,是将client因引入

https://img1.sycdn.imooc.com//5bcfd7fb0001109305280073.jpg

init 是初始化锁

首先使用命名空间

https://img1.sycdn.imooc.com//5bcfd81a0001facb05580207.jpg

创建总结结点,并添加监听(需要监听到父节点)

https://img1.sycdn.imooc.com//5bcfd865000135cb05580191.jpg

https://img1.sycdn.imooc.com//5bcfd899000184b005580275.jpg

之后是获取分布式锁的方法:

首先用一个死循环来卡主工程

之后创建业务锁,创建的是临时节点(当回话结束时,节点自动消失 释放锁)

如果成功创建业务锁,就可以直接跳出死循环。

如果当前业务锁已经有了,会抛出路径存在异常,说明锁被占用。那么就进入异常处理,进行等待 使用zkLocklatch.await() 阻塞我们的线程,使得当前线程挂起。

https://img1.sycdn.imooc.com//5bcfd9030001517c05580321.jpg

之后是添加的watcher监听方法是在容器启动(初始化)添加的监听事件

我们要针对父节点的一个子节点进行监听所以用PathChildrenCache进行创建

对PathChildrenCache进行启动后添加监听器

之后是里面对监听事件的处理,当接收到一个watcher事件后对事件类型进行处理判断

就是event.getType()  是不是CHILD_REMOVED

这说明总结点下的一把锁被删除了 ,但是还不知道那一把锁被删除了(我们可能有订单锁,清算锁,还有一些账务锁),所以说我们要获取event节点出发的路径

String path = event.getData().getPath();  获取后对比名称和业务锁是否相同,如果想同,我们就zkLocklatch.countDown();释放计数器,获取锁的死循环就可以集训进行。

https://img1.sycdn.imooc.com//5bcfd942000152f505580272.jpg

在看释放锁的方法,就是删除节点

就是先判断路径是否存在,存在就删除,不存在就不做操作,删除成功就是return true  删除失败就是return fasle

https://img1.sycdn.imooc.com//5bcfda5e0001283f05580221.jpg


下面是在service层添加分布式锁

在业务开始前枷锁

// 执行订单流程之前使得当前业务获得分布式锁

                   distributedLock.getLock();

 

在执行创建订单流程

最后释放锁

// 释放锁,让下一个请求获得锁

distributedLock.releaseLock();

(出现异常也要释放锁,库存不够也需要释放锁)

可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

https://img1.sycdn.imooc.com//5bcfdac40001c70106540310.jpg

Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。

使用ZK实现的分布式锁好像完全符合了本文开头我们对一个分布式锁的所有期望。但是,其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。

其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端可ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)


基于缓存的分布式锁

redis 实现分布式锁

setnx-------setNX是Redis提供的一个原子操作,如果指定key存在,那么setNX失败,如果不存在会进行Set操作并返回成功。我们可以利用这个来实现一个分布式的锁,主要思路就是,set成功表示获取锁,set失败表示获取失败,失败后需要重试。

客户端 在尝试获取失败之后,检查锁的超时时间,并为未设置超时时间的锁设置超时时间

/**

     * 加锁

     * @param locaName  锁的key

     * @param acquireTimeout  获取超时时间

     * @param timeout   锁的超时时间

     * @return 锁标识

     */

    public String lockWithTimeout(String locaName,

                                  long acquireTimeout, long timeout) {

        Jedis conn = null;

        String retIdentifier = null;

        try {

            // 获取连接

            conn = jedisPool.getResource();

            // 随机生成一个value

            String identifier = UUID.randomUUID().toString();

            // 锁名,即key值

            String lockKey = "lock:" + locaName;

            // 超时时间,上锁后超过此时间则自动释放锁

            int lockExpire = (int)(timeout / 1000);

 

            // 获取锁的超时时间,超过这个时间则放弃获取锁

            long end = System.currentTimeMillis() + acquireTimeout;

            while (System.currentTimeMillis() < end) {

                if (conn.setnx(lockKey, identifier) == 1) {

                    conn.expire(lockKey, lockExpire);

                    // 返回value值,用于释放锁时间确认

                    retIdentifier = identifier;

                    return retIdentifier;

                }

                // 返回-1代表key没有设置超时时间,为key设置一个超时时间

                if (conn.ttl(lockKey) == -1) {

                    conn.expire(lockKey, lockExpire);

                }

 

                try {

                    Thread.sleep(10);

                } catch (InterruptedException e) {

                    Thread.currentThread().interrupt();

                }

            }

        } catch (JedisException e) {

            e.printStackTrace();

        } finally {

            if (conn != null) {

                conn.close();

            }

        }

        return retIdentifier;

    }

https://img1.sycdn.imooc.com//5bcfdbe500019e1405580147.jpg

或者用redis+lua脚本 来保证设置锁的原子性


优化在jedis新版本中2.7.0以上

ublic class RedisTool {
 
     private static final String = "OK";
     private static final String = "NX";
     private static final String = "PX";
 
     public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
 
         String result = jedis.set(lockKey, requestId, , , expireTime);
 
         if (.equals(result)) {
             return true;
         }
         return false;
 
     }
 
 }

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

·          第一个为key,我们使用key来当锁,因为key是唯一的。

·          第二个为value,我们传的是requestId,原因就是我们在上面讲到可靠性时,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。可以使UUID.randomUUID().toString()方法生成。

·          第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

·          第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

·          第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

 

总结:

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

Redisson分布式锁

引用的redisson最近发布的版本3.2.3,不同的版本可能实现锁的机制并不相同,早期版本好像是采用简单的setnx,getset等常规命令来配置完成,而后期由于redis支持了脚本Lua变更了实现原理。

public class RedissonLockUtil {

 

    private static Redisson redisson = RedissonManager.getRedisson();

 

    private static final String LOCK_FLAG = "redissonlock_";

 

    /**

     * 根据name对进行上锁操作,redissonLock 阻塞事的,采用的机制发布/订阅

     * @param key

     */

    public static void lock(String key){

        String lockKey = LOCK_FLAG + key;

        RLock lock = redisson.getLock(lockKey);

        //lock提供带timeout参数,timeout结束强制解锁,防止死锁 :1分钟

        lock.lock(1, TimeUnit.MINUTES);

    }

 

    /**

     * 根据name对进行解锁操作

     * @param key

     */

    public static void unlock(String key){

        String lockKey = LOCK_FLAG + key;

        RLock lock = redisson.getLock(lockKey);

        //如果锁被当前线程持有,则释放

        if(lock.isHeldByCurrentThread()){

            lock.unlock();

        }

    }

 

 

}

复制代码

注意:上面的unlock()中不加isHeldByCurrentThread()条件的话,在执行的task的时间超过timeout时,此时如果unlock,其实redisson已经主动unlock了,就会出现IllegalMonitorStateException 异常

调用:

复制代码

try

{

    //获取锁,这里的key可以上面场景中的业务id

    RedissonLockUtil.lock(key);

    //具体要执行的代码....

} catch (Exception e)

{

    e.printStackTrace();

}finally {

    //释放锁

    RedissonLockUtil.unlock(key);

}


点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消