[黑马点评]关于原子性,锁的笔记

        不得不说,黑马点评是一个非常不错的课程,对于线程安全方面的讲解十分详细且明朗,故写下这篇笔记方便复习及帮助后人()


目标

        我们的目标是对于大量对于优惠劵的访问时,要防止超卖问题以及一人多单问题。


单JVM(非集群)

        非集群的话解决方式很简单:

1.超卖问题

        问题出在我们每次操作完查询优惠劵数量时,准备将优惠劵数量减一时,这个间隔出现了大量的其他线程进行同样操作,把优惠劵减到了0以下:

解决方式也很简单:

        在减操作时同时检测当时的数量是否大于0即可:

        boolean success = seckillVoucherService
                .update().setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock",0)
                .update();
        if(!success){
            return Result.fail("已抢光!");
        }

如果大于0,说明当前的确有剩下,反正说明没有了,就返回报错。

这里利用的是数据库操作的原子性,我们之所以会出错,就是因为我们的查询和操作之间有间隔,但是数据库操作时会加锁,数据库的查询与操作是具有原子性的!

这样就能利用数据库的原子性来帮助我们变相“加锁”

这样的话仍然是并行,效率较高,属于乐观锁

如果采用悲观锁的话,只能像之前用互斥锁那样来手动变成串行,效率较低,不建议使用


2.一人一单

        这里的思路很明确:在购买之前手动查数据库看看是不是已经买过了。

但是同样存在线程问题:你的查询和购买存在间隙,可能你查的时候你还没买,正在买时另一个线程已经买完了提交了,那么你就买了两张

怎么解决呢?手动在查询和购买之间加锁:

    @Transactional
    public Result getResult(Long voucherId) {
        synchronized (userId.toString().intern()) {
            long userId = UserHolder.getUser().getId();
            int cnt = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            if (cnt > 0) {
                return Result.fail("已经购买过!");
            }

            boolean success = seckillVoucherService
                    .update().setSql("stock = stock - 1")
                    .eq("voucher_id", voucherId)
                    .gt("stock", 0)
                    .update();
            if (!success) {
                return Result.fail("已抢光!");
            }
            long orderId = redisIdGenerator.nextId("order");

            VoucherOrder voucherOrder = new VoucherOrder();
            voucherOrder.setId(orderId);
            voucherOrder.setVoucherId(voucherId);
            voucherOrder.setUserId(userId);
            save(voucherOrder);
            return Result.ok(orderId);
        }
    }

这样子看上去的确防止了这种问题

但是我们对这个方法开启了事务处理:

在执行完这个方法后,

锁被释放,但是事务还没有提交!!!

那么数据库的操作还没有被提交!!!

这个间隙就可能会被其他线程进入,继续操作!!!

那么我们的锁就要包括整个方法,让这个方法从头到尾完全执行完之后才能释放锁!

    synchronized (userId.toString().intern()) {
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.getResult(voucherId);
    }

改成这样即可

这里为什么要手动获取代理对象呢?

原因在于,只有 getResult是被@Transactional代理的,但你直接

return getResult(voucherId)实际上是 returbn this.getResult(voucherId),返回的是直接调用而不是通过代理对象导入(我写过一篇关于代理的文章)

所以要手动从代理处调用

这样即可解决一人一单问题。


集群的问题

1.误删问题

        误删是指当线程A堵塞,导致锁超时自动释放时,线程B开始获取锁进行工作,就在工作中,堵塞的线程A苏醒并完成了工作释放了锁,线程C就会进入与正在工作的线程B竞争,引发安全问题。

        解决方案非常简单:对于每一把锁赋予创建线程的唯一标识,只有具有该标识的线程才能释放该锁:

        


@Component
public class SimpleRedisLock {
    @Autowired
    private RedisTemplate redisTemplate;

    static final String uuid = UUID.randomUUID().toString() + "-";

    public boolean tryLock(String name, Long expireTime) {
        long threadId = Thread.currentThread().getId();
        //threadId区分线程,uuid区分JVM
        String key = "lock:" + name ;
        Boolean b = redisTemplate.opsForValue().setIfAbsent(key, uuid + threadId, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(b);
    }

    public void unlock(String name) {
        String key = "lock:" + name;
        long threadId = Thread.currentThread().getId();
        String id = (String) redisTemplate.opsForValue().get(key);
        if (id == uuid + threadId) {
            redisTemplate.delete(key);
        }

    }

}

        UUID是static,这样的话同一个JVM的uuid就相同了,uuid用来区分不同JVM,而threadId用来区分不同的线程。

        这样就貌似保证了在集群环境下每把锁只能解锁自己获取的锁。

2.原子性问题

        但是这样还可能有一个问题:

假如一个线程判断完了线程属于自己,正准备释放锁时,被阻塞了(GC等),时间长到了触发了锁的自动释放,就会有另外一个线程获得锁并进入。

        这时假如阻塞线程苏醒,那它就会立刻释放锁:因为之前判断过了:就会放进其他线程造成安全问题。

但实际上这种情况并不会发生(黑马点评例子没举好):

因为我们的锁标识是threadId+uuid,阻塞线程想要释放锁执行的是:

public void unlock(String name) {
        String key = "lock:" + name;
        long threadId = Thread.currentThread().getId();
        String id = (String) redisTemplate.opsForValue().get(key);
        if (id == uuid + threadId) {
            redisTemplate.delete(key);
        }

    }

就算是删也是根据这个锁的唯一标识(threadId + uuid)来删,根本不会误删其它线程的锁.

但是,如果无法保证查询和操作的原子性,就存在安全性问题,还是需要解决,思路也很简单,让数据库帮我们查询和删除,因为数据库操作是有原子性的!

这里我们就需要Redis执行Lua脚本来实现同时查询和删除!

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

Lua的语法建议学一下

将脚本存储到resources中

修改锁代码:


@Component
public class SimpleRedisLock {
    @Autowired
    private RedisTemplate redisTemplate;

    static final String uuid = UUID.randomUUID().toString() + "-";
    static final DefaultRedisScript<Long> script;
    static {
        script = new DefaultRedisScript<>();
        script.setResultType(Long.class);
        script.setLocation(new ClassPathResource("unlock.lua"));
    }

    public boolean tryLock(String name, Long expireTime) {
        long threadId = Thread.currentThread().getId();
        //threadId区分线程,uuid区分JVM
        String key = "lock:" + name ;
        Boolean b = redisTemplate.opsForValue().setIfAbsent(key, uuid + threadId, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(b);
    }

    public void unlock(String name) {
        String key = "unlock:" + name ;
        long threadId = Thread.currentThread().getId();
        redisTemplate.execute(script, Collections.singletonList(key),uuid + threadId);
    }

}

即可完美解决原子性问题.


终极解决方案

有些人可能会问了:

        作者作者,你说的Lua脚本实现原子性,什么悲观锁乐观锁确实高大上,但是有没有更简单的方案啊

        有的兄弟有的,这样简单又好用的我们还有 Redisson! 各种锁一键傻瓜式使用,轻松帮你实现各种高大上复杂的锁!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

mapbox高阶,结合threejs(threebox)添加三维球体

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️mapboxgl.Map style属性1.3 ☘️threebox Sphere静态对象二、🍀使用t…

MAC 本地搭建部署 dify(含 github访问超时+Docker镜像源拉取超时解决方案)

目录 一、什么是 dify&#xff1f; 二、安装 docker 1. 什么是 docker&#xff1f; 2. docker下载地址 三、安装 dify 1. dify下载地址 2.可能遇到问题一&#xff1a; github访问超时 3.下载后完成解压 4.进入到 cmd 终端环境&#xff0c;执行下面三个命令 5.可能遇到…

Pytorch xpu环境配置 Pytorch使用Intel集成显卡

1、硬件集显要为Intel ARC并安装正确驱动 2、安装Intel oneAPI Base Toolkit &#xff08;https://www.intel.cn/content/www/cn/zh/developer/tools/oneapi/base-toolkit-download.html&#xff09;安装后大约20G左右&#xff0c;注意安装路径 3、安装Visual Studio Build To…

若依前后端分离版使用Electron打包前端Vue为Exe文件

1.前言 本文详细介绍如何使用electron将若依框架前后端分离版的前端Vue页面打包为Exe文件&#xff0c;并且包括如何实现应用更新。使用若依基础代码体现不出打包功能&#xff0c;因此我使用开发的文件管理系统&#xff0c;介绍上述过程&#xff0c;具体可以查看我的文章《若依…

docker:Dockerfile案例之自定义centos7镜像

1 案例需求 自定义centos7镜像。要求&#xff1a; 默认登录路径为 /usr可以使用vim 2 实施步骤 编写dockerfile脚本 vim centos_dockerfile 内容如下&#xff1a; #定义父镜像 FROM centos:7#定义作者信息 MAINTAINER handsome <handsomehandsome.com># 设置阿里云…

SpringBoot校园管理系统设计与实现

在现代校园管理中&#xff0c;一个高效、灵活的管理系统是不可或缺的。本文将详细介绍基于SpringBoot的校园管理系统的设计与实现&#xff0c;涵盖管理员、用户和院校管理员三大功能模块&#xff0c;以及系统的部署步骤和数据库配置。 管理员功能模块 管理员是系统的核心管理…

[项目]基于FreeRTOS的STM32四轴飞行器: 四.LED控制

基于FreeRTOS的STM32四轴飞行器: 四.LED控制 一.配置Com层二.编写驱动 一.配置Com层 先在Com_Config.h中定义灯位置的枚举类型&#xff1a; 之后定义Led的结构体&#xff1a; 定义飞行器状态&#xff1a; 在Com_Config.c中初始化四个灯&#xff1a; 在Com_Config.h外部声明…

Linux部署java项目

前言 Xshell下载地址 点击连接 常见命令 ls ls:显示当前目录下的文件 ll:可以显示隐藏文件和非隐藏文件与ls -l一样 ls -a -l这两个掌握就可以了 ls --help就可以知道这个后面可以跟什么 ls -al还可以这样 cd cd&#xff1a;进入文件夹 cd后面可以跟相对路径&#xff0…

鸿蒙Android4个脚有脚线

效果 min:number122max:number150Row(){Stack(){// 底Text().border({width:2,color:$r(app.color.yellow)}).height(this.max).aspectRatio(1)// 长Text().backgroundColor($r(app.color.white)).height(this.max).width(this.min)// 宽Text().backgroundColor($r(app.color.w…

盛铂科技 SLMF315频率综合器200MHz至15GHz 国产频综模块

在当今科技飞速发展的时代&#xff0c;射频技术在众多领域发挥着关键作用&#xff0c;从通信、雷达系统到科研实验&#xff0c;对频率综合器的性能要求日益严苛。以下是关于盛铂科技的 SLMF315 超低相位噪声频率综合器的介绍&#xff1a; SLMF315超低相位噪声0.2至15GHz频率综合…

wheel_legged_genesis 开源项目复现与问题记录

Reinforcement learning of wheel-legged robots based on Genesis System Requirements Ubuntu 20.04/22.04/24.04 python > 3.10 开始配置环境&#xff01; 点击releases后进入&#xff0c;下载对应最新版本的代码&#xff1a; 将下载后的代码包解压到你的自定义路径下&…

软考架构师笔记-计算机网络

1.9 计算机网络 OSI/RM 七层模型 物理层 二进制传输(中继器、集线器) (typedef) 数据链路层 传送以帧为单位的信息(网桥、交换机、网卡) 网络层 分组传输和路由选择(三层交换机、路由器)ARP/RARP/IGMP/ICMP/IP 传输层 端到端的连接(TCP/UDP)在前向纠错系统中&#xff0c;当接…

Qt6.8.2创建WebAssmebly项目使用FFmpeg资源

Qt6新出了WebAssmebly功能&#xff0c;可以将C写的软件到浏览器中运行&#xff0c;最近一段时间正在研究这方便内容&#xff0c;普通的控件响应都能实现&#xff0c;今天主要为大家分享如何将FFmpeg中的功能应用到浏览器中。 开发环境&#xff1a;window11&#xff0c;Qt6.8.2…

浅论数据库聚合:合理使用LambdaQueryWrapper和XML

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、数据库聚合替代内存计算&#xff08;关键优化&#xff09;二、批量处理优化四、区域特殊处理解耦五、防御性编程增强 前言 技术认知点&#xff1a;使用 XM…

DeepSeek大模型深度解析:架构、技术与应用全景

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。https://www.captainbed.cn/north 文章目录 一、大模型时代与DeepSeek的定位1.1 大模型发展历程回顾大模型发展历程时间轴&#xff08;20…

极狐GitLab 正式发布安全版本17.9.1、17.8.4、17.7.6

本分分享极狐GitLab 补丁版本 17.9.1、17.8.4、17.7.6 的详细内容。这几个版本包含重要的缺陷和安全修复代码&#xff0c;我们强烈建议所有私有化部署用户应该立即升级到上述的某一个版本。对于极狐GitLab SaaS&#xff0c;技术团队已经进行了升级&#xff0c;无需用户采取任何…

windows server 2019创建教程,新建存储池,新建虚拟磁盘,文件共享

1.网页地址Windows Server 2019 ISO镜像下载 - 我的MSDN 迅雷下载路径ed2k://|file|cn_windows_server_2019_updated_jan_2020_x64_dvd_4bbe2c37.iso|5608552448|39C663ABF26079240030395C7CB3F975|/ 开始创建 注意;虽然我们的镜像是windows server2019,但是虚拟机最高只能选w…

Linux系统之配置HAProxy负载均衡服务器

Linux系统之配置HAProxy负载均衡服务器 前言一、HAProxy介绍1.1 HAProxy简介1.2 主要特点1.3 使用场景二、本次实践介绍2.1 本次实践简介2.2 本次实践环境规划三、部署两台web服务器3.1 运行两个Docker容器3.2 编辑测试文件3.3 访问测试四、安装HAProxy4.1 更新系统软件源4.2 安…

使用Dockerfile打包java项目生成镜像部署到Linux_java项目打docker镜像的dockerfile

比起容器、镜像来说&#xff0c;Dockerfile 非常普通&#xff0c;它就是一个纯文本&#xff0c;里面记录了一系列的构建指令&#xff0c;比如选择基础镜像、拷贝文件、运行脚本等等&#xff0c;每个指令都会生成一个 Layer&#xff0c;而 Docker 顺序执行这个文件里的所有步骤&…

【TCP/IP协议栈】【传输层】端口号、套接字、多路复用/分解、网络字节序

参考资料&#xff1a; 前言&#xff1a; 总结&#xff1a; 【计算机网络】套接字&#xff08;应用层和传输层之间的接口&#xff09; 套接字是一个通用的通信接口抽象不仅限于TCP/IP协议族作为应用层和传输层之间的桥梁支持多种通信方式和协议族 套接字定义 在 TCP 或者 UDP…