(9)redis实现分布式锁

一、 分布式锁的实现

关于锁,其实我们或多或少都有接触过一些,比如synchronized、 Lock这些,这类锁的目的很简单,在多线程环境下,对共享资源的访问造成的线程安全问题,通过锁的机制来实现资源访问互斥。那么什么是分布式锁呢?或者为什么我们需要通过Redis来构建分布式锁,其实最根本原因就是Score(范围),因为在分布式架构中,所有的应用都是进程隔离的,在多进程访问共享资源的时候我们需要满足互斥性,就需要设定一个所有进程都能看得到的范围,而这个范围就是Redis本身。所以我们才需要把锁构建到Redis中。

Redis里面提供了一些比较具有能够实现锁特性的命令,比如SETEX(在键不存在的情况下为键设置值),那么我们可以基于这个命令来去实现一些简单的锁的操作

二、 分布式锁的实战

分布式锁的实现会采用两种方式来实现,一种自己手动来实现,一种是基于Redission的客户端来实现

  • 基于jedis手动实现分布式锁

代码如下:

/**
 * @Project: redis
 * @description:   redis的分布式锁的手动实现
 * @author: sunkang
 * @create: 2019-01-12 16:54
 * @ModificationHistory who      when       What
 **/
public class DistributedLock {
        /**
     * 获取锁
     * @param lockName  锁的名称
     * @param accquireTimeout   获取锁的超时时间
     * @param lockTimeout  锁本身过期时间
     * @return
     */
    public String acquireLock(String lockName ,long accquireTimeout,long  lockTimeout){
        String identify  = UUID.randomUUID().toString();
        String lockKey = "lock:"+lockName;
        //获取redis客户端
        long endTime = System.currentTimeMillis()+ accquireTimeout;
        Jedis redis = JedisPoolUtils.getRedis();
        int expireTime = (int) (lockTimeout/1000);

        try{
            while (System.currentTimeMillis() 0){
            return true;
        }
        return  false;
    }
}
  • 测试代码
/**
 * @Project: redis
 * @description: 分布式测试 
 * 开启10个线程,模拟十个客户端请求锁
 * @author: sunkang
 * @create: 2019-01-12 17:45
 * @ModificationHistory who      when       What
 **/
public class LockTest extends Thread{
    @Override
    public void run() {
        while(true){
            DistributedLock distributedLock=new DistributedLock();
            String rs=distributedLock.acquireLock("updateOrder",
                    2000,5000);
            if(rs!=null){
                System.out.println(Thread.currentThread().getName()+"-> 成功获得锁:"+rs);
                try {
                    Thread.sleep(1000);
                    //释放锁
                    distributedLock.releaseLockWithLua("updateOrder",rs);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
    }
    public static void main(String[] args) {
        LockTest lockTest=new LockTest();
        for(int i=0;i<10;i++){
            new Thread(lockTest,"tName:"+i).start();
        }
    }
}
  • 输出结果 : 基本是锁释放了之后,其他的线程才得到了锁
tName:7-> 成功获得锁:38c683cb-1797-4abb-aa57-200e770f7445
updateOrder开始释放锁:38c683cb-1797-4abb-aa57-200e770f7445
tName:6-> 成功获得锁:873dfe4e-00ee-4d2b-9bc5-bb16e83f8879
updateOrder开始释放锁:873dfe4e-00ee-4d2b-9bc5-bb16e83f8879
tName:8-> 成功获得锁:e6c48dea-87d9-4fd5-ab04-09ed2f725307
updateOrder开始释放锁:e6c48dea-87d9-4fd5-ab04-09ed2f725307
tName:0-> 成功获得锁:e0d764a5-30b5-4b6b-8490-73ffd78727e0
...

总结:
acquireLock的方法主要是竞争锁的时间内,不断轮训去查看lockKey的是否存在,不存在则设置锁,返回锁,存在会一直尝试获取锁,直到获取锁的超时时间结束,然后释放redis连接

在释放锁的方式,有两种方式,一种是给锁本身设置超时时间,时间到了,锁本身释放了,一种是自动方式来删除锁,删除锁为了锁在释放过程中的原子性,一个使用了lua脚本,一个使用redis的watch机制和事务删除的方式

  • 基于redisson实现分布式锁

maven依赖:

   
      org.redisson
      redisson
      2.15.0
    

redisson的分布式代码如下

/**
 * @Project: redis
 * @description:  基于redission的api实现分布式锁
 * @author: sunkang
 * @create: 2019-01-12 18:08
 * @ModificationHistory who      when       What
 **/
public class RedissionLock {
    public static void main(String[] args) {
        Config config = new Config();
        //使用单个节点连接,并且设置连接地址
        config.useSingleServer().setAddress("redis://192.168.44.129:6379");
        RedissonClient redissonClient = Redisson.create(config);
        RLock lock =  redissonClient.getLock("updateOrder");
        try {
            //尝试获取锁100秒,锁释放时间为10秒
            lock.tryLock(100,10,TimeUnit.SECONDS);
            System.out.println("得到锁");
            Thread.sleep(100);
            //释放锁
            lock.unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

基于redission 的实现分布式锁的方式,获取锁的关键代码在org.redisson.RedissonLock#tryLockInnerAsync这个方法上:

     RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  "if (redis.call('exists', KEYS[1]) == 0) then " +
                      "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  "return redis.call('pttl', KEYS[1]);",
                    Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }
 
 

通过lua脚本来实现加锁的操作

  1. 判断lock键是否存在,不存在直接调用hset存储当前线程信息并且设置过期时间,返回nil,告诉客户端直接获取到锁。
  2. 判断lock键是否存在,存在则将重入次数加1,并重新设置过期时间,返回nil,告诉客户端直接获取到锁。
  3. 被其它线程已经锁定,返回锁有效期的剩余时间,告诉客户端需要等待。

释放锁的关键代码在方法: org.redisson.RedissonLock#unlockInnerAsync

    protected RFuture unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; "+
                "end; " +
                "return nil;",
                Arrays.asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

    }
 
 
  1. 如果lock键不存在,发消息说锁已经可用,发送一个消息
  2. 如果锁不是被当前线程锁定,则返回nil
  3. 由于支持可重入,在解锁时将重入次数需要减1
  4. 如果计算后的重入次数>0,则重新设置过期时间
  5. 如果计算后的重入次数<=0,则发消息说锁已经可用

你可能感兴趣的:((9)redis实现分布式锁)