Redis入门(五)之分布式锁

1.写在前面

Redis上篇博客讲了下集群的搭建,我们只知道Redis的集群中有16384个插槽,当我们录入一个值的时候,先对这个键进行hash的算法然后,然后对这个hash出来的数,进行取余,然后就得到一个0~16384中间的某个值,然后根据这个值找到对应的机器,将值写入到对应的Redis服务上,我们都知道Redis的客户端提供了一个重定向的功能,但是我们在java中使用jedis如何操作呢?这就是我们今天的博客的一部分的内容,同时会带大家写一个Redis的分布式的锁。

2.Redis集群下,Jedis如何录入数据?

我们首先来看下Redis集群下录入数据的原理吧!具体的如下:

在这里插入图片描述

从上面的示意图,我们可以一个key需要通过Redis的计算,才能知道插入那个机器上去,而Jedis刚开始是不知道,那么该怎么办呢?而重定向的功能只有Redis的客户端可以的。于是我们想出了一个方案:就是根据这些槽位进行对应的连接池的映射,然后录入的数据的时候,我们先计算出来这个key在哪个槽位 中,然后从Map的映射中,取出这个连接池,这样就不会出错了,而Jedis就是这样做,具体的流程如下:

在这里插入图片描述

思路和我说的差不多,那么我们怎么证明我们的思路是对的,那么当然需要我们去写代码去证明去了,具体的代码如下:

import redis.clients.jedis.*;

import java.util.*;

public class TestRedisCluster {

    public static void main(String[] args) {
      	Set<HostAndPort> nodesList = new HashSet<>();
        nodesList.add(new HostAndPort("192.168.181.6", 7000));
        nodesList.add(new HostAndPort("192.168.181.6", 7001));
        nodesList.add(new HostAndPort("192.168.181.6", 7002));
        nodesList.add(new HostAndPort("192.168.181.6", 7003));
        nodesList.add(new HostAndPort("192.168.181.6", 7004));
        nodesList.add(new HostAndPort("192.168.181.6", 7005));

        // Jedis连接池配置
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 最大空闲连接数, 默认8个
        jedisPoolConfig.setMaxIdle(200);
        // 最大连接数, 默认8个
        jedisPoolConfig.setMaxTotal(1000);
        //最小空闲连接数, 默认0
        jedisPoolConfig.setMinIdle(100);
        // 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间,  默认-1
        jedisPoolConfig.setMaxWaitMillis(3000); // 设置2秒
        //对拿到的connection进行validateObject校验
        jedisPoolConfig.setTestOnBorrow(false);

        JedisCluster jedisCluster = new JedisCluster(nodesList, 2000, 2000, 5, "123456", jedisPoolConfig);
        Map<String, JedisPool> clusterNodes = jedisCluster.getClusterNodes();
        clusterNodes.entrySet().forEach(System.out::println);
    }
}

运行的结果如下:

在这里插入图片描述

可以为我们的每一个节点分配了一个JedisPool,当然还有一个map,存的是键值对是槽位和JedisPool,下面这部分的代码是从源码中拷贝出来,就不放上来了,大家可以看下运行结果,具体的如下:

在这里插入图片描述

从上面的运行的结果,我们可以得出如果槽位在一台Redis服务上,那么它的JedisPool的对象是一样的。那么我们来测试下我们写的代码吧,看看能不能添加东西到Redis的集群中,具体的代码如下:

import com.google.common.collect.Lists;
import com.ys.redis.MyRedisCluster;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.util.JedisClusterCRC16;

import java.util.*;

public class TestRedisCluster {

    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(TestRedisCluster.class);
        Set<HostAndPort> nodesList = new HashSet<>();
        nodesList.add(new HostAndPort("192.168.181.6", 7000));
        nodesList.add(new HostAndPort("192.168.181.6", 7001));
        nodesList.add(new HostAndPort("192.168.181.6", 7002));
        nodesList.add(new HostAndPort("192.168.181.6", 7003));
        nodesList.add(new HostAndPort("192.168.181.6", 7004));
        nodesList.add(new HostAndPort("192.168.181.6", 7005));


        // Jedis连接池配置
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 最大空闲连接数, 默认8个
        jedisPoolConfig.setMaxIdle(200);
        // 最大连接数, 默认8个
        jedisPoolConfig.setMaxTotal(1000);
        //最小空闲连接数, 默认0
        jedisPoolConfig.setMinIdle(100);
        // 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间,  默认-1
        jedisPoolConfig.setMaxWaitMillis(3000); // 设置2秒
        //对拿到的connection进行validateObject校验
        jedisPoolConfig.setTestOnBorrow(false);

        JedisCluster jedisCluster = new JedisCluster(nodesList, 2000, 2000, 5, "123456", jedisPoolConfig);
        Map<String, JedisPool> clusterNodes = jedisCluster.getClusterNodes();
        clusterNodes.entrySet().forEach(System.out::println);
        while (true) {
            try {
                String s = UUID.randomUUID().toString();
                jedisCluster.set("k" + s, "v" + s);
                System.out.println(jedisCluster.get("k" + s));
                Thread.sleep(1000);
            }catch (Exception e) {
                logger.error(e.getMessage());
            }
        }
    }
}

运行的结果如下:

在这里插入图片描述

发现我们的循环写是没有问题,那么问题来了,如果我们是调用mset这种一次写入多个键的,能不能成功呢?因为我们知道是先计算key的slot,然后再写入,那么从理论上来说,是可以实现,而且也不是很复杂,我们来测试一下,具体的如下:

import com.google.common.collect.Lists;
import com.ys.redis.MyRedisCluster;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.util.JedisClusterCRC16;

import java.util.*;

public class TestRedisCluster {

    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(TestRedisCluster.class);
        Set<HostAndPort> nodesList = new HashSet<>();
        nodesList.add(new HostAndPort("192.168.181.6", 7000));
        nodesList.add(new HostAndPort("192.168.181.6", 7001));
        nodesList.add(new HostAndPort("192.168.181.6", 7002));
        nodesList.add(new HostAndPort("192.168.181.6", 7003));
        nodesList.add(new HostAndPort("192.168.181.6", 7004));
        nodesList.add(new HostAndPort("192.168.181.6", 7005));


        // Jedis连接池配置
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 最大空闲连接数, 默认8个
        jedisPoolConfig.setMaxIdle(200);
        // 最大连接数, 默认8个
        jedisPoolConfig.setMaxTotal(1000);
        //最小空闲连接数, 默认0
        jedisPoolConfig.setMinIdle(100);
        // 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间,  默认-1
        jedisPoolConfig.setMaxWaitMillis(3000); // 设置2秒
        //对拿到的connection进行validateObject校验
        jedisPoolConfig.setTestOnBorrow(false);

        JedisCluster jedisCluster = new JedisCluster(nodesList, 2000, 2000, 5, "123456", jedisPoolConfig);
        System.out.println(jedisCluster.mset("k1", "v1", "k2", "v2", "k3", "v3"));
    }
}

运行的结果如下:

在这里插入图片描述

可以发现出错了,出错的原因是:无法将此命令分派到 Redis 集群,因为键具有不同的插槽。那么怎么办?这个时候我只有重写,具体的代码如下:

public class MyRedisCluster extends JedisCluster {

    public MyRedisCluster(Set node, int connectionTimeout, int soTimeout, int maxAttempts, String password, GenericObjectPoolConfig poolConfig) {
        super(node, connectionTimeout, soTimeout, maxAttempts, password, poolConfig);
    }

    @Override
    public String mset(String... keyValues) {
        for (int i = 0; i < keyValues.length; i++) {
            if (i % 2 == 0) {
                int slot = JedisClusterCRC16.getCRC16(keyValues[i]) % 16384;
                Jedis connectionFromSlot = this.getConnectionFromSlot(slot);
                System.out.println(keyValues[i] + "---" + keyValues[i + 1]);
                System.out.println(connectionFromSlot.set(keyValues[i], keyValues[i + 1]));
                connectionFromSlot.close();
            }
        }
        return "OK";
    }
}

上面的代码就是先计算这个键的slot,然后获取对应Jedis客户端,然后就可以设置对应的值。我们再来测试一下,具体的代码的如下:

import com.google.common.collect.Lists;
import com.ys.redis.MyRedisCluster;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.util.JedisClusterCRC16;

import java.util.*;

public class TestRedisCluster {

    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(TestRedisCluster.class);
        Set<HostAndPort> nodesList = new HashSet<>();
        nodesList.add(new HostAndPort("192.168.181.6", 7000));
        nodesList.add(new HostAndPort("192.168.181.6", 7001));
        nodesList.add(new HostAndPort("192.168.181.6", 7002));
        nodesList.add(new HostAndPort("192.168.181.6", 7003));
        nodesList.add(new HostAndPort("192.168.181.6", 7004));
        nodesList.add(new HostAndPort("192.168.181.6", 7005));

        // Jedis连接池配置
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 最大空闲连接数, 默认8个
        jedisPoolConfig.setMaxIdle(200);
        // 最大连接数, 默认8个
        jedisPoolConfig.setMaxTotal(1000);
        //最小空闲连接数, 默认0
        jedisPoolConfig.setMinIdle(100);
        // 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间,  默认-1
        jedisPoolConfig.setMaxWaitMillis(3000); // 设置2秒
        //对拿到的connection进行validateObject校验
        jedisPoolConfig.setTestOnBorrow(false);
        MyRedisCluster redisCluster = new MyRedisCluster(nodesList, 2000, 2000, 5, "123456", jedisPoolConfig);
        System.out.println(redisCluster.mset("k1", "v1", "k2", "v2", "k3", "v3"));
    }
}

运行的结果如下:

在这里插入图片描述

发现我们这儿就重写完成了。这个时候我们需要讲我们的Redis的分布式锁了。

3.Redis分布式锁

随着业务的复杂,传统的单体的项目已经无法满足我们现在的需求了,所以我们需要上分布式了,所以传统的jvm锁已经无法满足我们的需求了,需要有新的方案,那么有什么方案呢?既然原来的多台机器多台部署,所以加锁的时候无法是同一把锁,这个时候我们需要上升一个维度,就是找一个公用的地方,进行加锁,这样就不会有问题了,于是我们想到了Redis,那么用Redis,怎么实现加锁呢?首先我们需要了解几个命令,具体的如下:

setex 键 秒值 真实值    设置带过期时间的key,动态设置。

setnx  key   value    只有在 key 不存在时设置 key 的值。

我们可以用上面的API实现对应的加锁的设置,具体的如下图:

在这里插入图片描述

这个图看着似乎很完美,但是似乎有个很大的问题,就是如果线程A抢到了锁,然后执行下单操作的时候,直接崩了,这个时候锁也释放不了,这个时候就会造成死锁的现象了,其他的线程永远拿不到锁,线程A的下单操作也执行不下去。于是我们想到的第二版的解决方案,就是加一个过期时间,于是有了下面的图。

在这里插入图片描述

这个图似乎也没有啥问题,但是如果下单的操作的时间过长,那么这个时候这个lock的键失效了,那么这个时候线程B抢到的锁,然后线程A执行完了,意味着释放锁的时候del lock 释放的是线程B的锁,这样可不行,于是我们又改版。于是有了第三版,具体的如下:

在这里插入图片描述

这儿似乎没有问题,但是又有问题来了,如果我们的业务时间过长的话,大于lock过期的时间,这样同一时间段,就有多个线程来操作这个下单操作了,有人可能说,我把这个时间设置长一点,不就行了嘛!这样就不会导致多个线程加锁成功了,但是这样效率就变低了,所以这种方法似乎也是不行的,于是有了第四版,具体的如下:

在这里插入图片描述

第四版似乎很完美了,但是万一请求多,Redis直接挂了,似乎我们程序就直接崩了,现在全要高可用,所以这儿我们可以用多台Redis,组成一个集群,这样问题就会小很多,于是我们有了第五版的,具体的如下:

在这里插入图片描述

其实上面的几个版本的Redis分布式锁,有个框架叫redission已经帮我们实现了,但是我们今天不打算讲这个,我们要实现一个分布式锁,用第四版的方案实现,先定义我们的接口类,具体的代码如下:

package com.ys;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;

public interface Lock {
    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock() throws Exception;

    Condition newCondition();
}

我们这儿只定义了两个方法,真正的锁有很多的方法,但是由于篇幅的原因,所以我这儿就定义常用的两个方法,然后实现这个类,具体的如下:

package com.ys;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;


@Component
public class RedisLock implements Lock {

    @Autowired
    private JedisPool jedisPool;

    private static final String key = "lock";

    private ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private static AtomicBoolean isHappened = new AtomicBoolean(true);

    //加锁
    @Override
    public void lock() {
        boolean b = tryLock();  //尝试加锁
        if (b) {
            //拿到了锁 直接返回
            return;
        }
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
      //一直递归到获取到锁
        lock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    //尝试加锁
    @Override
    public boolean tryLock() {
      //原子操作
        SetParams setParams = new SetParams();
        setParams.ex(2);  //2s
        setParams.nx();
        String s = UUID.randomUUID().toString();
        Jedis resource = jedisPool.getResource();
        String lock = resource.set(key, s, setParams);
        resource.close();
        if ("OK".equals(lock)) {
            //拿到了锁
            threadLocal.set(s);
            if (isHappened.get()) {
              //设置守护线程
                ThreadUtil.newThread(new MyRunnable(jedisPool)).start();
                isHappened.set(false);
            }
            return true;
        }
        return false;
    }


    static class MyRunnable implements Runnable {

        private JedisPool jedisPool;

        public MyRunnable(JedisPool jedisPool) {
            this.jedisPool = jedisPool;
        }

        @Override
        public void run() {
            Jedis jedis = jedisPool.getResource();
            while (true) {
             	//获取过期时间
                Long ttl = jedis.ttl(key);
              //给过期时间加上1
                if (ttl != null && ttl > 0) {
                    jedis.expire(key, (int) (ttl + 1));
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }


    //第一步判断设置时候的value 和 此时redis的value是否相同
    //解锁
    @Override
    public void unlock() throws Exception {
        String script = "if redis.call(\"get\",KEYS[1])==ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";

        Jedis resource = jedisPool.getResource();
      //比较key和值一样的,才删除
        Object eval = resource.eval(script, Arrays.asList(key), Arrays.asList(threadLocal.get()));
        if (Integer.parseInt(eval.toString()) == 0) {
            resource.close();
            throw new Exception("解锁失败");
        }
        resource.close();
    }

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

}

再来看这个工具类ThreadUtil,具体的如下:

package com.ys;

public class ThreadUtil {

    public static volatile Thread thread;

    public static Thread newThread(Runnable runnable){
        if (thread==null){
            synchronized (ThreadUtil.class){
                if(thread==null){
                    thread = new Thread(runnable);
                  //设置为守护线程
                    thread.setDaemon(true);
                }
                return thread;
            }
        }
        return thread;
    }
}

测试类如下:

package com.ys;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.Redisson;
import org.redisson.RedissonMultiLock;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=SpringConfig.class)
public class Dome {

    private int count=10;

    @Autowired
    @Qualifier("redisLock")
    private com.ys.Lock lock;


    @Test
    public void Test() throws InterruptedException {
        TicketsRunBle ticketsRunBle=new TicketsRunBle();
        Thread thread1=new Thread(ticketsRunBle,"窗口1");
        Thread thread2=new Thread(ticketsRunBle,"窗口2");
        Thread thread3=new Thread(ticketsRunBle,"窗口3");
        Thread thread4=new Thread(ticketsRunBle,"窗口4");
        Thread thread5=new Thread(ticketsRunBle,"窗口5");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
        Thread.currentThread().join();
    }


    public class TicketsRunBle implements Runnable{

        @Override
        public void run() {
            while (count>0){
                lock.lock();
                try {
                    if(count>0){
                      //业务处理的时间大于超时的时间
                        Thread.sleep(ThreadLocalRandom.current().nextInt(2000, 3000));
                        System.out.println(Thread.currentThread().getName()+"售出第"+count--+"张票");
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    try {
                        lock.unlock();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

运行的结果如下:

在这里插入图片描述

可以发现我们的第4版的Redis分布式锁实现完成。

4.写在最后

本篇博客主要介绍Redis的分布式锁和在Redis集群下,Jedis如何录入数据。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值