Redis实现分布式锁

一、分布式锁和普通锁的区别

1.普通的锁

        用来解决一个进程的多个线程同时操作同一资源的问题,如果是在分布式系统或者集群中,普通的锁是锁不住的。如图1:

图1

2.分布式锁

        用来解决多个进程同时操作同一资源的问题。他的原理就是所有的获取到同一把锁,无论有多少个服务,只有一个服务可以获取到锁,其他没有获得到的锁的服务则进行等待或者自旋等,直到锁释放,其他的服务才可以尝试获取到锁,如图2:

图2

二、Redis实现分布式锁

1.使用setnx加锁和释放锁

        Redis有一个setnx可以实现分布式锁,sexnx在指定的key不存在时,会为key设置指定的值,若给定的key已存在,setnx不做任何操作。

if(jedis.setnx(lock_stock,1) == 1){	//获取锁
    try {
        //需要执行的业务代码
    } finally {
        //释放锁
        jedis.del(lock_stock)			 
}

2.为锁设置过期时间

        使用setnx的命令进行加锁和释放锁,我们可以发现可能会存在死锁的现象,当我们在锁释放之前,服务宕机了,就会导致锁无法释放,那么其他的服务也就无法获取到锁,就会造成死锁,因此,我们在加锁时还应该为锁设置一个过期的时间。

if(jedis.setnx(lock_stock,1) == 1){	//获取锁
    //设置锁超时
    expire(lock_stock,2)		
    try {
        //需要执行的业务代码
    } finally {
        //释放锁
        jedis.del(lock_stock)
    }
}

        但是使用了expire设置了过期时间,仍会存在一种极值的现象,就是若在加锁和设置锁超时之间,服务发生了宕机,依然会发生死锁的现象,因此我们需要去保证加锁和释放的原子性。Redis中提供了一个set命令可以解决这个问题,相当于是加锁的设置过期时间的一个组合命令。

if(set(lock_stock,1,"NX","EX",2) == 1){    //获取锁并设置超时
    try {
        //需要执行的业务代码
    } finally {
        //释放锁
        del(lock_stock)                    
    }
}

        - EX : 将键的过期时间设置为秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
        - PX : 将键的过期时间设置为毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
        - NX : 只在键不存在时,才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。
        - XX : 只在键已经存在时, 才对键进行设置操作。

3.删除锁之前判断锁

        使用set命令进行加锁和释放锁,虽然保证了原子性,但是又出现了新的问题,如果我们需要执行的业务代码时间超过了锁设置的过期时间,那么在锁达到过期时间时会将这个锁释放,而在执行完业务后又进行了一次锁的删除,就会导致第二次删除的锁不是执行该业务时加的锁,造成锁的一个误删除现象。为解决这一问题,我们需要在删除锁的时候进行锁的判断,判断当前删除的锁是不是自己的锁。

String uuid = UUID.randomUUID().toString();
if(jedis.set(lock_stock,uuid,"NX","EX",5) == 1){    //获取锁并设置超时
    try {
        //执行自己的业务代码
    } finally {
        //获取锁的值
        String lockValue = jedis.get(lock_stock);    
        //判断是不是自己的锁
        if(lockValue.equals(uuid)){            
            //释放锁
            jedis.del(lock_stock)                   
        }
    }
}

        除了要判断锁,我们还应该去保证判断锁和删除锁的一个原子性,否则依然会有极值的问题,导致锁的误删除,因此这里使用Redis+Lua脚本来解决一致性的问题。

String script = "if redis.call('get', KEYS[1]) == ARGV[1] 
	then return redis.call('del', KEYS[1]) else return 0 end";

        上述的这段Lua脚本,可以保证多个命令的原子性,其中

  •  redis.call(‘get’, KEYS[1]) :是调用redis的get命令,key可以通过参数传入
  •  == ARGV[1] :意思是是否和 某个值相等,这里的值也可以参数传入
  •  then return redis.call(‘del’, KEYS[1]) :如果相等就执行 redis.call('del', KEYS[1]) 删除操作
  •  else return 0 end :否则就返回

        修改上述的代码为:

String uuid = UUID.randomUUID().toString();
if(jedis.set(lock_stock,uuid,"NX","EX",2) == 1){	//获取锁并设置超时
    try {
        //需要执行的业务代码
    } finally {
        //lua脚本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        //执行脚本
        jedis.eval(script, Collections.singletonList("lock_stock"),Collections.singletonList(uuid));
    }
}

        到此一个基本的redis实现分布式锁就完成了,但是考虑到实际的业务,未获得锁的线程可以做重入,等待一会儿,再次尝试获取锁。

public void method(){
    String uuid = UUID.randomUUID().toString();
    if(jedis.set(lock_stock,uuid,"NX","EX",5) == 1){    //获取锁并设置超时
        try {
            //需要执行的业务代码
        } finally {
            //lua脚本
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            //执行脚本
            jedis.eval(script, Collections.singletonList("lock_stock"),Collections.singletonList(uuid));
        }
    }else{
        //休眠之后重入方法,尝试获取锁,休眠的时间可根据自己的时间调整
        Thread.sleep(100);
        //自旋,重新进入方法
        method();    
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值