SpringBoot实现分布式锁

SpringBoot.Redis实现分布式锁

[提前声明]
文章由作者:张耀峰 结合自己生产中的使用经验整理,最终形成简单易懂的文章
写作不易,转载请注明,谢谢!
spark代码案例地址: https://github.com/Mydreamandreality/sparkResearch


线程锁

哎?我们不是要实现分布式锁吗,为啥扯到了线程锁?
不要急,防止有些朋友不熟悉分布式锁的概念,在实现分布式锁之前,咱们先了解下线程锁
线程锁可能对这个概念也有些人不是很清楚,但如果我说synchronize同步关键字,大家是不是就知道了

  • 线程锁:主要用来给类,方法,代码加锁,当某个方法或者某块代码使用synchronize关键字来修饰,那么在同一时刻最多只能有一个线程执行该代码,如果同一时刻有多个线程访问该代码,其它未抢到资源的线程标记为阻塞状态,直到获取到锁的线程执行完毕自动释放其余线程才能执行
  • 线程锁synchronize在单机部署的状态下,确实可以保证线程安全,但是如果是集群或者分布式部署呢?
集群模式下线程锁为何无法满足需求?
  • 分布式的CAP理论:

任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项

  • 现在许多项目考虑到性能和扩展性都通过分布式的方式进行部署,分布式中的数据一致性一直是一个比较重要的问题,基于CAP的理论,在系统最开始设计的时候,就需要对这三点进行取舍,在我们的场景中,一般来讲都是牺牲强一致性,来提升高可用,系统只需要保证最终一致性即可
  • 如果要保证数据的最终一致性可以通过分布式锁,分布式事务等一些技术来实现,很多时候要达到最终一致性,也要保证一个方法在同一时刻只能被一个线程执行
  • 上面我们也说了,在单机环境中,synchronize也确实可以解决我们的问题,但是在分布式环境中,就不可以了,为什么?
  • 我们要知道,分布式系统和单机系统最大的区别就是,单机系统中是多线程,分布式系统中是多进程
  • 多线程可以共享一个Jvm中的堆内存,所以可以采取内存作为标记存储的位置,多进程的情况下,有可能这些进程都不在同一台物理机上,是无法共享同一Jvm中的堆内存,所以就需要把标记存储在一个第三方上面,保证所有进程可见

讲到这里大家也大概能反推出synchronize的底层实现了吧,其实就是Jvm在方法常量池中的方法表结构访问标志区来判断某个方法是否同步方法,方法被调用的时候,调用的指令就会检查方法的访问标记有没有被设置同步,如果设置了,当前执行线程就会持有一个标记,然后执行方法,最后执行完再释放这个标记

设计分布式锁
  • 通过上面的了解,大家应该可以再次反推出分布式锁的实现了吧
  • 在实现分布式锁之前,我们先设计下分布式锁的实现
我们需要什么样的分布式锁?

(我说一下如果是我们场景下的设计)

  • 首先性能肯定要好,获取锁和释放锁的性能一定要高
  • 阻塞锁就可以满足我们的需求(还有非阻塞,公平锁等等各种方式)
  • 不能出现死锁的情况
    然后基于以上的几点,我们来开发分布式锁
使用Redis实现分布式锁
  • 增加Redis的Pom文件
        <!--Redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- redis依赖commons-pool 这个依赖一定要添加 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
  • 我们使用Redis的setnx(Set If Not Exists)

  • setnx的解释:如果指定的key不存在则写入

  • 在RedisClient中setnx写入成功返回1,否则返回0

  • 在JavaRedisApi中写入成功返回True,否则返回False

    • 可以成功写入代表当前的方法并没有被其它进程占用
    • 写入失败代表当前的方法正在被其它进程占用
    • Redis本身是单线程IO多路复用技术,不存在线程安全的问题,所以不用考虑setInx本身线程安全的问题
  • 了解了setnx的用法后,我们的思路就是:

    • 获取锁:使用setnx在Redis中标记当前方法正在被其它进程占用(指定Key)
    • 释放锁:删除我们在Redis中的标记(指定Key)
    • 避免死锁:如果极端情况下,获取锁后执行方法异常导致服务挂了,那么锁是不会释放的,有可能会死锁,所以在获取锁后,需要设置过期时间,防止死锁
  • 如下所示:
    在这里插入图片描述

    扫描二维码关注公众号,回复: 9125065 查看本文章
  • 思路整理清楚后,conding就简单多了

  • 创建RedisUtils工具类

package com.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import v1.exceptionding.DingCloverExceptionEnum;
import v1.exceptionding.DingException;
import v1.util.ToolUtil;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author 孤
 * @version v1.0
 * @Developers 张耀烽
 * @serviceProvider 四叶草安全(SeClover)
 * @description Redis工具
 * @date 2020/1/7
 */
@Component
public class RedisUtils {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * Redis分布式锁
     *
     * @return
     */
    public boolean tryLock(String key, String value, long timeout) {
        if (timeout == 0) {
            timeout = 60 * 3;
        }
        boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(key, value);
        //设置过期时间,防止死锁
        if (isSuccess) {
            redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
        return isSuccess;
    }

    /**
     * Redis 分布式锁释放
     *
     * @param key
     * @param value
     */
    public void unLock(String key, String value) {
        try {
            String currentValue = redisTemplate.opsForValue().get(key);
            if (ToolUtil.isNotEmpty(currentValue) && ToolUtil.equals(currentValue, value)) {
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
        //这个是我的自定义异常,你可以删了
            throw new DingException(DingCloverExceptionEnum.InternalServerError);
        }
    }
}
  • 获取锁和释放锁的工具写好后,定义我们的业务代码
    public void mockLock() {
        RedisUtils redisUtils = SpringContextHolder.getBean(RedisUtils.class);
        InetAddress addr = null;
        try {
            addr = InetAddress.getLocalHost();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        //获取本机ip
        String ip = addr.getHostAddress();
        //此key存放的值为任务执行的ip,
        // expire_time 不能设置为永久,避免死锁
        boolean lock = redisUtils.tryLock("lock_key", ip, 0);
        if (lock) {
            System.out.println("获取分布式锁成功");
            run();
            //释放锁
            redisUtils.unLock("lock_key",ip);
            System.out.println("释放分布式锁成功");
        } else {
            System.out.println("获得分布式锁失败");
            ip = (String) redisUtils.get(lock_key);
            System.out.println(ip+"正在执行该任务");
            return;
        }
    }

    public void run() throws InterruptedException {
        System.out.println("业务执行中");
        Thread.sleep(60 * 3);
        System.out.println("业务执行结束");
    }

关于锁的总结

  • 锁的实现方式和设计有很多方式,Mysql,zookeeper等等都可以,其中的坑也有很多,锁的两大设计模式即是 乐观锁,悲观锁 之后我会抽空把锁这块的知识点汇总然后分享
  • 有任何问题可以留言交流!
发布了55 篇原创文章 · 获赞 329 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/youbitch1/article/details/103906249