redis实现分布式锁(完善版)

针对之前文章实现的redis分布式锁的方式,有几个问题,需要改进

谨记:Redis运行模式是单线程的

问题1:如果执行setnx()之后在执行expire()的中间,多线程情况下发生,其他线程进行其他操作出现卡顿现象,那么这个过期时间就设置不了了,如果解决这个原子性问题???
答:使用原子性命令,将setnx和expire操作一步完成,使用命令:set key value [EX seconds] [PX milliseconds] [NX|XX]
EX: key在多少秒之后过期
PX:key在多少毫秒之后过期
NX: 当key不存在的时候,才创建key,效果等同于setnx
XX:当key存在的时候,覆盖key

问题2:为什么有释放锁的操作还要设置过期时间???
因为如果A业务执行过程中,线程挂掉了,那么就不会执行释放锁的操作,这样就会造成死锁问题

问题3:如果线程A拿到锁后设置过期时间为5s,A业务却执行了6s,这是当第6s的时候,线程A就会去释放锁,但是这时锁已经被线程B获取到了,所以线程A就会将线程B获取的锁释放掉,这个应该如何解决???
答:第一种:使用ThreadLocal(),将线程A的key对应的value存储进去,然后释放锁的时候,先判断ThreadLocal()里面的值和Redis存储的值是否一样,一样在释放锁,不一样就抛异常回滚
第二种:为线程A的锁续期,创建一个线程,专门用于监听锁是否过期,每隔5s执行一次,线程A的锁就不会过期
比较第一种会有两个线程同时拥有锁的操作,这样数据回滚也会有问题,而且逻辑上容易出现bug,所以第二种会更好一些。

问题3:那么如果线程A挂了,用于监听的线程是不会停止的,就会一直为线程A的锁续期,依旧会产生死锁问题???
答:该监听线程为守护线程,和普通的线程不一样,当线程A这个主线程挂掉之后,守护线程也会随之而停止

针对上面提出的几个问题,在改造一版逻辑更加完善,更加安全的Redis实现分布式锁

实现思路:加锁:我们使用setnx+ex的方式设置一个key,同时给kye设置过期时间和value值,value值为一个随机数。事先我们需要创建一个ThreadLocal()实例,用于当前线程存储key对应的value值。将value储存到ThreadLocal()实例中,释放锁时需要。在加锁的时候还需要单独启动一个守护线程,为当前线程获取的锁续期服务。
释放锁:释放锁时我们需要拿到当前key的value与我们放到ThreadLocal()里面的值进行对比,对比相同之后,在进行释放锁操作。为了保证查询value操作、对比值操作和del()释放锁操作的原子性问题,我们可以使用lua表达式完成该步骤。释放锁的操作也是从线程池中获取的线程,所以该线程用完也是需要释放到线程池中。

具体代码实现一下:

首先我们添加主要的pom依赖文件

        
        
            redis.clients
            jedis
            3.0.1
        

        
        
            junit
            junit
            4.12
        

接下来我们编写spring配置类

package com.chuxin.example.redis.lock.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.*;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @FileName: SpringConfig
 * @Description: 配置类
 * @author: myp
 * @create: 2019-08-02 14:40
 */
@Configuration
@ComponentScan("com.chuxin.example.redis.lock")
public class SpringConfig {

    @Scope("prototype")
    @Lazy
    @Bean
    public Jedis jedis(JedisPool jedisPool){
        return jedisPool.getResource();
    }

    //配置一个连接池
    @Bean
    public JedisPool jedisPool(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(10);
        jedisPoolConfig.setMaxIdle(10);
        jedisPoolConfig.setMinIdle(1);
        jedisPoolConfig.setMaxWaitMillis(2000);
        jedisPoolConfig.setTestOnBorrow(true);
//        jedisPoolConfig.setTestOnReturn(false);
        jedisPoolConfig.setTestOnReturn(true);//生产环境必须使用true
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379);
        return jedisPool;
    }

}

编写一个创建线程的单例模式的工具类,这个作为守护线程使用

package com.chuxin.example.redis.lock.config;

/**
 * @FileName: ThreadUtil
 * @Description: 线程工具类
 * @author: myp
 * @create: 2019-08-02 14:49
 */
public class ThreadUtil {

    public static 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;
    }

}

接下来就是重点实现我们加锁和释放锁的关键代码了
重新定义lock接口

package com.chuxin.example.redis.lock.test;

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

/**
 * @FileName: Lock
 * @Description:
 * @author: myp
 * @create: 2019-08-02 15:13
 */
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.chuxin.example.redis.lock.test;

import com.chuxin.example.redis.lock.config.ThreadUtil;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;

/**
 * @FileName: RedisLock
 * @Description: Redis锁
 * @author: myp
 * @create: 2019-08-02 14:00
 */
@Component
@Service("lock")
public class RedisLock implements Lock {

    @Resource
    private JedisPool jedisPool;

    private static final String key = "lock";

    private ThreadLocal threadLocal = new ThreadLocal();

    private static AtomicBoolean isHappened = new AtomicBoolean(true);

    //加锁方法
    @Override
    public void lock() {
        boolean b = tryLock();
        if (b) {
            return ;
        } try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock();
    }


    //尝试加锁方法
    @Override
    public boolean tryLock() {
        //jedis3.0之后才提供的这个对象
        SetParams setParams = new SetParams();
        setParams.ex(1);//设置过期时间为2秒
        setParams.nx();
        String s = UUID.randomUUID().toString();
        Jedis resource = null;
        try {//这里捕获一下异常,如果获取资源时出现异常,那么将没有用到的资源放回资源池中
            resource = jedisPool.getResource();
        } catch (RuntimeException e) {
            if (resource != null) {
                resource.close();
            } else {
                System.out.println("资源池中资源不够用啦,在这里处理。。。阻塞等待或者丢弃请求");
                return false;
            }
        }

        //加锁,同时设置value值、过期时间。如果返回ok就说明之前这个key是不存在的,现在已经添加上了
        String lock = resource.set(key,s,setParams);

        //jedis3.0之前使用下面这行代码
//        String lock = resource.set(key,s,"NX","PX",5000);
        
        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;
    }

    @Override
    public void unLock() throws Exception {

        //解析:注释的这段代码和下面的代码都是释放锁用的。上面的由原子性问题。下面的是使用lua表达式解决了lua表达式的。
//        Jedis resource = jedisPool.getResource();
//        if (resource.get(key).equals(threadLocal.get())) {
//            Long del = resource.del(key);
//            if (del == 0) {
//                resource.close();
//            throw new Exception("解锁失败!");
//            }
//        }
//        resource.close();



        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();
        Object eval = resource.eval(script, Arrays.asList(key),Arrays.asList(threadLocal.get()));
        if (Integer.valueOf(eval.toString()) == 0) {
            resource.close();
            throw new Exception("解锁失败!");
        } else {
            resource.close();
        }
    }

    public 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);
                if (ttl != null && ttl > 0) {
                    jedis.expire(key,(int)(ttl+1));//单位s
                }
                try {
                    Thread.sleep(1000);
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }


    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

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

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


}

上面的具体实现里面使用的技术点:
1、lua脚本,lua脚本可以保证原子性操作。
2、AtomicBoolean isHappened,它是是Java.util.concurrent.atomic包下的原子变量,保证线程中的某个操作值进行一次。
3、ThreadLocal类,保证对象里的值只能当前本地线程访问。用于存储当前锁对应的value值,释放锁的时候需要该值

最后我们写一个Demo测试一下我们的代码:

package com.chuxin.example.redis.lock.test;

import com.chuxin.example.redis.lock.config.SpringConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.api.RedissonClient;
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.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;

/**
 * @FileName: Demo
 * @Description: 单元测试demo
 * @author: myp
 * @create: 2019-08-02 14:56
 */
@ContextConfiguration(classes = SpringConfig.class)
@RunWith(SpringRunner.class)
@WebAppConfiguration
public class Demo {

    private static int count = 1000;

    @Autowired
    @Qualifier("lock")
    private Lock lock;

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

    public class TicketRunBle implements Runnable{

        @Override
        public void run(){
            while (count > 0) {
                //调用加锁方法
                lock.lock();
                try {
                    if (count > 0) {
                        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 (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

最终就打印出来了多线程并发情况下,这1000张票出票的记录,不会出现重复出票的现象。。。


2020年07月19日补充更新:
如果redis我们使用主从模式实现的情况下,这样就会出现一个问题:如果redis主节点突然宕机怎么办?有的小伙伴会说,在主节点宕机之后,使用从节点就可以了,但是我们知道redis的主从复制是异步执行的,现在如果A获取锁之后,执行正常的业务处理,这时主节点中A获取的key还未同步到从节点上,主节点突然宕机,这时使用从节点,但是从节点中并没有A获取的key信息,这样B节点此时来获取锁,那么就会出现A和B同时获取到锁。如何解决这个问题呢???

答案:第一种方案:我们上面的实现方式其实是可以解决这个问题的,因为我们释放锁的时候是存在一次校验的,如果校验失败,我们可以做业务数据回滚,也就是如果上面问题发生时,A的业务就是执行不成功的,实际上执行不成功的原因就是由于主节点宕机导致的,这样可以解决问题,同时也能保证数据正确性;

小型项目中实际应用场景:
防重复提交;拼团活动参团操作;秒杀活动;

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