分布式锁实现方式

前言

实现分布式锁的三种方式,1:数据库乐观锁,2:redis锁,3:zk锁,本文将说下这三种的分布式锁,因为我平时用的最多的就是redis锁,所以先以它开写。

高可用

要保证分布式锁的高可用性,需要满足以下几个条件:
1:唯一性(互斥性):同一时间,只有个客户端获得锁
2:无死锁:要保证由于解锁失败导致其他客户端一直不能获得锁。
3:容错性:要保证大部分redis节点能用就可以加锁,解锁。
4:解铃还须系铃人:要保证解锁的客户端一定是加锁的客户端。

Redis 分布式锁

问题1: 死锁

死锁1:

 Long lock=jedis.setnx(key,value);
 if(lock==1){
    doSomeing();    
    jedis.del(key);
 }

该demo 是存在发送比较大概率的死锁问题,如doSomeing() 发生异常就会导致 jedis.del(key)执行不了,所以死锁发生。那这时我们可能会想到给doSomeing()一个 try cath finally。

死锁2:

  Long lock=jedis.setnx(key,value);
  if(lock==1){
    try{
        doSomeing();
    }catch(Exception e){
        LOGGER.error(".....");
    }finally{
        jedis.del(key);
    }
 }

这个demo 看上去挺nice的,finally 一定会执行(如果排除,kill 9情况,和doSomeing中有System.exit(0) ) ,但是该demo 还是存在一定概率发生死锁问题。如果jedis.del(key) 出现连接数据库异常,就会发生死锁。那这时我们可能想到,redis中还可以设置过期时间,那我们只要给锁加上一个过期时间是不是就ok了呢?

问题2:错误解锁

错误解锁1:

    Long lock=jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    if(lock==1){
        try{
            doSomeing();
        }catch(Exception e){
            LOGGER.error("...");
        }finally{
            jedis.del(key);
        }           
    }

该demo 我们已经加上了过期时间,当然过期时间的设置是有要求的–要远大于业务执行时间,这里就对业务执行时间进行预估,比如业务正常最大执行时间为5s,那么我们设置成1m。这样加上过期时间,我们就可以解决之前jedis.del(key) 连接数据库失败的问题,如果失败,到了设置的过期时间还是可以释放的,解决了死锁问题。
但是这样就没有问题了吗?
举个例子:
假设我们在doSomeing() 发生了阻塞执行了2m,在这过程锁已经释放,其他客户端已经获得该锁,然后doSomeing() 执行完,执行了jedis.del(key) 这时就会出现问题,既违反了解铃还须系铃人。

错误解锁2:

    UUID uuid=UUID.randomUUID();
    Long lock=jedis.set(key, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    if(lock==1){
        try{
            doSomeing();
        }catch(Exception e){
            LOGGER.error("...");
        }finally{
            if(uuid.equals(jedis.get(key))){
                jedis.del(key);
            }
        }           
    }

该demo 我们给value 设置了一个uuid,保证锁能被标识为那个客户端。一个客户端只能删除自己的加的锁。这样我们再看上面的问题,好像也没有太大问题,就算doSomeing 阻塞了2m 后,继续执行也不会删除锁,因为此时key对应的value已经改变。
但是 仔细想想是不是还有问题呢? 是的,由于判断和删除加在一起不是原子的,导致了如果在判断 uuid.equals(jedis.get(key)) 为true,但是 在执行jedis.del(key)前被其他改了,这时候还是会导致错误解锁。
所以我们只要保证判断和删除时原子的就完美解决了。

那么我们如果保证它是原子的呢? 如果仅仅靠jedis 是不行的,它没有提供类似的操作。我们可以利用lua脚本来保证,redis中执行lua 脚本是原子的。

正确版:

UUID uuid=UUID.randomUUID();
    Long lock=jedis.set(key, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    if(lock==1){
        try{
            doSomeing();
        }catch(Exception e){
            LOGGER.error("...");
        }finally{
             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(key), Collections.singletonList(uuid));
        }           
    }

这样已经解决了前面的所有问题,但是每次要写这样一坨东西,是不是挺烦,我们可以给他封装下,具体封装就不写了。


数据库乐观锁

乐观锁: 假设大部分情况不会发生冲突,所以只有在提交数据时检测是否冲突。

version

乐观锁一种实现就是通过version 来实现:

举个例子:我们要更新货品数量

    CREATE TABLE `goods` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(64) NOT NULL DEFAULT '',
      `remaining_number` int(11) NOT NULL,
      `version` int(11) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

思路:我们先查询 某货物信息,获得version,更新时带上version 查询。

    <update id="updateGoodCAS" parameterType="com.ztl.domain.Goods">
        <![CDATA[
          update goods
          set `name`=#{name}, 
          remaining_number=#{remainingNumber}, 
          version=version+1
          where id=#{id} and version=#{version}
        ]]>
    </update>

这样就不会存在同时更新某个货物库存问题,导致库存错误。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值