Redission分布式锁 - 抢课系统

使用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 已存在,则会忽略。*/@Beanpublic 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();// 先判断消息是否已经已经消费过,5sString 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 + 任务课程idLong taskId = param.getTaskId();    // 选课任务idLong taskcourseId = param.getTaskcourseId();    // 选课任务课程idString key = taskId + "::" + taskcourseId;// 获得课程容量库存锁:任务id + 任务课程id,才可以操作库存缓存String fullKey = redisLockUtil.getFullKey(COURSE_SELECTION_STUDENT_LOCK_PREFIX, key);final RLock lock = redissonClient.getLock(fullKey);// 尝试抢【课程容量库存锁】锁时间调整 5sboolean 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是否已选课程* 加入锁对象*/@Overridepublic void handleCourseSelectionStudent(CourseSelectionStudentParam param, RLock lock) {Date current = new Date();// 库存余量判断 -> 查询库存keyString 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/教师idselectionStudent.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());     // 学生选课记录idcourseVo.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.mzph.cn/news/838000.shtml

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

相关文章

知识图谱开发日志

应用于应用环境的配置.测试.发布 假如你写了一个web,并且测试调试都没有问题 并且,你想发给你的朋友,导师,或者部署到远程云服务器上 那么,你需要配置相同的软件,比如数据库,web服务器,必要的插件,库,etc…但这并不一定能保证软件的正常运行,因为别人可能使用完全不同的操作系统…

在VMware安装Androidx86_64系统要点

上篇使用VirtualBox安装过Androidx86_64系统&#xff0c;尝试了没有蓝牙共享的好方法。本篇记录下使用Vmware虚机安装改系统&#xff0c;并使用蓝牙共享功能。 1.准备材料 本篇安装环境是安装Window10_64位系统。需要下载好Vmware安装包&#xff0c;VMWare版本&#xff1a;VMw…

python:rename函数用法

在Pandas库中&#xff0c;rename函数是一个非常实用的方法&#xff0c;用于重命名DataFrame或Series的轴标签&#xff08;如列名或索引&#xff09;。以下是rename函数的基本用法、参数以及一些示例。 1.rename基本语法 DataFrame.rename(mapperNone, indexNone, columnsNone…

【以规划为导向的自动驾驶】Planning-oriented Autonomous Driving

ABSTRACT 研究背景&#xff1a; 现代自动驾驶系统是顺序化地排列多个任务模块, 近期的主流方法&#xff1a; ①为单个任务部署独立模型 ②设计具有分离式头部的多任务(multi-task)范式。 但是&#xff0c;这些方法会累积误差或任务间协同不足而不利于自动驾驶。 作者认为重…

【devops】Linux 日常磁盘清理 ubuntu 清理大文件 docker 镜像清理

日常磁盘清理 1、查找大文件 find / -type f -size 1G2、清理docker无用镜像&#xff08;drone产生的残余镜像文件&#xff09; docker system prune -a一、清理服务器磁盘 1、查找大文件 在Ubuntu系统中&#xff0c;你可以使用find命令来查找大文件。find命令是一个强大的…

从离线到实时:无锡锡商银行基于 Apache Doris 的数据仓库演进实践

作者&#xff1a;武基鹏&#xff0c;无锡锡商银行 大数据技术经理 编辑整理&#xff1a;SelectDB 技术团队 导读&#xff1a;为实现数据资产的价值转化以及全面数字化、智能化的风险管理&#xff0c;无锡锡商银行大数据平台经历从 Hive 离线数据仓库到 Apache Doris 实时数据仓…

5G技术相关部分图解

1、面向5G商用网络的全系列解决方案 面向5G商用网络的全系列解决方案涵盖了从核心网到接入网的各个方面&#xff0c;确保网络的高性能、高可靠性和高安全性 2、2\3\4\5G带宽图解 G带宽的提升将推动许多新型应用的发展&#xff0c;并提供更快速、更可靠的移动通信体验。然而…

Springboot3 链接Redis遇到的报错(本文仅记录保存,优质文章移步springboot专栏)

出现的报错&#xff1a; cannot connect to Redisedis.clients.jedis.exceptions.JedisDataException: ERR Client sent AUTH, but no password is setredis wrong number of arguments for ‘auth’ command 其实上面的三个报错是不同界面显示的&#xff0c;后面两个是通过Ide…

git入门操作

一、介绍 Git是一个开源的分布式版本控制系统&#xff0c;由Linus Torvalds创建&#xff0c;用于有效、高速地处理从小到大的项目版本管理。 二、注册Git代码托管平台账号 以下几个平台可供选择&#xff1a; Gitee: https://gitee.com/(国内) Gitee&#xff08;码云&…

【源码】2024全新多语言区块链交易所源码/期权交易/申购/币币秒合约交易所

全新ui&#xff0c;更新很多内容&#xff0c;具体看图&#xff0c;全部开源 全新多语言区块链交易所源码/期权交易/申购/币币秒合约交易所 - 吾爱资源网

03-数据结构(一)

链接&#xff1a;C# 数据结构_哔哩哔哩_bilibili https://www.bilibili.com/video/BV1a541147Nk/?spm_id_from333.337.search-card.all.click&vd_source6eb7d966aa03ff5cb02b63725f651e68 链接&#xff1a;使用 C#.Net 学习掌握数据结构 (更新中)_哔哩哔哩_bilibili 一…

CheckStyle静态样式之道

优质博文&#xff1a;IT-BLOG-CN 在标准化的统一样式检查规范里&#xff0c;最为常用的统一样式工具是checkstyle插件&#xff0c;而不是国内阿里的代码规约插件。 【1】下载插件 【2】配置生效 配置生效及告警设置 【3】配置checkstyle.xml 官网地址 官网最新Releases 下面…

模拟量电机控制器PWM 输出隔离转换器4-20mA/0-5V/0-10V转50Hz/100Hz/1KHz/10KHz/100KHz

主要特性: 精度、线性度误差等级&#xff1a; 0.1、0.2、0.5 级4-20mA/0-5V/0-10V 等标准信号输入可选择RS485 通讯输入&#xff0c;支持 Modbus 协议PWM 信号输出&#xff0c;PWM 频率可选PWM 输出驱动能力可达 5A信号输入/信号输出 3000VDC 隔离可选择一进一出&#xff0c;一…

OpenAI 震撼发布:GPT-4o免费,实时语音视频交互开启新纪元

OpenAI 震撼发布&#xff1a;GPT-4o免费&#xff0c;实时语音视频交互开启新纪元 在仅仅问世17个月后&#xff0c;OpenAI 研制出了仿佛科幻片中登场的超级人工智能——GPT-4o&#xff0c;而且所有人都可以完全免费使用&#xff0c;让这个科技界的巨浪让人震撼无比&#xff01;…

真JAVA代码审计之XSS漏洞

Part1 漏洞案例demo&#xff1a; 没有java代码审计XSS漏洞拿赏金的案例。 所以将就看看demo吧 漏洞原理&#xff1a;关于XSS漏洞的漏洞原理核心其实没啥好说的&#xff0c;网上一查一大堆。 反射性XSS漏洞 <% page language"java" contentType"text/ht…

图搜索算法-最短路径算法-贝尔曼-福特算法

相关文章&#xff1a; 数据结构–图的概念 图搜索算法 - 深度优先搜索法&#xff08;DFS&#xff09; 图搜索算法 - 广度优先搜索法&#xff08;BFS&#xff09; 图搜索算法 - 拓扑排序 图搜索算法-最短路径算法-戴克斯特拉算法 贝尔曼-福特算法&#xff08;Bellman-Ford&#…

克鲁斯CLOOS机器人维修知识分享

克鲁斯工业机器人是一种高度精密的自动化设备&#xff0c;广泛应用于制造业、物流等领域。为了确保机器人的正常运行&#xff0c;了解一些基本的CLOOS工业机械手维修知识是必不可少的。 【常见CLOOS机械臂故障及解决方法】 1. 机器人无法启动&#xff1a;检查电源是否正常&…

Milvus 安装与配置

一、环境准备 在安装 Milvus 之前&#xff0c;确保你的系统满足以下要求&#xff1a; 操作系统&#xff1a;Milvus 支持 Linux 操作系统&#xff0c;如 Ubuntu、CentOS 等。硬件资源&#xff1a;推荐使用具有足够 CPU、内存和 SSD 存储的机器。对于大规模数据集&#xff0c;高…

5---Linux小程序“进度条”及缓冲区的概念(逻辑梳理,轻松理解)

一、缓冲区的解析&#xff1a; 1.1论点的引入&#xff1a; 在Linux中有一个接口sleep&#xff0c;可以使得C/C程序休眠一段指定的时间。他需要依赖的头文件是<unistd.h>&#xff0c;注意这个接口为Linux私有。 usleep接口类似sleep接口&#xff0c;但是单位为微秒。下面…

计算机视觉中的计算几何

计算几何领域出现于 20 世纪 70 年代&#xff0c;研究解决几何问题的数据结构和算法。这尤其包括确定图像内的拓扑结构&#xff0c;或者实际上是更高维的表示&#xff0c;例如点邻域&#xff0c;这可以帮助从数字图像数据等中导出几何意义[1]。 计算机视觉主要涉及静态或动态图…