参考了 深度剖析:Redis 分布式锁到底安全吗?看完这篇文章彻底懂了!
参考了 尚学堂老师的课程,感谢老师的讲解
前言:在自己的业务中研究了一下redis锁的问题,现整理如下

  1. 为什么需要分布式锁?
/**
     * 根据 id 更新商品名字 (不考虑业务场景,只研究一下分布式锁的问题)
     *  为什么需要分布式锁?
     *      单机锁: 与分布式锁相对应的是「单机锁」,我们在写多线程程序时,避免同时操作一个共享变量产生数据问题,通常会使用一把锁来「互斥」,以保证共享变量的正确性,其使用范围是在「同一个进程」中。
     *          1. 在单进程的情况下, 这种加锁方式是能锁住共享资源的,不会多个线程同时操作数据库
     *          2. 如果在多进程(部署多个服务的时候会出现), 则会出现不同进程的线程会同时操作数据库,因此需要加分布式锁
     *
     * @param project
     */
    @Override
    public void updateProjectNameById(Project project){
        synchronized (name){
            int i = projectMapper.updateById(project);
        }
//        updateProjectNameById06(project);
    }

  1. 分布式锁怎么实现?
 /**
     *   分布式锁怎么实现?
     *  分布式锁: 利用第三方系统 redis 实现分布式锁
     *      1. 在多进程的情况下, 能保证资源始终被一个线程访问
     *      2. 有可能出现的问题:
     *          1. 程序处理业务逻辑异常,没及时释放锁
     *          2. 进程挂了,没机会释放锁
     *      3. 这时,这个客户端就会一直占用这个锁,而其它客户端就「永远」拿不到这把锁了
     *
     * @param project
     */
    public void updateProjectNameById02(Project project){
        String key = "lock";
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "qinenqi");  // 1. 占分布式锁,去redis中占坑
        if(flag){  // 加锁成功
            int i = projectMapper.updateById(project);
            stringRedisTemplate.delete(key); // 删除锁
        }else{  // 加锁失败
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            updateProjectNameById02(project);
        }
    }

  1. 如何避免死锁?
/**
     *  如何避免死锁?
     *      在申请锁时,给这把锁设置一个「租期」
     *      可能会出现的问题:
     *          1. 加锁成功,由于网络原因 还没有来得及设置租期
     *          2. 加锁成功之后,redis宕机,还没有来得及设置租期
     *          2. 加锁成功,客户端崩溃,还没来得及设置租期
     *          总之,这两条命令不能保证是原子操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生「死锁」问题。
     *
     * @param project
     */
    public void updateProjectNameById03(Project project){
        String key = "lock";
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "qinenqi");  // 1. 占分布式锁,去redis中占坑
        if(flag){  // 加锁成功
            stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS); // 设置过期时间
            int i = projectMapper.updateById(project);
            stringRedisTemplate.delete(key); // 删除锁
        }else{  // 加锁失败
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            updateProjectNameById03(project);
        }
    }
  1. 加锁和设置租期是一个原子性操作
/**
     *  加锁和设置租期是一个原子性操作
     *      可能出现的问题: 如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了,因为锁过期释放之后,别的请求又加了锁
     *          有可能会释放别人加的锁也就是说 一个客户端释放了其它客户端持有的锁
     *
     * @param project
     */
    public void updateProjectNameById04(Project project){
        String key = "lock";
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "qinenqi", 10, TimeUnit.SECONDS);  // 1. 占分布式锁,去redis中占坑
        if(flag){  // 加锁成功
            int i = projectMapper.updateById(project);
            stringRedisTemplate.delete(key); // 删除锁
        }else{  // 加锁失败
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            updateProjectNameById04(project);
        }
    }
  1. 一个客户端释放了其它客户端持有的锁
 /**
     *  一个客户端释放了其它客户端持有的锁
     *      解决办法是:客户端在加锁时,设置一个只有自己知道的「唯一标识」进去。
     *      可能出现的问题:加锁和释放锁是两条命令,这时,又会遇到我们前面讲的原子性问题了,出现了原子性问题就可能会出现死锁情况
     *		 stringRedisTemplate.delete(key); 删除锁的这条指令不是原子性的, 因此引入了 释放锁的 Lua 脚本
     * @param project
     */
    public void updateProjectNameById05(Project project){
        String key = "lock";
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, 10, TimeUnit.SECONDS);  // 1. 占分布式锁,去redis中占坑
        if(flag){  // 加锁成功
            int i = projectMapper.updateById(project);
            String value = stringRedisTemplate.opsForValue().get(key);
            if(uuid.equals(value)){
                stringRedisTemplate.delete(key); // 删除锁
            }
        }else{  // 加锁失败
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            updateProjectNameById05(project);
        }
    }

  1. 无论出现什么情况都会释放锁
 /**
     * 无论出现什么情况都会释放锁
     *  安全释放锁的 Lua 脚本
     * @param project
     */
    public void updateProjectNameById06(Project project){
        String key = "lock";
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, 10, TimeUnit.SECONDS);  // 1. 占分布式锁,去redis中占坑
        if(flag){  // 加锁成功
            try {
                int i = projectMapper.updateById(project);
                String value = stringRedisTemplate.opsForValue().get(key);
            }finally {
                String script = "if redis.call('get', KEYS[1])== ARGV[1] then returnredis.call('del', KEYS[1])else return 0 end";
                //删除锁
                Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                        , Arrays.asList("lock"), uuid);
            }
        }else{  // 加锁失败
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            updateProjectNameById06(project);
        }
    }

  1. redis锁自动续期
    由于设置了锁自动过期,如果业务逻辑耗时较长,有可能出现业务还没有执行完,redis分布式锁就过期了,如果出现了这种情况,有可能会带来严重的问题(如:库存还没有扣除呢,新的购买业务进来了),因此这种情况下,引入了锁自动续期的功能。实现过程大约是,起一个线程,一直观察redis锁还有多长时间,如果时间小于10秒,则自动续期(30秒),等业务完成并释放锁之后,则结束这个观察线程。
    新建一个观察线程 WatchDogThread

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;

/**
 * 观察线程
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class WatchDogThread extends Thread{

    /**
     * K key   redis  key
     */
    private String key;

    /**
     *  标识符
     */
    private boolean flag;

    private StringRedisTemplate stringRedisTemplate;

    /**
     *  起一个线程  时刻观察者,如果redis锁的时间 小于 5 秒,则续期
     */
    @Override
    public void run() {
       while(flag){
           Long expire = stringRedisTemplate.getExpire(key);
           // 代替调用 releaseLock(), 判断和调用都是为了退出while循环
           if(expire <= 0){
               break;
           }
           log.info("我还是在监控中:" + expire);
           if(expire < 10L){ // redis 锁的时间小于10秒
              // public Boolean expire(K key, long timeout, TimeUnit unit)
               stringRedisTemplate.expire(key, 30, TimeUnit.SECONDS);
           }
           try {
               Thread.sleep(5000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }

    /**
     *  释放锁
     */
    public void releaseLock(){
        flag = false;
    }

}

业务逻辑

/**
     * 锁过期时间不好评估怎么办
     *  redis  自动续期  WatchDog
     * @param project
     */
    public void updateProjectNameById07(Project project){
        Date date01 = new Date();
        String key = "lock";
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, 30, TimeUnit.SECONDS);  // 1. 占分布式锁,去redis中占坑
        if(flag){  // 加锁成功

            // 采用看门狗 时刻观察者,如果redis锁的时间 小于 5 秒,则续期
            WatchDogThread watchDogThread = new WatchDogThread(key, true, stringRedisTemplate);
            watchDogThread.start();
            try {
                int i = projectMapper.updateById(project);
                String value = stringRedisTemplate.opsForValue().get(key);

                Thread.sleep(1000 * 58);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                String script = "if redis.call('get', KEYS[1])== ARGV[1] then return redis.call('del', KEYS[1])else return 0 end";
                //删除锁
                Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                        , Arrays.asList("lock"), uuid);
            }
        }else{  // 加锁失败
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            updateProjectNameById07(project);
        }
        Date date02 = new Date();
        log.info("计算执行时间:" + (date02.getTime() - date01.getTime()));
    }

Logo

更多推荐