Redission分布式锁 - 抢课系统

news2025/1/20 6:03:55

使用Redission分布式锁与Kafka消息队列,实现学生抢课系统(高并发秒杀场景)。

目录

  • 一、思路
    • 1.为频繁访问的信息设置缓存
      • (1)登陆
      • (2)课程任务信息
      • (3)用户抢课记录
    • 2.消息队列和分布式锁
      • (1)抢课消息队列
      • (2)锁定缓存抢夺
      • (3)数量缓存操作
  • 二、具体流程
    • 1.抢课任务设置
    • 2.用户抢课
  • 三、实体类表
  • 四、核心代码
    • 1.消费端
    • 2.数量缓存操作逻辑
  • 五、两种分布式锁:
    • 1.基于redis命令的分布式锁
      • a.加锁
      • b.解锁
      • c.局限性
    • 2. redission分布式锁
  • 参考文章

一、思路

1.为频繁访问的信息设置缓存

后台设置抢课任务,配置抢课时间范围。一般情况下,用户会在抢课时间开始前至抢课前期一段的时间集中访问系统。

(1)登陆

系统在登陆时会查询数据库返回用户信息,所以需要对登陆接口进行改造,将用户信息(比如姓名、年级、班级、联系方式等)保存至缓存中。当用户信息发生变更时,才将信息从缓存中清除。
用户登陆页面

(2)课程任务信息

用户登陆进入系统后,进入到抢课任务模块对应的抢课任务中,进行课程选择。在抢课任务期间,用户会为频繁的访问该模块信息(抢课任务、关联课程、课程库存等),后台在进行信息校验时也会用到以上数据。
抢课任务详情
选课列表

(3)用户抢课记录

在抢课期间,用户抢课退课操作和查询比较频繁,可将该数据保存至缓存,方便查询。
已选课程列表

2.消息队列和分布式锁

为Kafka的抢课消息队列设置了一个主题五个分区,可处理大量并发抢课消息的情况。然而在每个分区内消息是有序的;不同分区中的消息无序,会出现多个进程同时进行的情况,而多个进程必须以互斥的方式独占共享资源(课程库存)。

为保证每一课程库存操作的独占性,为课程库存设置了锁定缓存(LockKey)数量缓存(StockKey)
注意:当消息抢夺到锁定缓存时,才可对数量缓存进行扣除(-1)的操作。

(1)抢课消息队列

对用户抢课数据进行校验(是否符合年级、已选课程日期冲突炖、课程班级人数限制、课程库存余量等),校验通过后将请求数据发送至Kafka抢课消息队列。

(2)锁定缓存抢夺

此处使用了Redission分布式锁,当同时有多个用户发送同一课程的消息时,消费端接收到消息在5秒内尝试获取锁定缓存(LockKey),若获取成功加锁30秒,否则失效。

(3)数量缓存操作

获取锁定缓存(LockKey)成功后,查询数量缓存(StockKey)。此时需要对各项业务数据再次进行校验,因为在数据进入消息队列前进程仍是并发的,可能会出现数据已变动的情况。在满足抢课条件后,取出数量缓存(StockKey)库存进行数量减一的操作,操作完毕最终释放锁定缓存(LockKey)

二、具体流程

1.抢课任务设置

(1)后台管理人员配置抢课任务信息(开始时间、结束时间、课程、课程时间等)。
(2)课程任务正式发布,保存信息(任务、关联课程、自定义课程可选人数等)到缓存;当任务取消发布时,需要将对应的缓存删除。

2.用户抢课

(1)首次登陆查询用户信息,并保存至缓存。
(2)进入抢课任务信息,查看可选课程列表。
(3)点击抢课按钮发送请求。
(4)选课成功:保存学生对应任务已选课程集合到缓存;更新任务课程库存数量等缓存信息。
(5)退课:更新学生已选课程缓存;清除任务课程库存数量缓存。

三、实体类表

此处列举部分核心数据库表设计。

1.课程表

id课程名称课程编号课程教室课程简介教师id教师名称
1舞蹈兴趣班C00011号楼6楼舞蹈教室面向0基础学生10001王老师
2画图兴趣班C00021号楼2楼美术教室面向0基础学生10002李老师
3音乐兴趣班C00033号楼1楼音乐教室面向0基础学生10003陈老师

2.选课任务表

id任务名称可选年级id集合可选班级id集合学生可选课程总数开始时间结束时间发布状态发布时间任务状态
12023年秋季选课2019级id,2020级id2020级-1班id,2020级-2班id,2019级-3班id22023-08-01 09:00:002023-08-07 20:00:00已发布2023-07-20 09:00:00已结束
22024年春季选课2019级id,2020级id32024-01-30 09:00:002024-01-07 20:00:00已发布2024-01-20 09:00:00进行中

3.选课任务课程关联表

id任务id课程id课程可选人数开始日期结束日期课程表json
111502023-09-202023-12-30[{“name”:“周一”, “section”:[{“name”:“第5节”,“state”:“1”}]},{“name”:“周二”, “section”:[{“name”:“第3节”,“state”:“1”}]}]
212502023-09-202023-12-30[{“name”:“周二”, “section”:[{“name”:“第3节”,“state”:“1”}]},{“name”:“周四”, “section”:[{“name”:“第5节”,“state”:“1”}]}]
313502023-09-202023-12-30[{“name”:“周三”, “section”:[{“name”:“第5节”,“state”:“1”}]},{“name”:“周五”, “section”:[{“name”:“第5节”,“state”:“1”}]}]
421502024-03-012024-05-30[{“name”:“周一”, “section”:[{“name”:“第5节”,“state”:“1”}]},{“name”:“周二”, “section”:[{“name”:“第3节”,“state”:“1”}]}]
522502024-03-012024-05-30[{“name”:“周二”, “section”:[{“name”:“第3节”,“state”:“1”}]},{“name”:“周四”, “section”:[{“name”:“第5节”,“state”:“1”}]}]
633502024-03-012024-05-30[{“name”:“周三”, “section”:[{“name”:“第5节”,“state”:“1”}]},{“name”:“周五”, “section”:[{“name”:“第5节”,“state”:“1”}]}]

4.学生选课关联表

id任务id课程id学生id选课状态老师帮选表示选课时间
1111已选课2023-08-01 09:01:00
2112已取消2023-08-01 09:01:01

四、核心代码

1.消费端

(1)Kafka配置:主题、分区初始化
(2)用户抢课数据初步通过校验,发送到消息队列中的处理逻辑。

@Component
@Slf4j
public class KafkaConsumer {
/**
     * 初始化学生选课主题分区 5个
     * 通过注入一个 NewTopic 类型的 Bean 来创建 topic,如果 topic 已存在,则会忽略。
     */
    @Bean
    public NewTopic courseSelectionBatchTopic() {
        log.info("创建学生选课主题courseSelectionBatchTopic : szxy_oa_course_selection_student_add_topic,分区:5,副本数:1 >>>>>>>>>>>>>>>>>>>>>>>>>>>>> ");
        NewTopic newTopic = new NewTopic(OaConstant.COURSE_SELECTION_STUDENT_ADD_TOPIC, 5, (short) 1);
        log.info("newTopic:topicName:{},分区: {} >>>>>>>>>>>>>>>>>>>>>>>>>>>> ", newTopic.name(), newTopic.numPartitions());
        return newTopic;
    }
 /**
     
     *添加学生选课主题消息
     */
    @KafkaListener(topics = OaConstant.COURSE_SELECTION_STUDENT_ADD_TOPIC,groupId = KafkaProducer.TOPIC_GROUP)
    public void courseSelectionStudentAddMsg(ConsumerRecord<?, ?> record, Acknowledgment ack, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
        log.info("COURSE_SELECTION_STUDENT_ADD_TOPIC-学生选课队列消费端 topic:{}, 收到消息>>>>>>>>>>>>>>>>>", topic);
        try {
            Optional message = Optional.ofNullable(record.value());
            if (message.isPresent()) {
                Object msg = message.get();

                // 先判断消息是否已经已经消费过,5s
                String fullKey2 = redisLockUtil.getFullKey(COURSE_SELECTION_STUDENT_CONSUME_LOCK_PREFIX , String.valueOf(msg));
                if(redisLockUtil.getLock(fullKey2 , 5000)){

                    // 获得学生选课入参
                    CourseSelectionStudentParam param = objectMapper.readValue(String.valueOf(msg), CourseSelectionStudentParam.class);

                    // 选课任务课程 key 任务id + 任务课程id
                    Long taskId = param.getTaskId();    // 选课任务id
                    Long taskcourseId = param.getTaskcourseId();    // 选课任务课程id
                    String key = taskId + "::" + taskcourseId;

                    // 获得课程容量库存锁:任务id + 任务课程id,才可以操作库存缓存
                    String fullKey = redisLockUtil.getFullKey(COURSE_SELECTION_STUDENT_LOCK_PREFIX, key);
                    final RLock lock = redissonClient.getLock(fullKey);
                    // 尝试抢【课程容量库存锁】锁时间调整 5s
                    boolean bool = lock.tryLock(5, 30, TimeUnit.SECONDS);   // 5s内尝试加锁,加锁成功后30s失效
                    if (bool) {
                        tCourseSelectionStudentService.handleCourseSelectionStudent(param, lock);
                        log.info("courseSelectionStudentAddMsg 取得更新库存锁,消费了: Topic:" + topic + ",Message:" + String.valueOf(msg));
                    }
                }else {
                    log.info("courseSelectionStudentAddMsg 已经被消费: Topic:" + topic + ",Message:" + String.valueOf(msg));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("解析 <"+OaConstant.COURSE_SELECTION_STUDENT_ADD_MESSAGE_KAFKA_TOPIC+"> 数据异常");
        } finally {
            ack.acknowledge();
        }
        log.info("COURSE_SELECTION_STUDENT_ADD_TOPIC-学生选课队列消费端 消费结束 >>>>>>>>>>>>>>>>>");
    }
}

2.数量缓存操作逻辑

通过Redission分布式锁抢夺锁定缓存后的处理逻辑。

 /**
     * 处理学生抢课消息
     * 入库并修改库存缓存
     * 编辑学生选课列表redis
     * 通过验证后发送消费消息,需要再次校验redis是否已选课程
     * 加入锁对象
     */
    @Override
    public void handleCourseSelectionStudent(CourseSelectionStudentParam param, RLock lock) {

        Date current = new Date();
        // 库存余量判断 -> 查询库存key
        String stockKey = COURSE_SELECTION_STUDENT_STOCK_PREFIX + param.getTaskId() + "::" + param.getTaskcourseId();
        String stockNumStr = redisTemplate.opsForValue().get(stockKey);
        log.info("handleCourseSelectionStudent-学生抢课添加处理,开始>>>>>>>>>>>>>>>>>>>>>>>>>>> key={},stockNum={},stuIndentityId={}", stockKey, stockNumStr, param.getStuIdentityId());

        if (StringUtils.isBlank(stockNumStr)) {
            log.error("handleCourseSelectionStudent-学生抢课添加处理,未查询到库存, key={},stockNum={}", stockKey, stockNumStr);
            // 释放锁
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                // 是当前执行线程的锁
                lock.unlock();
            }
            return;
        }
        int num = Integer.parseInt(stockNumStr) - 1;
        if (num < 0) {
            log.error("handleCourseSelectionStudent-学生抢课添加处理,库存余量不足,key={},stockNum={},studentNum={}", stockKey, stockNumStr, 1);
            // 释放锁
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                // 是当前执行线程的锁
                lock.unlock();
            }
            return;
        }

        // 入库前再次校验是否已选该课程
        // 查询对该课程的选课结果 redis key = taskcourseId + classId + stuIdentityId,防止多次重复点击选课
        String couseSelectionStudentPkIdKey2 = CourseSelectionConstants.COURSE_SELECTION_CLASS_STUDENT_PKID_PREFIX + param.getTaskcourseId() + ":" + param.getClassId() + ":" + param.getStuIdentityId();
        String courseSelectionPkId = redisTemplate.opsForValue().get(couseSelectionStudentPkIdKey2);
        if (StringUtils.isNotBlank(courseSelectionPkId)) {
            log.error("handleCourseSelectionStudent-学生抢课添加处理,已存在学生选课记录,key={},taskcourseId={},stuIndentityId={}", couseSelectionStudentPkIdKey2, param.getTaskcourseId(), param.getStuIdentityId());
            // 释放锁
//            redisLockUtil.delLock(redisLockUtil.getFullKey(COURSE_SELECTION_STUDENT_LOCK_PREFIX, param.getTaskId() + "::" + param.getTaskcourseId()));
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                // 是当前执行线程的锁
                lock.unlock();
            }
            return;
        }

        // 入库前再次校验,所属班级是否有剩余人数
        // 查询是否超过班级可选限制
        if (!"0".equals(param.getClassLimit())) {
            // 校验是否超过班级可选人数,班级可选人数配置从taskcourse中获取
            int classSelectNum = 0;
            String classLimitKey = CourseSelectionConstants.COURSE_SELECTION_TASKCOURSE_CLASSLIMIT_PREFIX + param.getOrgId() + ":" + param.getTaskId() + ":" + param.getTaskcourseId() + ":" + param.getClassId();
            String classLimitStr = redisTemplate.opsForValue().get(classLimitKey);
            if (StringUtils.isNotBlank(classLimitStr)) {
                classSelectNum = Integer.parseInt(classLimitStr);
            }


        // 校验是否超过任务选课数量上限
        // 查询学生当前任务的课程记录列表(已选 + 已取消) redis中获取
        String stuCourseListKey3 = CourseSelectionConstants.COURSE_SELECTION_STUDENT_COURSELIST_PREFIX + param.getOrgId()  + ":" + param.getTaskId() + ":" + param.getStuIdentityId();
        String stuCourseStr3 = redisTemplate.opsForValue().get(stuCourseListKey3);
        List<CourseSelectionTaskcourseVo> selectionCourseList = new ArrayList<>();
        if (StringUtils.isNotBlank(stuCourseStr3)) {
            selectionCourseList = JSONUtil.toList(JSONUtil.toJsonStr(stuCourseStr3), CourseSelectionTaskcourseVo.class);
            if (CollectionUtil.isNotEmpty(selectionCourseList)) {
                // 筛选出是已选状态的课程列表
                selectionCourseList = selectionCourseList.stream().filter(c -> "1".equals(c.getSelectionStatus())).collect(Collectors.toList());
            }
        }
        if (CollectionUtil.isNotEmpty(selectionCourseList)) {
            // 筛选学生已选当前任务的课程数量、是否存在当前课程的选课记录、是否存在重复上课时间
            int taskCourseNum = 0;  // 学生在该任务中已选的任务数量
            for (CourseSelectionTaskcourseVo taskcourseVo : selectionCourseList) {
                String taskId = taskcourseVo.getTaskId();
                if (taskId.equals(param.getTaskId().toString())) {
                    taskCourseNum ++;
                }
            }

            if (taskCourseNum >= param.getMaxSelectNum()) {
                log.error("handleCourseSelectionStudent-当前选课任务选课数量已达到上限,任务id:{},学生id:{}", param.getTaskId(), param.getStuIdentityId());
                return;
            }
        }

        // 添加选课信息
        TCourseSelectionStudent selectionStudent = new TCourseSelectionStudentDTO();
        BeanUtil.copyProperties(param, selectionStudent);
        selectionStudent.setSelectionStatus(1);     //1 选中
        selectionStudent.setDelFlag(0);
        selectionStudent.setCreatorId(UserHandle.getUserId());  // 学生id/教师id
        selectionStudent.setCreateTime(new Date());
        getBaseMapper().insert(selectionStudent);

        // 修改redis缓存库存,释放锁
        redisTemplate.opsForValue().decrement(stockKey, 1);     // 库存量-1

        // 添加选课结果 redis key = taskcourseId + classId + stuIdentityId,value = pkId,保存30天,退课时删除。
        String couseSelectionStudentPkIdKey = CourseSelectionConstants.COURSE_SELECTION_CLASS_STUDENT_PKID_PREFIX + selectionStudent.getTaskcourseId() + ":" + selectionStudent.getClassId() + ":" + selectionStudent.getStuIdentityId();
        redisTemplate.opsForValue().set(couseSelectionStudentPkIdKey, selectionStudent.getPkId().toString(), 30, TimeUnit.DAYS);

        // 构造个人选课记录缓存
        String stuCourseListKey = CourseSelectionConstants.COURSE_SELECTION_STUDENT_COURSELIST_PREFIX + selectionStudent.getOrgId() + ":" + selectionStudent.getTaskId() + ":" + selectionStudent.getStuIdentityId();
        String stuCourseStr = redisTemplate.opsForValue().get(stuCourseListKey);
        List<CourseSelectionTaskcourseVo> taskcourseList = new ArrayList<>();
        if (StringUtils.isNotBlank(stuCourseStr)) {
            taskcourseList = JSONUtil.toList(JSONUtil.toJsonStr(stuCourseStr), CourseSelectionTaskcourseVo.class);
        }
        // 构造学生选课记录列表对象
        CourseSelectionTaskcourseVo courseVo = new CourseSelectionTaskcourseVo();
        courseVo.setTaskId(param.getTaskId().toString());
        courseVo.setPkId(param.getTaskcourseId().toString());
        courseVo.setCourseName(param.getTaskcourseName());
        courseVo.setCourseNo(param.getTaskcourseNo());
        courseVo.setWeekSection(param.getWeekSection());
        courseVo.setCourseNumber(param.getCourseNumber());
        courseVo.setCourseSelectionStudentId(selectionStudent.getPkId().toString());     // 学生选课记录id
        courseVo.setSelectionStatus("1");   //设置为1已选
        courseVo.setTeacherSelection(param.getTeacherSelection().toString());
        courseVo.setCreateTime(current);
        taskcourseList.add(courseVo);

        // 保存个人选课记录30天
        redisTemplate.opsForValue().set(stuCourseListKey, JSONUtil.toJsonStr(taskcourseList), 30, TimeUnit.DAYS);

        // 最后才释放锁
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            // 是当前执行线程的锁
            lock.unlock();
        }
    }

五、两种分布式锁:

1.基于redis命令的分布式锁

a.加锁

1)setnx(lockKey, expireTime)
set if not exist,如果不存在就设置锁。原子方法,返回1代表成功存入锁。

2)get(lockKey)
获取值oldExpireTime,与当前系统时间比较,判断锁是否已超时。若已超时允许其他请求重新获取。

3)getset(lockKey, newValue)
返回当前锁的过期时间。如果与(2)的oldExpireTime相等,说明获取到了锁;否则失败。

b.解锁

delete(lockKey)
在锁定时间内完成操作,主动调用delete解锁

c.局限性

多服务器并发进行getset会出现过期时间覆盖问题。
锁不具备拥有者表示,任何客户端都可解锁。
不支持阻塞等待和重入。

2. redission分布式锁

(1)lua脚本:原子性执行加锁、解锁、广播解锁消息。
(2)可重入锁:通过redis的hash结构实现,内含一对键值对。锁名为hash的名称,UUID+线程ID作为hash的key,锁被重入的次数为value。
(3)等待锁:订阅解锁消息,获取解锁时间,阻塞待唤醒或者超时。

参考文章

Redis分布式锁-这一篇全了解(Redission实现分布式锁完美方案)
redis分布式锁RedissonLock的实现细节
Redis 分布式锁实现的一些方法 setnx()、get()、getset()

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

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

相关文章

文字转成活码的3步操作,手机扫码即可查看文本信息

现在经常会通过二维码的方式来传递通知的文字信息&#xff0c;只需要分享文字生成二维码的图片到微信群或者印刷出来&#xff0c;其他人就可以通过扫码来查看文字内容&#xff0c;有利于其他人更快速的获取信息。 目前文本静态码无法通过微信来扫码展示&#xff0c;那么想要解…

【unity小技巧】减少Unity中的构建打包大小

文章目录 正常默认打包查看编辑器打包日志压缩图片压缩网格模型压缩贴图压缩音频文件只打64位包最终大小完结 正常默认打包 这里以安卓为例。先什么都不干&#xff0c;直接打包安卓apk&#xff0c;查看包大小 查看编辑器打包日志 搜索build report构建报告。构建报告我们应该…

Nat Plants | 植物抽核单细胞!多组学探究大豆根瘤成熟过程

发表时间&#xff1a;2023-04 发表期刊&#xff1a;Nature Plants 影响因子&#xff1a;17.352 DOI&#xff1a;10.1038/s41477-023-01387-z 研究背景 根瘤菌是亲和互作寄主植物&#xff0c;感染宿主并在根部形成共生器官根瘤&#xff0c;具有固氮…

新手做抖店一般多久出单?想尽快出单需要做什么准备工作?

大家好&#xff0c;我是电商糖果 有不少刚开店的朋友&#xff0c;喜欢搜索&#xff0c;咨询多久出单的问题。 根据糖果做店四年多的经验来说&#xff0c;新手运营新店&#xff0c;只要操作思路正确&#xff0c;一般是一周左右就会出单&#xff0c;三到四周左右&#xff0c;店…

如何部署TDE透明加密实现数据库免改造加密存储

安当TDE&#xff08;透明数据加密&#xff09;实现数据库加密的步骤主要包括以下几个部分&#xff1a; 准备安装环境&#xff1a;确保操作系统和数据库环境已经安装并配置好&#xff0c;同时确保具有足够的权限来安装和配置TDE透明加密组件。下载安装包&#xff1a;从官方网站…

flutter开发实战-本地SQLite数据存储

flutter开发实战-本地SQLite数据库存储 正在编写一个需要持久化且查询大量本地设备数据的 app&#xff0c;可考虑采用数据库。相比于其他本地持久化方案来说&#xff0c;数据库能够提供更为迅速的插入、更新、查询功能。这里需要用到sqflite package 来使用 SQLite 数据库 预…

如何快速生成接口文档(swagger和knife4j两种方式及其使用)

如何快速生成接口文档&#xff08;swagger和knife4j两种方式&#xff09; 1、什么是接口文档&#xff1f; 在项目开发中&#xff0c;web项目的前后端分离开发&#xff0c;APP开发&#xff0c;需要由前后端工程师共同定义接口&#xff0c;编写接口文档&#xff0c;之后大家都根…

使用PyQt5设计订单查询界面—了解界面布局2

想要实现的界面效果 增加Tab Widge的页签 在MainWindow窗口中选中水平布局&#xff0c;将一个Label控件和一个默认自带两个页签的Tab Widget控件放到水平布局中&#xff0c;Tab Widget控件右键选择“插入页”再选择“在当前页之后”增加页签。 为每一个Tab页签界面都选择“栅格…

【小积累】@Qualifier注解

今天在看rabbitMQ的时候需要绑定交换机和队列&#xff0c;交换机和队列都已经注入到spring容器中&#xff0c;写了一个配置类&#xff0c;使用了bean注解注入的。所以这时候绑定的时候需要使用容器中的交换机和队列&#xff0c;必须要使用Qualifier去确定是容器中的哪个bean对象…

240W 宽电压输入 AC/DC 导轨式开关电源——TPR/SDR-240-XS 系列

TPR/SDR-240-XS 导轨式开关电源&#xff0c;额定输出功率为240W&#xff0c;产品输入范围&#xff1a;85-264VAC。提供24V、48V输出&#xff0c;具有短路保护&#xff0c;过载保护等功能&#xff0c;并具备高效率&#xff0c;高可靠性、高寿命、更安全、更稳定等特点&#xff0…

Uncaught InternalError: too much recursion

今天在敲代码的时候偶然间发现项目因为一次操作导致浏览器变得非常卡&#xff0c;而且控制台还报错了 Uncaught InternalError: too much recursior 页面截图如下 &#xff1a; 突如起来的报错和页面异常卡顿给我整不会了ovo&#xff0c;点开报错的地方&#xff0c;直接跳转到对…

FullCalendar日历组件集成实战(3)

背景 有一些应用系统或应用功能&#xff0c;如日程管理、任务管理需要使用到日历组件。虽然Element Plus也提供了日历组件&#xff0c;但功能比较简单&#xff0c;用来做数据展现勉强可用。但如果需要进行复杂的数据展示&#xff0c;以及互动操作如通过点击添加事件&#xff0…

【Linux线程(二)】线程互斥和同步

前言&#xff1a; 在上一篇博客中&#xff0c;我们讲解了什么是线程以及如何对线程进行控制&#xff0c;那么了解了这些&#xff0c;我们就可以在程序中创建多线程了&#xff0c;可是多线程往往会带有许多问题&#xff0c;比如竞态条件、死锁、数据竞争、内存泄漏等问题&#…

【Unity】 HTFramework框架(四十八)使用Location设置Transform位置、旋转、缩放

更新日期&#xff1a;2024年5月14日。 Github源码&#xff1a;[点我获取源码] Gitee源码&#xff1a;[点我获取源码] 索引 Location定义Location复制Location变量的值复制Transform组件的Location值粘贴Location变量的值粘贴Location值到Transform组件在代码中使用Location Loc…

GPT-4o omni全能 openAI新flagship旗舰模型,可以通过音频、视觉、文本推理。自然人机交互,听懂背景噪音、笑声、歌声或表达情感,也能输出。

新旗舰模型GPT-4o GPT-4o 是openAI新flagship旗舰模型&#xff0c;可以通过音频、视觉、文本推理reason&#xff0c;也能组合输出text, audio, and image。 接受文本、音频和图像的任意组合作为输入&#xff0c;并生成文本、音频和图像输出的任意组合。 速度快 2 倍&#xff…

华火5.0台嵌式喷火电燃单灶,更懂未来生活需求

在厨电技术不断革新的今天&#xff0c;第五代华火电燃灶以其独特的技术升级和卓越性能&#xff0c;成功吸引了市场的广泛关注。作为华火品牌的最新力作&#xff0c;第五代电燃灶不仅继承了前代产品的优点&#xff0c;更在多个方面进行了显著的升级和创新。下面&#xff0c;我们…

PXI/PXIe规格 A429/717 航电总线适配卡

A429是一款标准的PXI/PXIe1规格的多协议总线适配卡。该产品最多支持36个A429通道&#xff0c;或32个A429通道加4个A717通道&#xff0c;每个A429和A717通道可由软件配置成接收或发送&#xff0c;可满足A429总线和A717总线的通讯、测试和数据分析等应用需求。 该产品的每个A429通…

Simulink|虚拟同步发电机(VSG)惯量阻尼自适应控制仿真模型

主要内容 该模型为simulink仿真模型&#xff0c;主要实现的内容如下&#xff1a; 随着风力发电、光伏发电等新能源发电渗透率增加&#xff0c;电力系统的等效惯量和等效阻尼逐渐减小&#xff0c;其稳定性问题变得越来越严峻。虚拟同步发电机&#xff08;VSG&#xff09;技…

Django项目之电商购物商城 -- 修改/删除收货地址/设置默认地址

Django项目之电商购物商城 – 修改/删除收货地址/设置默认地址 修改和删除收货地址依旧实在user应用下进行 , 其思路和新增收货地址非常相似 依旧是更具前端的数据来写 在这里修改和删除地址的URL是相同的 , 所以我们只要设置一个模型类就可以实现这两个功能 一 . 修改地址…

Go 多模块工作区处理一个go项目下有多个module(即多个go.mod)的情况

背景 在现在微服务盛行的年代&#xff0c;一个人会维护多个代码仓库&#xff0c;很多的时候是多个仓库进行同时开发&#xff0c;也就是在当前项目下有多个目录&#xff0c;每个目录对应一个微服务&#xff0c;每个微服务都有一个go.mod文件。那么我在其中一个目录下要怎么导入…