浅谈分布式锁的原理

news2025/1/16 1:01:35

1.业务场景引入

在进行代码实现之前,我们先来看一个业务场景:

系统A是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单。
由于系统有一定的并发,所以会预先将商品的库存保存在redis中,用户下单的时候会更新redis的库存。

此时系统架构如下:
在这里插入图片描述
但是这样一来会产生一个问题:

假如某个时刻,redis里面的某个商品库存为1,此时两个请求同时到来,其中一个请求执行到上图的第3步,更新数据库的库存为0,但是第4步还没有执行。

而另外一个请求执行到了第2步,发现库存还是1,就继续执行第3步。

这样的结果,是导致卖出了2个商品,然而其实库存只有1个。

很明显不对啊!这就是典型的库存超卖问题

此时,我们很容易想到解决方案:用锁把2、3、4步锁住,让他们执行完之后,另一个线程才能进来执行第2步。

在这里插入图片描述
按照上面的图,在执行第2步时,使用Java提供的synchronized或者ReentrantLock来锁住,然后在第4步执行完之后才释放锁。

这样一来,2、3、4 这3个步骤就被“锁”住了,多个线程之间只能串行化执行。

但是好景不长,整个系统的并发飙升,一台机器扛不住了。现在要增加一台机器,如下图:
在这里插入图片描述
增加机器之后,系统变成上图所示,我的天!

假设此时两个用户的请求同时到来,但是落在了不同的机器上,那么这两个请求是可以同时执行了,还是会出现库存超卖的问题。

为什么呢?因为上图中的两个A系统,运行在两个不同的JVM里面,他们加的锁只对属于自己JVM里面的线程有效,对于其他JVM的线程是无效的。

因此,这里的问题是:Java提供的原生锁机制在多机部署场景下失效了

这是因为两台机器加的锁不是同一个锁(两个锁在不同的JVM里面)。

那么,我们只要保证两台机器加的锁是同一个锁,问题不就解决了吗?

此时,就该分布式锁隆重登场了,分布式锁的思路是:

任何场景下,一旦加锁,效率不可能会高,数据是安全的!

在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。

至于这个“东西”,可以是Redis、Zookeeper,也可以是数据库。

在这里插入图片描述
通过上面的分析,我们知道了库存超卖场景在分布式部署系统的情况下使用Java原生的锁机制无法保证线程安全,所以我们需要用到分布式锁的方案。

那么,如何实现分布式锁呢?

2.分布式锁的实现

2.1.分布式锁的简单实现代码

package com.bruceliu.lock;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;
import java.util.UUID;

/**
 * @BelongsProject: RedisLock
 * @BelongsPackage: com.bruceliu.lock
 * @Author: bruceliu
 * @QQ:1241488705
 * @CreateTime: 2020-05-06 17:51
 * @Description: 分布式锁简单实现
 */
public class DistributedLock {

    private final JedisPool jedisPool;

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

    /**
     * 加锁
     * @param lockName       锁的key
     * @param acquireTimeout 获取超时时间
     * @param timeout        锁的超时时间
     * @return 锁标识
     */
    public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
        Jedis conn = null;
        String retIdentifier = null;
        try {
            // 获取连接
            conn = jedisPool.getResource();
            // 随机生成一个value
            String identifier = UUID.randomUUID().toString();
            // 锁名,即key值
            String lockKey = "lock:" + lockName;

            // 超时时间,上锁后超过此时间则自动释放锁
            int lockExpire = (int) (timeout / 1000);

            // 获取锁的超时时间,超过这个时间则放弃获取锁
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (conn.setnx(lockKey, identifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                    // 返回value值,用于释放锁时间确认
                    retIdentifier = identifier;
                    return retIdentifier;
                }
                // 返回-1代表key没有设置超时时间,为key设置一个超时时间
                if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retIdentifier;
    }


    /**
     * 释放锁
     * @param lockName   锁的key
     * @param identifier 释放锁的标识
     * @return
     */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = "lock:" + lockName;
        boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            while (true) {
                // 监视lock,准备开始事务
                conn.watch(lockKey);
                // 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
                if (identifier.equals(conn.get(lockKey))) {
                    Transaction transaction = conn.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                    if (results == null) {
                        continue;
                    }
                    retFlag = true;
                }
                conn.unwatch();
                break;
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}

2.2.测试刚才实现的分布式锁

例子中使用50个线程模拟秒杀一个商品,使用–运算符来实现商品减少,从结果有序性就可以看出是否为加锁状态。

模拟秒杀服务,在其中配置了jedis线程池,在初始化的时候传给分布式锁,供其使用。

package com.bruceliu.lock;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @BelongsProject: RedisLock
 * @BelongsPackage: com.bruceliu.lock
 * @Author: bruceliu
 * @QQ:1241488705
 * @CreateTime: 2020-05-07 09:23
 * @Description: TODO
 */
public class SkillService {

    private static JedisPool pool = null;
    private DistributedLock lock = new DistributedLock(pool);

    int n = 500;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        // 设置最大连接数
        config.setMaxTotal(200);
        // 设置最大空闲数
        config.setMaxIdle(8);
        // 设置最大等待时间
        config.setMaxWaitMillis(1000 * 100);
        // 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的
        config.setTestOnBorrow(true);
        pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
    }

    public void seckill() {
        // 返回锁的value值,供释放锁时候进行判断
        String identifier = lock.lockWithTimeout("resource", 5000, 1000);
        System.out.println(Thread.currentThread().getName() + "获得了锁");
        System.out.println(--n);
        lock.releaseLock("resource", identifier);
    }
}

2.3.模拟线程进行秒杀服务

package com.bruceliu.lock;

/**
 * @BelongsProject: RedisLock
 * @BelongsPackage: com.bruceliu.lock
 * @Author: bruceliu
 * @QQ:1241488705
 * @CreateTime: 2020-05-07 09:24
 * @Description: TODO
 */
public class TestLock {

    public static void main(String[] args) {
        SkillService service = new SkillService();
        for (int i = 0; i < 50; i++) {
            ThreadA threadA = new ThreadA(service);
            threadA.setName("ThreadNameA->"+i);
            threadA.start();
        }
    }
}

class ThreadA extends Thread {

    private SkillService skillService;

    public ThreadA(SkillService skillService) {
        this.skillService = skillService;
    }

    @Override
    public void run() {
        skillService.seckill();
    }
}

2.4.测试结果

若注释掉使用锁的部分:

public void seckill() {
    // 返回锁的value值,供释放锁时候进行判断
    //String identifier = lock.lockWithTimeout("resource", 5000, 1000);
    System.out.println(Thread.currentThread().getName() + "获得了锁");
    System.out.println(--n);
    //lock.releaseLock("resource", identifier);
}

从结果可以看出,有一些是异步进行的:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/365373.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SPINAND UBI 离线烧录 开发指南

SPINAND UBI 离线烧录 开发指南 1 概述 编写目的: 介绍Sunxi SPINand 烧写时的数据布局 2 名词解释 词义UBIunsorted block imagePEBphysical erase blockLEBlogical erase block PEB 和logical block 关系 1 PEB 1 logical block 1 logical block 2 physical blocks3 总…

React从入门到精通二

React从入门到精通之购物车案例1. 购物车需求说明使用到的data list2. 项目code1. 购物车需求说明 list data展示到列表中每个item的通过按钮来控制购买的数据量删除按钮可以删除当前的itemTotal Price计算当前购物车的总的价格 使用到的data list const books [{id: 1,name…

OAK相机深度流探测草莓距离

编辑&#xff1a;OAK中国 首发&#xff1a;oakchina.cn 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 ▌前言 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是助手…

uniapp 悬浮窗(悬浮球、动态菜单、在其他应用上层显示) Ba-FloatBall

简介&#xff08;下载地址&#xff09; Ba-FloatBall 是一款在其他应用上层显示的悬浮球插件。支持展示菜单、拖动、自动贴边等&#xff1b;支持自定义样式。 支持添加展示菜单&#xff0c;可自定义&#xff08;不添加菜单&#xff0c;可只显示悬浮球&#xff09;支持自定义悬…

一口吃不成ChatGPT,复旦版MOSS服务器被挤崩后续

ChatGPT 是目前最先进的 AI&#xff0c;由于 ChatGPT 的训练过程所需算力资源大、标注成本高&#xff0c;此前国内暂未出现对大众开放的同类产品。 适逢ChatGPT概念正火&#xff0c;2 月 21 日&#xff0c;复旦团队发布首个中国版类 ChatGPT 模型「MOSS」&#xff0c;没想到瞬时…

Python-生成列表

1.生成列表使用列表前必须先生成列表。1.1使用运算符[ ]生成列表在运算符[ ]中以逗号隔开各个元素会生成包含这些元素的新列表。另外&#xff0c;如果[ ]中没有元素就会生成空列表示例>>> list01 [] >>> list01 [] >>> list02 [1, 2, 3] >>…

云、安全、网络三位一体,Akamai 推出大规模分布式边缘和云平台 Akamai Connected Cloud

出品 | CSDN 云计算 云服务市场规模在持续增长。 基于网络技术积累与优势&#xff0c;与布局边缘计算之后&#xff0c;巨头 Akamai 在继续推进它的技术与产品进程。近日&#xff0c;Akamai 正式推出大规模分布式边缘和云平台 Akamai Connected Cloud&#xff0c;包含云计算、安…

软考学习笔记(题目知识记录)

答案为 概要设计阶段 本题涉及软件工程的概念 软件工程的任务是基于需求分析的结果建立各种设计模型&#xff0c;给出问题的解决方案 软件设计可以分为两个阶段&#xff1a; 概要设计阶段和详细设计阶段 结构化设计方法中&#xff0c;概要设计阶段进行软件体系结构的设计&…

学生管理系统-课后程序(JAVA基础案例教程-黑马程序员编著-第六章-课后作业)

【案例6-2】 学生管理系统 【案例介绍】 1.任务描述 在一所学校中&#xff0c;对学生人员流动的管理是很麻烦的&#xff0c;本案例要求编写一个学生管理系统&#xff0c;实现对学生信息的添加、删除、修改和查询功能。每个功能的具体要求如下&#xff1a; 系统的首页&#…

视频技术基础知识

一、视频图像基础 像素&#xff1a;图像的基本单元&#xff0c;即一个带有颜色的小块分辨率&#xff1a;图像的大小或尺寸&#xff0c;用像素个数来表示。原始图像分辨率越高&#xff0c;图像就越清晰位深&#xff1a;存储每位像素需要的二进制位数&#xff1b;位深越大&#…

JAVA线程入门简介

线程入门简介什么是程序?什么是进程?什么是线程&#xff1f;单线程与多线程并发与并行线程的使用用java查看有多少个cpu创建线程的两种方式继承Thread类&#xff0c;重写run方法实现Runnable接口&#xff0c;重写run方法多线程机制为社么是start?源码解析什么是程序? 是为完…

防错料使用二维码解决方案 生产过程物料防错管理

生产过程中&#xff0c;物料的防错管理是非常重要的一环。它能够有效地防止物料错用或混用&#xff0c;从而降低产品质量问题的发生率&#xff0c;减少生产成本和生产周期&#xff0c;提高生产效率和产品质量。以下是生产过程物料防错管理的具体措施&#xff1a;1.明确物料标识…

SpringBoot Data Redis来操作Redis

SpringBoot Data Redis来操作Redis1、Redis启动Redis主要的作用安装的位置启动2、Java中来操作Redis3、Spring Data Redis(重点)测试连接配置Redis序列化器redisTemplate操作常见数据类型通用操作&#xff0c;针对不同的数据类型都可以操作申明&#xff1a; 未经许可&#xff0…

浅谈Springboot自动化配置原理

文章目录1.前言2.SpringBoot的入口3.SpringBootApplication背后的秘密4.Configuration5.ComponentScan扫描bean6.EnableAutoConfiguration7.自动配置生效1.前言 不论在工作中&#xff0c;亦或是求职面试&#xff0c;Spring Boot已经成为我们必知必会的技能项。除了某些老旧的政…

java面试题-JUC线程池

1.FutureTask的作用?FutureTask 是 Java 并发编程中的一个类&#xff0c;用于异步执行任务并获取其结果。它实现了 Future 和 Runnable 接口&#xff0c;因此可以作为一个可运行的任务提交给 Executor 执行&#xff0c;也可以通过 Future 接口获取任务执行的结果。FutureTask …

2023年DAMA-CDGA/CDGP数据治理认证选择哪家机构好?

DAMA认证为数据管理专业人士提供职业目标晋升规划&#xff0c;彰显了职业发展里程碑及发展阶梯定义&#xff0c;帮助数据管理从业人士获得企业数字化转型战略下的必备职业能力&#xff0c;促进开展工作实践应用及实际问题解决&#xff0c;形成企业所需的新数字经济下的核心职业…

将整数数组变为浮点型数组的np.asfarray()方法

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 将整数数组转换为浮点型数组 np.asfarray() 选择题 关于以下代码说法错误的一项是? import numpy as np a1 np.array([1,2,3]) print("【显示】a1",a1) print("【执行】a…

网络工程(一) 简单的配置

网络工程 简单的配置 需求 两台交换机 两台路由器 两台PC AR1配置静态路由 system-view [HUAWEI]sysname ar1 [ar1]interface g 0/0/0 [ar1-G…0/0/0]ip address 192.168.2.1 24 [ar1-G…0/0/0]quit [ar1]interface g 0/0/1 [ar1-G…0/0/1]ip address 192.168.3.1 24 [ar1-G…

关于学习git时的一些疑惑与笔记

关于学习git时的一些疑惑与笔记SSH相关问题SSH是什么&#xff1f;SSH有什么作用&#xff1f;如何在github配置SSH?分支什么是本地分支&#xff0c;远程分支&#xff1f;main主分支与master主支&#xff1f;为什么要把master分支修改为main分支&#xff1f;什么时候用分支&…

Java线程——常见方法

一、 常见方法 1.1 概述 ① start_vs_run&#xff1a;直接调用run方法并不会启动新的线程 import cn.itcast.n2.util.FileReader; import lombok.extern.slf4j.Slf4j;Slf4j(topic "c.Test") public class Test {public static void main(String[] args) {Thread t…