秒杀测试案例 Java Redis Mysql

news/2024/4/24 12:34:51/文章来源:https://blog.csdn.net/hanhanhanxu/article/details/129196171

基于redis和MySQL乐观锁实现秒杀优惠券场景,一人一单。MySQL乐观锁改良控制不出现超卖和少卖问题,使用redisson分布式锁在用户维度加锁控制一人一单。

源码:https://github.com/hanhanhanxu/SeckillTest

文中图片看不清的地方可以鼠标右键->在新标签页中打开图片。我的个人网站:https://riun.xyz/ 所有内容优先个人网站发布。

1、场景

一个基本的秒杀场景

现有80抵100的优惠券要给用户送福利,限时秒杀100张先到先得。

这种秒杀场景一般要注意的点有:超卖、少卖、一人一单。

2、表结构:

create table voucher (id bigint(20) unsigned not null auto_increment primary key comment '主键',shop_id bigint(20) unsigned default null comment '商铺id',title varchar(255) not null comment '券标题',sub_title varchar(255) default null comment '副标题',rules varchar(1024) default null comment '使用规则',pay_value bigint(10) unsigned not null comment '支付金额,单位:分,例如:200,代表2元',actual_value bigint(10) unsigned not null comment '抵扣金额,单位:分,例如:100,代表1元',type tinyint(1) unsigned not null default '0' comment '券类型,0普通券,1秒杀券',status tinyint(1) unsigned not null default '1' comment '状态,1上架,2下架,3过期',create_time datetime not null default CURRENT_TIMESTAMP comment "创建时间",update_time datetime not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP comment "更新时间"
) default charset = utf8mb4 comment '优惠券表';create table seckill_vouscher (voucher_id bigint(20) unsigned not null primary key comment '主键,关联的优惠券的id',stock int(8) unsigned not null comment '库存',begin_time datetime not null default '0000-00-00 00:00:00' comment "生效时间",end_time datetime not null default '0000-00-00 00:00:00' comment "失效时间",create_time datetime not null default CURRENT_TIMESTAMP comment "创建时间",update_time datetime not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP comment "更新时间"
) default charset = utf8mb4 comment '秒杀券表,与优惠券是一对一关系';create table voucher_order (id bigint(20) not null primary key comment '主键',user_id bigint(20) unsigned not null comment '下单的用户id',voucher_id bigint(20) unsigned not null comment '购买的优惠券id',pay_type tinyint(1) unsigned not null default '1' comment '支付方式,1余额支付,2支付宝,3微信',status tinyint(1) unsigned not null default '1' comment '订单状态,1未支付,2已支付,3已核销,4已取消',create_time datetime not null default CURRENT_TIMESTAMP comment "创建时间,也即下单时间",pay_time datetime default null comment "支付时间",use_time datetime default null comment "核销时间",refund_time datetime default null comment "退款时间",update_time datetime not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP comment "更新时间"
) default charset = utf8mb4 comment '优惠券订单表';

voucher表是优惠券表,存放普通优惠券和秒杀优惠券。普通优惠券日常天天都有,秒杀券由于优惠力度较大,所以只在特定情况下上架一批。

当添加秒杀券时,会同时向voucher和seckill_vouscher表中添加信息。

用户购买一张优惠券时,会向voucher_order添加一条记录。

3、接口

3.1、添加优惠券

比较简单,只展示service层逻辑:

    /*** 新增一张秒杀券* @param voucher*/@Overridepublic void addSeckillVoucher(Voucher voucher) {//保存优惠券save(voucher);SeckillVoucher seckillVoucher = new SeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock());seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());//保存秒杀券seckillVoucherService.save(seckillVoucher);}

3.2、基于redis的分布式id生成器

用户抢到优惠券后一般会返回给用户一个订单id,用户拿着这个订单id去商家核销使用优惠券,所以订单id一般是要给长长的号码,又不能太有规律,所以一般不会选择MySQL的自增id。

基于这个场景下的id需要满足三个需求:1、不重复 2、不容易发现规律 3、由于我们要将其持久化到MySQL,所以为了保证效率,应该是趋势递增的。

这种东西在业内叫“发号器”,指的就是能生成分布式唯一Id的东西。有多种解决方案,这里使用基于redis自增实现的。

package xyz.riun.seckilltest.utils;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import xyz.riun.seckilltest.constants.RedisConstant;import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;/*** @Author:Hanxu* @url:https://riun.xyz/* @Date:2023/2/23 16:09* 基于redis的分布式id生成器* 最长可用到2090-01-19 03:14:07,每秒并发及每天最多获取id个数 4294967295(42亿)*/
@Component
public class RedisIdWorker {private static final long startTime = 1640995200;private static final int COUNT_BITS = 32;//private static final int startIncr = 0;@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 首位是符号位固定为0 后续31位是秒级时间戳 最后32位是自增数字** 时间戳计算方式是当前时间-起始时间,31位二进制位最大可表示2^31-1( 2147483647)。*          如果用2022.1.1 00:00:00( 1640995200)作为起始时间,最长可用到2090-01-19 03:14:07 (1640995200+2147483647 = 3788478847,秒级时间戳转为时间)** 自增数字在redis里从1开始自增,并发获取id时(同一秒内来获取id),前31位秒级时间戳可能相同,因此每秒支持获取4294967295(42亿)个不同的id(2^32-1 redis的自增首个获取到的值是1,因此这32个二进制位不可能全为0)*          但由于自增数字只占32个二进制位,所以假设一秒内获取了2^32-1次id,那么今天就无法再获取其他id了,因为再继续自增,32个二进制位存不下,位移时就会丢失数据,导致和之前生成的id重复。*          因此每天最多支持获取4294967295(42亿)个不同的id。要想改善这个问题可以增多自增数字占的位数,减少时间戳占的位数。** 这里key是 incr:bizKey:yyyy:MM:dd 所以每天都会有一个新的key去做自增,这样可以方便的统计每天获取了多少id,做其他业务上的统计。* @param bizKey 业务标识* @return 业务内的唯一id*/public long nextId(String bizKey) {//当前时间戳 - 起始时间戳LocalDateTime now = LocalDateTime.now();long nowTime = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowTime - startTime;//自增位 incr:voucher:20230223String formatTime = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));Long incrNum = stringRedisTemplate.opsForValue().increment(RedisConstant.INCR_PRE_KEY + bizKey + ":" + formatTime);//incrNum = startIncr + incrNum;//位移return (timestamp << COUNT_BITS) | (incrNum);}public static void main(String[] args) {long startTime = LocalDateTime.of(2022, 1, 1, 0, 0, 0).toEpochSecond(ZoneOffset.UTC);System.out.println(startTime);//1640995200long nowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);System.out.println(nowTime);System.out.println(nowTime - startTime);long timeMax = startTime + 2147483647;System.out.println(timeMax);LocalDateTime localDateTime = Instant.ofEpochSecond(timeMax).atZone(ZoneOffset.UTC).toLocalDateTime();System.out.println(localDateTime);}
}

3.3、抢购秒杀券

这个接口是最核心的,所以从controller层开始讲起:

    /*** 购买秒杀券,没有登录部分,所以直接传入userId模拟某个用户购买* @param voucherId* @return*/@PostMapping("seckill")public Long seckillVoucher(Long voucherId, Long userId) {long orderId = voucherOrderService.seckillVoucher(voucherId, userId);return orderId;}

seckillVoucher接口就是秒杀逻辑,这里将要秒杀的优惠券id:voucherId和用户id:userId传下去。

这个接口的逻辑应该是这样的:

1、先检查秒杀券能否购买,也就是常规的时间检查,库存检查。我们按照正常的可用购买往下走

2、秒杀券库存-1

3、优惠券订单添加该用户的购买记录,使用分布式id生成器作为订单id

4、一切成功,返回订单id

第一步是个查询sql,第2、3步是修改sql,而且2、3步应该是原子性的,所以我们要将其封装为一个事务。

①超卖问题

这里面最容易发生问题的点就是第1-2步,在高并发的情况下,很容易出现优惠券剩下1张,然后多个请求并发过来,同时做了第一步的判断,都确定有库存可以向下执行,然后都对库存做了-1操作,这样就容易出现超卖情况。

解决这种情况有很多方式,最常见的就是加锁。

悲观锁

如果加悲观锁,要切记需要使用分布式锁,而不能使用synchronized或者ReentrantLock这种JVM层面的锁。如果使用后者,那么在集群项目中多个请求并发打到多台机器上,每个机器上的线程都能获取它所在机器上面的锁,那么这个锁在服务层面就是失效的。

比如可以使用redis分布式锁,每次执行时先抢占锁,由于在外层加了锁就一定能保证数据的安全。不过这种每个线程过来都需要抢占一下锁,效率太低了,一般不用。

乐观锁

如果使用乐观锁,一般的做法是添加一个version版本号字段:

像update table set stock = stock - 1 and version = version + 1 where id = #{id} and version = #{version}; 这样。

但在库存场景下,stock本身就能作为版本号控制,因为我们是先查询库存当库存充足才去减库存的,也就是说我们是知道库存是多少的:

update table set stock = stock - 1 where id = #{id} and stock = #{stock};

但是乐观锁有一个问题,就是并发执行时一定会只有一个线程(请求)能够执行成功,其他并发的线程全部失败。就是说如果有100个人同时抢100个库存的秒杀券,他们刚好在同一时间执行,理论上来说100个人100张券应该改好抢完。但是如果同时执行到这条sql:update table set stock = stock - 1 where id = #{id} and stock = #{stock}; 由于数据库的行锁,只有一个人能够执行成功,抢到券。剩余的99个人执行时都是不满足stock = #{stock};的,他们都会失败。

这是我们不希望看到的,版本号形式的乐观锁失败率太高了。也就是会发生少卖问题

我们是100张相同的券,只要有券的库存,都希望人们能够抢到,所有我们只需要关系库存是否有就行了,因此可以稍微改变一下:

update table set stock = stock - 1 where id = #{id} and stock > 0;

这样100个人同时来抢100张券的话,他们就都能够执行成功了,都能够抢到了。

②一人一单

这种优惠力度的活动,一般是希望一人只能购买一单,让更多用户参与进来的。为了避免刷单,我们可以优化一下,在购买时做个检查:当前用户是否购买过,已购买过就无法购买了。

也就是在第二部之前添加一步:

1、先检查秒杀券能否购买,也就是常规的时间检查,库存检查。我们按照正常的可用购买往下走

2、当前用户是否购买过该秒杀券,没有购买过可以往下走

3、秒杀券库存-1

4、优惠券订单添加该用户的购买记录,使用分布式id生成器作为订单id

5、一切成功,返回订单id

现在试想有个人没有购买过秒杀券,然后想刷单购买多张秒杀券,也就是准备并发的用他自己的信息(userId)调我们的接口。当一个线程走到第二步时,没有购买过,向下走;此时还有若干个携带同样用户信息的线程也走到第2步,由于前面的线程还没有向数据库中插入订单信息,所以这若干个线程也能走过第二步,继续往下走。这些线程同时往下走,意味着同一个用户能够购买多张优惠券,这和我们一人一单的需求是不符的。

要解决这个问题,可以在第2步之前添加redis分布式锁,不过锁的粒度要特别小,锁当前用户。即redis锁的key是这样:lock:order:userId。

这样不同用户进来时就不会被锁互斥,只有同一个用户的多个请求并发进来时,才会被锁住。

解锁的时机比较重要,一定要等到事务提交之后才能解锁。否则可能出现:一个线程获取锁执行完,事务还没提交,然后先解锁了。这时另外一个线程过来,拿着同一个用户信息,加锁,前面的事务没有提交也就意味着数据库中没有用户的订单信息,也就是说还能通过第2步。

所以一定要等待事务执行完,然后再解锁。

③核心代码:

/*** @Author:Hanxu* @url:https://riun.xyz/* @Date:2023/2/23 18:52* 优惠券订单相关*/
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate IVoucherOrderService voucherOrderService;@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate RedissonClient redissonClient;/*** 购买一张秒杀券* @param voucherId* @return*/@Overridepublic long seckillVoucher(Long voucherId, Long userId) {//检测秒杀券是否可以正常购买SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);checkAvailable(seckillVoucher);//检测用户是否购买过//每个用户维度加锁 lock:order:userIdRLock rLock = redissonClient.getLock(RedisConstant.LOCK_PRE_KEY + RedisConstant.BIZ_ORDER + ":" + userId);boolean isLock = rLock.tryLock();if (!isLock) {log.error("可能存在刷单行为:userId:{} voucherId:{}", userId, voucherId);throw new RuntimeException("正在购买中,请勿重复提交!");}try {//使用代理执行对应方法,确保事务生效IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();long orderId = proxy.createVoucherOrder(voucherId, userId);return orderId;} finally {//一定要在事务提交后再解锁。// 若事务未提交时解锁,则可能voucherOrder还未写入,那么其他线程进入createVoucherOrder方法判断count=0,可继续向下执行,就不再是一人一单了。rLock.unlock();}}@Override@Transactional(propagation= Propagation.REQUIRED, isolation= Isolation.READ_COMMITTED)public long createVoucherOrder(Long voucherId, Long userId) {//查询用户是否已经购买 如果不加分布式锁,这里可能有多个线程同时满足条件,同时向下执行,那么一个用户就有可能通过抢单软件抢到多个优惠券// select count(*) from voucher_order where user_id = #{userId} and voucher_id = #{voucherId}int count = voucherOrderService.query().eq("user_id", userId).eq("voucher_id", voucherId).count();if (count > 0) {throw new RuntimeException("已经购买过!");}//减库存 stock > 0 控制不会超卖// update seckill_vouscher set stock = stock - 1 where voucher_id = #{voucherId} and stock > 0boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();if (!success) {throw new RuntimeException("库存不足!");}//添加订单信息long nextId = redisIdWorker.nextId(RedisConstant.BIZ_ORDER);VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(userId);voucherOrder.setId(nextId);voucherOrderService.save(voucherOrder);return nextId;}private void checkAvailable(SeckillVoucher seckillVoucher) {//其他判断如时间...if (seckillVoucher.getStock() < 1) {throw new RuntimeException("库存不足!");}}
}

4、源码

https://github.com/hanhanhanxu/SeckillTest

5、测试

5.1、添加秒杀券

postman调用接口:

向voucher和seckill_vouscher中添加一条记录,voucher_order中没有任何记录。

5.2、用户购买一张秒杀券

返回 155442610268274789

数据库中新增了一条订单记录,秒杀券库存由100变为99

5.3、多用户秒杀压测

jmeter中200个线程,每个线程循环100遍压测秒杀接口:

userId使用以下代码写入本地文件:

    public static void main(String[] args) throws IOException {//向文件中写数据FileWriter fileWriter = new FileWriter(new File("E:\\TestFloder\\Seckill\\userId.txt"));BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);for (int i = 100003; i < 100503; i++) {bufferedWriter.write(i + "\n");}bufferedWriter.close();fileWriter.close();}

文件中是从100003到100502

jmeter将拿着这些userId并发的去秒杀剩余99张秒杀券。

执行,查看汇总报告。rt 133,tps 1400,99.5%的异常是因为20000个请求只有99张券,异常是正确的:

查看数据库:

seckill_vouscher表中该秒杀券的库存为0,说明全部被买掉了。

voucher_order表中出现很多记录,执行select count(*) from voucher_order;查看结果为100,说明订单记录也是一张也不多一张也不少:

6、总结

秒杀场景下要注意的点一般有:超卖问题、少卖问题、一人一单、事务提交后再解锁。

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

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

相关文章

谷歌外推留痕,谷歌搜索留痕快速收录怎么做出来的?

本文主要分享谷歌搜索留痕的收录效果是怎么做的&#xff0c;让你对谷歌留痕技术有一个全面的了解。 本文由光算创作&#xff0c;有可能会被修改和剽窃&#xff0c;我们佛系对待这样的行为吧。 谷歌搜索留痕快速收录怎么做出来的&#xff1f; 答案是&#xff1a;通过谷歌蜘蛛…

C语言-结构体对齐

详细说明参考博客 (1条消息) C语言结构体对齐&#xff0c;超详细&#xff0c;超易懂_haozigegie的博客-CSDN博客 (1条消息) #pragma pack详解_OuJiang2021的博客-CSDN博客_#pragma pack 以下个人理解总结 出现结构体对齐考虑的根本原因就是&#xff1a;【数据存取执行效率】…

Openwrt中动态IPV6 防火墙的正确设置方法

环境&#xff1a;光猫桥接公网IPV6 问题&#xff1a;动态IPV6地址不知道怎么设置防火墙 解决办法&#xff1a;模糊匹配前缀&#xff0c;特定后缀 背景&#xff1a;将家中光猫桥接后&#xff0c;获得了公网的IPV6地址&#xff0c;可以从外部用IPV6访问家中的设备&#xff0c;但I…

【AI写作】 机器人流程自动化 介绍 - Robotic Process Automation (RPA) Introduction

写一篇文章介绍RPA技术,未来的发展。使用markdown格式,有3级索引,超过3000字。 某位大师说过的: 任何行业、任何部门都有大量的场景,涉及重复、有规则逻辑的工作,都可以用 RPA 开发一个软件机器人帮助完成。 文章目录 机器人过程自动化(RPA)简介RPA的定义RPA的好处Robo…

【centos7下部署mongodb】

一.安装环境 CentOS7MongoDB4.0.13正式版。 二.下载MongoDB 1.1 官网下载地址&#xff1a;https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.13.tgz 1.2 将压缩包通过xftp上传到服务器/opt目录&#xff0c;然后解压、改名 三. 配置环境变量及配置文件 3.1配置系…

有限差分法求解不可压NS方程

网上关于有限差分法解NS方程的程序实现不尽完备&#xff0c;这里是一些补充注解 现有的优秀资料 理论向 【1】如何从物理意义上理解NS方程&#xff1f; - 知乎 【2】NS方程数值解法&#xff1a;投影法的简单应用 - 知乎 【3】[计算流体力学] NS 方程的速度压力法差分格式_…

pytorch1.2.0+python3.6

一、说明 pytorch1.2.0python3.6CUDA10.0cudnn7.4.1.5 二、步骤 在conda中创建一个新的虚拟环境 查看一下自己的所有环境 激活虚拟环境 conda activate torch1.2.0 关于cuda和cudnn 1、查看自己电脑系统是10.2版本 http://链接&#xff1a;https://pan.baidu.com/s/1v5cN6…

自学前端,你必须要掌握的3种定时任务

当你看到这篇博客的时候&#xff0c;一定会和狗哥结下不解之缘&#xff0c;因为狗哥的博客里不仅仅有代码&#xff0c;还有很多代码之外的东西&#xff0c;如果你可以看到最底部&#xff0c;看到投票环节&#xff0c;我相信你一定感觉到了&#xff0c;狗哥的真诚&#xff0c;狗…

DateTimeParseException

前端请求为字符串的时间格式2023-02-16 19:19:51&#xff0c;服务端用LocalDateTime类型接收时报解析异常java.time.format.DateTimeParseException: Text 2023-02-16 19:19:51 could not be parsed at index 10方法一&#xff1a;JsonFormat(shape Shape.STRING, pattern &q…

Redis 主从复制-服务器搭建【薪火相传/哨兵模式】

Redis 安装参考文章&#xff1a;Centos7 安装并启动 Redis-6.2.6 注意&#xff1a;本篇文章操作&#xff0c;不能在 静态IP地址 下操作&#xff0c;必须是 动态IP地址&#xff0c;否则最后主从服务器配置不成功&#xff01; 管道符查看所有redis进程&#xff1a;ps -ef|grep re…

Linux->父子进程初识和进程状态

目录 前言&#xff1a; 1. 父子进程创建 2. 进程状态 R(running)状态&#xff1a; S(sleep)状态&#xff1a; D(disk sleep)状态&#xff1a; T(stopped)状态&#xff1a; X(dead)和Z(zombie)状态&#xff1a; 孤儿进程&#xff1a; 前言&#xff1a; 本篇主要讲解关…

同事每天早下班,原来是用了这8个开发工具

引言 工欲善其事必先利其器&#xff0c;说的就是工匠要想更加高效的做事情&#xff0c;就得先将工具变得锋利。那么对于程序员来说同样也是如此&#xff0c;如果要想每天早点下班&#xff0c;就必须借助于一些开发工具来提高自己的工作效率&#xff0c;今天慕枫就给大家总结一…

365智能云打印怎么样?365小票无线订单打印机好用吗?

365智能云打印怎么样&#xff1f;365智能云打印是有赞官方首推的订单小票打印机&#xff0c;荣获2016年有赞最佳硬件服务商。可以实现远程云打印&#xff0c;无需连接电脑&#xff0c;只需通过GPRS流量或者WIFI即可连接&#xff0c;不受地理位置和距离限制。365小票无线订单打印…

关于高并发场景和进程线程协程的一些总结

1、IO复用和线程池哪个好&#xff1f;应用场景&#xff1f; IO复用就是一个线程处理多个客户端连接。如果自己实现的话&#xff0c;就是要不断轮询每个客户端连接&#xff0c;看看有没有事件发生&#xff08;数据到达&#xff09;&#xff0c;即使可以用非阻塞的read函数&…

Python编写GUI界面,实现小说下载器

嗨害大家好鸭&#xff01;我是小熊猫~思路一、数据来源分析二. 代码实现步骤代码实现一、单章小说下载二、整本小说下载三、多线程采集四、采集排行榜所有小说五、搜索小说功能六、GUI界面<center>**&#x1f447;问题解答 源码获取 技术交流 抱团学习请联系&#x1f…

金仓数据库安装

一、麒麟操作系统安装金仓数据库 操作系统 DISTRIB_IDKylin DISTRIB_RELEASEV10 DISTRIB_CODENAMEjuniper 按照安装文档的步骤安装&#xff0c;记得记住设置的数据库的用户名、密码 二、window安装连接数据库的工具软件 三、jdbc连接数据库 &#xff08;1&#xff09;连接工…

错误记录:py2neo.errors.ProtocolError: Cannot decode response content as JSON

py2neo.errors.ProtocolError: Cannot decode response content as JSON 原因&#xff1a;目前不清楚 解决方法&#xff1a;进入\lib\site-packages\py2neo\database.py中更改graph_name为neo4j 程序正常运行

Linux命令及CPU占用过高的定位分析思路

一、vim命令不要使用vim打开大文件&#xff0c;vim会一次性读取所有内容到内存&#xff0c;容易造成宿主机内存溢出。 打开文件前&#xff0c;可以使用du -h命令查看文件大小。一般&#xff0c;100MB以下为宜。1、普通模式j 向下30j 向下移动30行k 向上h 向左l 向右0 到行首^ 到…

分阶段构建golang运行环境Dockerfile镜像

在开始这项工作之前大家可以先去看一下docker官方给出关于空镜像scratch的说明&#xff0c;采用官方简单的一句话就是&#xff1a;scratch是一个明确的空图像&#xff0c;特别是对于“从头开始”构建图像。分阶段构建镜像就会用到scratch这个空镜像&#xff0c;这样的好处是可以…

Vulnhub靶场----7、DC-7

文章目录一、环境搭建二、渗透流程三、思路总结一、环境搭建 DC-7下载地址&#xff1a;https://download.vulnhub.com/dc/DC-7.zip kali&#xff1a;192.168.144.148 DC-7&#xff1a;192.168.144.155 二、渗透流程 nmap -T5 -A -p- -sV -sT 192.168.144.155思路&#xff1a; …