秒杀测试案例 Java Redis Mysql

news2025/7/29 7:07:47

基于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
     */
    @Override
    public 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;

    @Autowired
    private 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:20230223
        String 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);
        //1640995200

        long 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 {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private IVoucherOrderService voucherOrderService;

    @Resource
    private RedisIdWorker redisIdWorker;
    @Resource
    private RedissonClient redissonClient;

    /**
     * 购买一张秒杀券
     * @param voucherId
     * @return
     */
    @Override
    public long seckillVoucher(Long voucherId, Long userId) {
        //检测秒杀券是否可以正常购买
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        checkAvailable(seckillVoucher);

        //检测用户是否购买过
        //每个用户维度加锁 lock:order:userId
        RLock 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 > 0
        boolean 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.coloradmin.cn/o/367909.html

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

相关文章

01--微信小程序介绍

1、什么是微信小程序微信小程序&#xff0c;小程序的一种&#xff0c;英文名Wechat Mini Program&#xff0c;是一种不需要下载安装即可使用的应用&#xff0c;它实现了应用“触手可及”的梦想&#xff0c;用户扫一扫或搜一下即可打开应用。微信小程序是一种不需要下载安装即可…

python--排序总结

1.快速排序 a.原理 快速排序的基本思想是在待排序的 n 个元素中任取一个元素&#xff08;通常取第一个元素&#xff09;作为基准&#xff0c;把该元素放人最终位置后&#xff0c;整个数据序列被基准分割成两个子序列&#xff0c;所有小于基准的元素放置在前子序列中&#xff0…

Wireshark抓包

Wireshark 1 抓包时间显示格式 2 界面显示列设置 3 protocol协议解析 4 过滤器 tcp.port&#xff1a;TCP端口tcp.dstport&#xff1a;TCP目的端口tcp.srcport&#xff1a;TCP源端口udp.port&#xff1a;UDP端口udp.dstport&#xff1a;UDP目的端口udp.srcport&#xff1a;UDP…

HIVE --- 窗口函数

目录 简介 概念 数据准备 聚合函数over partition by子句 order by子句 window子句 窗口函数中的序列函数 ntile rank、dense_rank、row_number LAG、LEAD first_value、last_value 简介 本文主要介绍hive中的窗口函数.hive中的窗口函数和sql中的窗口函数相类似,都…

批量操作文件功能-课后程序(JAVA基础案例教程-黑马程序员编著-第七章-课后作业)

【实验7-1】 批量操作文件功能 任务介绍 1&#xff0e;任务描述 在日常工作中&#xff0c;经常会遇到批量操作系统文件的事情&#xff0c;通常情况下&#xff0c;只能手动重复的完成批量文件的操作&#xff0c;这样很是费时费力。本案例要求编写一个文件管理器&#xff0c;…

(二十六)、项目打包H5+微信小程序+app【uniapp+uinicloud多用户社区博客实战项目(完整开发文档-从零到完整项目)】

1&#xff0c;打包H5发布上线到前端页面托管 1.1 上传所有DBschema和云函数 上传所有DBschema到云服务空间 上传所有云函数到云服务空间 1.2 Hbuilderx基础配置 点击manifest.json文件----web配置&#xff1a; 点击发行—H5 1.3 H5打包完成 使用vscode中的live serve…

【C语言进阶】动态内存管理详解与常见动态内存错误以及柔性数组使用与介绍

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;C语言进阶 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录1.动态内存1.1 概述…

运营级手机直播平台源码 短视频直播带货APP源码

短视频直播带货APP源码 全开源原生直播APP源码 前端&#xff1a;原生APP 安卓端&#xff1a;Java 苹果端&#xff1a;OC 后台&#xff1a;PHP 数据库&#xff1a;Mysql 技术框架&#xff1a;Thinkphp5.1 系统特色功能包括&#xff1a;礼物系统&#xff1b;提现方式&#…

2月 公司来一00后卷王,我们这帮老油条真干不过.....

都说00后躺平了&#xff0c;但是有一说一&#xff0c;该卷的还是卷。这不&#xff0c;我们公司来了个00后&#xff0c;工作没两年&#xff0c;跳槽到我们公司起薪18K&#xff0c;都快接近我了。后来才知道人家是个卷王&#xff0c;从早干到晚就差搬张床到工位睡觉了。 2023年春…

【算法】——并查集

作者&#xff1a;指针不指南吗 专栏&#xff1a;算法篇 &#x1f43e;或许会很慢&#xff0c;但是不可以停下&#x1f43e; 文章目录1.思想2.模板3.应用3.1 合并集合3.2 连通块中点的数量1.思想 并查集是一种树型的数据结构&#xff0c;用于处理一些不相交集合的合并及查询问题…

ESP32 Arduino EspNow点对点双向通讯

ESP32 Arduino EspNow点对点双向通讯✨本案例分别采用esp32和esp32C3之间点对点单播无线通讯方式。 &#x1f33f;esp32开发板 &#x1f33e;esp32c3开发板 &#x1f527;所需库(需要自行导入到Arduino IDE library文件夹中&#xff0c;无法在IDE 管理库界面搜索下载到该库)&am…

【GO】k8s 管理系统项目[前端部分16–前端布局]

【GO】k8s 管理系统项目[前端部分–前端布局] 1. 前端布局 2. Layout 2.1 layout src/layout/Layout.vue <template><div class"common-layout"><el-container><el-side width"200">Aside</el-side><el-container>…

哪些骨传导运动蓝牙耳机好,分享几款不错的骨传导耳机

​骨传导耳机在运动中有很多优势&#xff0c;它是一款不入耳的耳机&#xff0c;适合在跑步、骑行、爬山等运动中使用&#xff0c;如果你是一个爱运动的人&#xff0c;骨传导耳机是不错的选择。由于骨传导技术不需要塞入耳朵中就能听到音乐&#xff0c;所以不会产生任何不适感。…

计算机图形学期末复习笔记

计算机图形学 ch1绪论 1.1计算机图形学及其概念 计算机图形学&#xff08;Computer Graphics&#xff09;是研究怎样利用计算机来生成、处理和显示图形的原理、方法和技术的学科。 cg研究对象是图形 图形的要素 几何&#xff08;轮廓、点、线、面&#xff09;非几何要素&…

前向传播与反向传播参数的更新方式(略高于高中数学水平)(附公式、代码)

前向传播与反向传播意义及其参数的更新方式 文章目录前向传播与反向传播意义及其参数的更新方式一、前言二、前反向传播的作用三、前向传播四、反向传播代码一、前言 因为本身非科班出身&#xff0c;数学又学的很差&#xff0c;一直都是傻瓜式地用tensorflow和pytorch搭网络。…

【容器】学习docker容器网络

在前面讲解容器基础时&#xff0c;曾经提到过一个 Linux 容器能看见的“网络栈”&#xff0c;实际上是被隔离在它自己的 Network Namespace 当中的。 而所谓“网络栈”&#xff0c;就包括了&#xff1a;网卡&#xff08;Network Interface&#xff09;、回环设备&#xff08;L…

Mac mini 外接移动硬盘无法写入或者无法显示的解决方法

文章目录1. 背景2. 让NTFS格式的移动硬盘正常读写方法3. 打开“启动安全性实用工具”4. 更改“安全启动”设置1. 背景 刚买mac min&#xff08;2023年2月3日&#xff09;不久&#xff0c;发现macOS的玩起来并不容易&#xff0c;勇习惯了windows系统的习惯&#xff0c;感觉 mac…

【storybook】你需要一款能在独立环境下开发组件并生成可视化控件文档的框架吗?(二)

storybook回顾继续说说用法配置文件介绍回顾 上篇博客地址&#xff1a; https://blog.csdn.net/tuzi007a/article/details/129192502说了部分用法。 继续说说用法 配置文件介绍 开发环境的配置都在.storybook目录中&#xff0c;里面包含了2个文件 main.js preview.js先看m…

STM32 触摸屏移植GUI控制控件

目录 1、emWin 支持指针输入设备。 2、 模拟触摸屏驱动 3、实现触摸屏的流程 3.1 实现硬件函数 3.2 实现对GUI_TOUCH_Exec()的定期调用 3.3 使用上一步确定的值&#xff0c;在初始化函数LCD_X_Config&#xff08;&#xff09;当中添加对GUI_TOUCH_Calibrate()的调用 4、…

Kubernetes入门教程 --- 使用二进制安装

Kubernetes入门教程 --- 使用二进制安装1. Introduction1.1 架构图1.2 关键字介绍1.3 简述2. 使用Kubeadm Install2.1 申请三个虚拟环境2.2 准备安装环境2.3 配置yum源2.4 安装Docker2.4.1 配置docker加速器并修改成k8s驱动2.5 时间同步2.6 安装组件3. 基础知识3.1 Pod3.2 控制…