Bootstrap

redis - 实现分布式锁

redis - 实现分布式锁

常用的实现分布式锁的技术:redis、zookeeper、数据库,各种锁的比较如下:

在这里插入图片描述

1,锁的基本概念

1.1 超卖案例:

在这里插入图片描述

1.2 超卖的根源:

在这里插入图片描述

1.3线程安全:

什么是线程安全:
在这里插入图片描述
如何实现线程安全:
在这里插入图片描述
加锁:
如果在方法上增加同步关键字,增加了锁,代码就会变成串行执行,但是代码效率变低了。
在这里插入图片描述

1.4 锁的性能优化:

在这里插入图片描述
1,把同步方法改成同步代码块
2,
3,读锁,写锁分离

1.4 锁的种类:(JVM锁,进程锁)

在这里插入图片描述
独享锁也叫排他锁

2,分布式锁

2.1 分布式环境:

在这里插入图片描述

分布式锁的注意事项:
**加粗样式

redis 分布式锁流程图:
在这里插入图片描述
步骤:
1,竞争锁,判断锁是否存在
如果存在:等待 或者 退出
如果不存在:创建锁,设置锁的有效期,获取锁的线程分配唯一标识

加锁分为三步:
在这里插入图片描述

1,判断锁是否存在
2,如果不存在,加锁
3,设置有效时间

按照以上步骤代码如下:

if(setnx(key,1== 1{   //此处挂掉了.....
    expire(key,30try {
        do something ......
    }catch(){
  } finally {
       del(key)
    }
}

setnx和expire的非原子性
设想一个极端场景,当某线程执行setnx,成功得到了锁:
setnx刚执行成功,还未来得及执行expire指令,节点1 Duang的一声挂掉了。
这样一来,这把锁就没有设置过期时间,变得“长生不老”,别的线程再也无法获得锁了。

怎么解决呢?setnx指令本身是不支持传入超时时间的,Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令。

setnx() 命令只能保证前两个步骤是原子操作。所以在加锁时还是使用set 命令
源码如图所示:set命令有一个nxxx的参数,可以实现setnx() 的功能,又能保证加锁的三步操作都是原子操作。
在这里插入图片描述

解锁分为两步:

在这里插入图片描述

解锁注意事项:超时后使用del 导致误删其他线程的锁

又是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是30秒。
如果某些原因导致线程B执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。

随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。

怎么避免这种情况呢?可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。

至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。

加锁:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)

doSomething.....
 
解锁:
if(threadId .equals(redisClient.get(key)){
    del(key)
}

但是,这样做又隐含了一个新的问题,if判断和释放锁是两个独立操作,不是原子性。

因为删除没有原子命令,所以需要借助luau脚本来实现原子删除:

String luaScript = ‘if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end’;

redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));

这样一来,验证和删除过程就是原子操作了。

3,使用redis实现分布式锁,具体代码:

3.1 引入 redis 依赖:

<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.9.0</version>
</dependency>

3.2 解锁的 lua 脚本(可以事先把luau脚本加载到内存中)

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

3.3 RedisClient:通过redis连接池,获取redis连接

package com.example.redis.lock;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @author h
 * @Description redis 客户端连接池
 * @createTime 2019年09月28日 15:29
 */
public class RedisClient {

    private static final JedisPool POOL;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(500);
        config.setMaxIdle(5);
        config.setMaxWaitMillis(1000 * 10);
        config.setTestOnBorrow(true);
        POOL = new JedisPool(config, "localhost");
    }

    public static Jedis getClient() {
        return POOL.getResource();
    }
}

3.4 RedisDistributeLock:实现redis分布式锁

package com.example.redis.yunxi_lock;

import redis.clients.jedis.Jedis;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author h
 * @Description 使用redis 实现分布式锁
 * @createTime 2019年09月28日 15:14
 */
public class RedisDistributeLock implements Lock {

    // 锁消息的上下文,保存当前锁的持有人ID
    private ThreadLocal<String> lockContext = new ThreadLocal<>();

    // 默认锁的超时时间
    private long time = 100L;

    private Thread exclusiveOwnerThread;

    @Override
    public void lock() {
        // 类似自旋锁的方式获取锁
        while (!tryLock()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        if (Thread.interrupted()) {
            throw new InterruptedException();
        }
        while (!tryLock()) {
            Thread.sleep(100);
        }
    }

    @Override
    public boolean tryLock() {
        return tryLock(time, TimeUnit.MILLISECONDS);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) {
        // 为每个锁的持有人分配唯一的id
        String id = UUID.randomUUID().toString();
        Thread thread = Thread.currentThread();
        Jedis jedis = null;

        try {
            jedis = RedisClient.getClient();
            // 加锁并设置有效期
            boolean getLockSuccess = "OK".equals(jedis.set("yunxi_lock", id, "NX", "PX", unit.toMillis(time)));
            if (getLockSuccess) {
                // 记录锁的持有人id
                lockContext.set(id);
                // 记录当前线程
                setExclusiveOwnerThread(thread);
                return true;
                // 当前线程已经获得了锁,可重入
            } else if (exclusiveOwnerThread == thread) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != jedis) {
                jedis.close();
            }
        }
        return false;
    }

    private final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    @Override
    public void unlock() {
        String script = null;
        Jedis jedis = RedisClient.getClient();
        try {
            jedis = RedisClient.getClient();
            // 读取lua脚本内容
            script = inputStreamToString(getClass().getResourceAsStream("/redis.script"));
            if (null == lockContext.get()) {
                return;
            }
            // 使用lua脚本,删除锁,通过使用lockContext.get()来确保,谁加的锁,只能有谁来删除
            jedis.eval(script, Arrays.asList("redis_lock"), Arrays.asList(lockContext.get()));
            lockContext.remove();
        }catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != jedis) {
                jedis.close();
            }
        }
    }

    private String inputStreamToString(InputStream in) throws IOException {
        StringBuffer out = new StringBuffer();
        byte[] b = new byte[4096];
        for (int n; (n = in.read(b)) != -1 ; ) {
            out.append(new String(b, 0, n));
        }
        return out.toString();
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

3.5 测试类

package com.example.redis.lock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author h
 * @Description TODO
 * @createTime 2019年09月28日 17:44
 */
public class RedisLockTest {

    private RedisDistributeLock lock = new RedisDistributeLock();
    // 初始库存数量
    private Integer stock = 8;

    /**
     * 买票减库存操作:未使用锁
     * @param num 购买票数
     */
    private void reduce(int num) {
        if (stock - num >= 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock = stock - num;
            System.out.println(Thread.currentThread().getName()
                    + "成功:卖出" + num + "张,库存剩余" + stock + "张票");
            // 后续增加积分的操作
        } else {
            System.out.println(Thread.currentThread().getName()
                    + "失败:库存不足" + num + "张,库存剩余" + stock + "张票");
        }
    }

    /**
     * 买票减库存操作:未使用锁
     * @param num 购买票数
     */
    private void reduceByRedisLock(int num){
        boolean flag = false;
        try {
            lock.lock();
            if (stock - num >= 0) {
                stock -= num;
                flag = true;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }

        if (flag){
            System.out.println(Thread.currentThread().getName()
                    + "成功:卖出" + num + "张,库存剩余" + stock + "张票");
            // 后续增加积分的操作
        } else {
            System.out.println(Thread.currentThread().getName()
                    + "失败:库存不足" + num + "张,库存剩余" + stock + "张票");
        }
    }

    public static void main(String[] args) throws InterruptedException {

        RedisLockTest redisLockTest = new RedisLockTest();

        // 未加锁
//        for (int i = 0; i < 10; i++) {
//            new Thread(() -> redisLockTest.reduce(1), "用户" + (i+1)).start();
//        }

        // 加锁
        for (int i = 0; i < 10; i++) {
            new Thread(() -> redisLockTest.reduceByRedisLock(1), "用户" + (i+1)).start();
        }
        Thread.sleep(1000L);
    }

}

4,使用redis实现分布式锁的注意事项

4.1 出现并发的可能性

还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。

怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。

当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。

当线程A执行完任务,会显式关掉守护线程。

另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。

5,基于数据库实现分布式锁

在这里插入图片描述

5,基于zookeeper实现分布式锁

准确性高,但性能不如 redis,用于与资金相关的对准确性高要求场景。
在这里插入图片描述
在这里插入图片描述

;