常见面试题知识点之:分布式锁

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 常见面试题知识点之:分布式锁

1 分布式锁基本概念


1.1 基本概念


对缓存查询数据库的代码进行加锁,有两种方式:

本地锁 进程锁 具有局限性,只能在同一个项目中生效,不能控制另一个项目中的方式。保证同一时刻只有一个线程可访问共享资源 例如:对查询数据库的方法加锁,同一时间就只能有一个人查询数据库。

分布式锁 分布式锁是指分布式环境下,系统部署在多个机器中,实现多进程分布式互斥的一种锁。为了保证多个进程能看到锁,锁被存在公共存储(比如 Redis、Memcache、数据库等三方存储中),以实现多个进程并发访问同一个临界资源,同一时刻只有一个进程可访问共享资源,确保数据的一致性。 例如:有两个服务器,其中都有查询数据库分类数据的方法,同一时间,只能够有一个项目中的一个线程能够查询成功过。

2 redis分布式锁


2.1 分布式锁的实现逻辑


基本逻辑:所有的客户端同一时间都去一个地方“占坑”,如果当前客户端占到了坑,就执行业务逻辑,如果没有占到就必须等待,直到其他的客户端释放锁。

2.2 基本实现


在 Redis 怎么占坑呢?【加锁】

set命令:set nx

image.png

image.png

/**
 * 分布式锁
 */
public List<Node> getDataByRedisLock(){
    List<Node> nodeList = null;
    //1.获取锁  执行业务代码 set nx (setIfAbsent)
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("redis-lock", "100");
    if (lock){
        //加锁成功查询数据库
        nodeList = getNodesByMysql();
        //查询完成解锁
        redisTemplate.delete("redis-lock");
    }else {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //重试
        getDataByRedisLock();
    }
    return nodeList;
}

问题分析:

  • 加锁之后 如果代码异常 会导致最终锁没有释放会导致死锁怎么办? 加锁的同时设置过期时间
  1. 加锁
  2. 设置过期时间
  • 如果要加锁 需要保证加锁和设置过期时间是原子性的

2.3 加锁原子性


在Redis中支持原子性的加锁和过期时间设置

image.png

image.png

上面的java方法setIfAbsent会被转换为redis的set key value EX 10 NXredis中的单条命令都是原子操作,要么全部执行成功,要么全部执行失败,不会被其他的命令干扰。redis的命令为什么是原子操作呢?redis底层是单线程执行命令。所有客户端的命令发给redis后都会放入一个队列一条条执行,中间没有其他的线程干扰。所以redis的一个命令都是线程安全的。


为什么redis的QPS那么高还是用的是单线程呢?


redis底层采用的非阻塞多路复用的io模型,可以使用单线程处理很高的访问量。

2.4 删锁优化


问题:

  • 在实际的执行中,线程1加了锁,但是有可能业务执行的时间特别长,所以线程1的锁会自己过期
  • 在此期间,线程2,加锁成功
  • 然后线程1执行完业务代码,执行删除锁的操作,此时会把线程2的锁给删除 如何保证不会删错锁?

解决方案:

加锁的时候,设置值为UUID,每个用户的UUID不同,作为key对应的值

image.png

问题分析:

删除锁的过程不是原子性的。删除锁是分为两步的

  1. 获取值 UUID
  2. 删除锁

如果在获取值之后,还没有删除锁之前,当前线程的锁过期了,其他的线程设置了分布式锁,那么就会出现问题。

if 判断可以通过,执行删除锁的代码,Redis会删除其他线程的锁

2.5 删锁原子性


LUA脚本

image.png

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

lua脚本:Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放

image.png

题分析:

以上代码实现了加锁和解锁的原子性,但是还有问题:在业务代码没有执行完之前,分布式锁不应该过期。

  • 锁的过期时间加长 + finally 保证一定会解锁

image.png

  • 自动续期

redis分布式锁最终版代码

/**
 * 分布式锁
 */
public List<Node> getDataByRedisLock(){
    List<Node> nodeList = null;
    //生成UUID
    String uuid = UUID.randomUUID().toString();
    //1.获取锁  执行业务代码 set nx (setIfAbsent)
    //setIfAbsent 该方法如果设置了过期时间 底层就是 set Ex nx 加锁和过期时间设置是原子性的
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("redis-lock", uuid,100,TimeUnit.SECONDS);
    if (lock){
        System.out.println("获取锁");
        //加锁成功查询数据库
        try {
            nodeList = getNodesByMysql();
        }finally {
            //查询完成解锁
            String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
            /**
             * 参数1 脚本
             * 参数2 要删除的key
             * 参数3 uuid
             */
            redisTemplate.execute(
                new DefaultRedisScript<Long>(script,Long.class),
                Arrays.asList("redis-lock"),
                uuid
            );
        }
    }else {
        try {
            System.out.println("没有获取锁重试");
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //重试
        getDataByRedisLock();
    }
    return nodeList;
}

redisson里面有看门狗watchDog可以对锁的时长进行自动续期。


看门狗的底层原理:客户端对redis加完锁后,会启动一个新线程(看门狗),该线程是一个定时线程,会每个时长/3的间隙给redis发出续期命令,续期的时候会校验该锁是不是当前线程的锁,如果是就续期,不是就终止看门狗线程。当主线程联系不上redis或者宕机后,看门狗线程也会联系不上redis,所以可以避免无限续期产生死锁。

2.6 redisson


Redisson是Redis官方推荐的Java版的Redis客户端。它提供的功能非常多,也非常强大。他里面内置了分布式锁的工具和方法使用起来非常方便就像使用本地锁一样。上面的内容就是redission分布式锁的底层原理。

项目中使用redisson实现分布式锁:

  1. 引入依赖
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.15.6</version>
    </dependency>
  1. 配置bean对象
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.setTransportMode(TransportMode.NIO);
        SingleServerConfig singleServerConfig = config.useSingleServer();
        //可以用"rediss://"来启用SSL连接
        singleServerConfig.setAddress("redis://linux100:6379");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}
  1. 使用redisson
@RestController
public class UserController {
    @Autowired
    private RedissonClient redissonClient;
    @GetMapping("/test")
    public String test() {
        RLock lock = redissonClient.getLock("test-lock");
        try {
            System.out.println("加锁成功!!!");
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println("释放锁成功!!!");
            lock.unlock();
        }
        return "success";
    }
}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
30天前
|
Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
|
30天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
30天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
1月前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
Android面试高频知识点(4) 详解Activity的启动流程
27 3
|
1月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
25 2
|
1月前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
49 1
|
1月前
|
Android开发
Android面试高频知识点(1) 图解 Android 事件分发机制
Android面试高频知识点(1) 图解 Android 事件分发机制
39 1
|
1月前
|
消息中间件 架构师 Java
阿里面试:秒杀的分布式事务, 是如何设计的?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试阿里、滴滴、极兔等一线互联网企业时,遇到了许多关于分布式事务的重要面试题。为了帮助大家更好地应对这些面试题,尼恩进行了系统化的梳理,详细介绍了Seata和RocketMQ事务消息的结合,以及如何实现强弱结合型事务。文章还提供了分布式事务的标准面试答案,并推荐了《尼恩Java面试宝典PDF》等资源,帮助大家在面试中脱颖而出。
|
2月前
|
Web App开发 前端开发 Linux
「offer来了」浅谈前端面试中开发环境常考知识点
该文章归纳了前端开发环境中常见的面试知识点,特别是围绕Git的使用进行了详细介绍,包括Git的基本概念、常用命令以及在团队协作中的最佳实践,同时还涉及了Chrome调试工具和Linux命令行的基础操作。
「offer来了」浅谈前端面试中开发环境常考知识点
|
2月前
|
NoSQL Java Redis
面试官:项目中如何实现分布式锁?
面试官:项目中如何实现分布式锁?
93 6
面试官:项目中如何实现分布式锁?
下一篇
无影云桌面