接口 V2 完善:基于责任链模式、Canal 监听 Binlog 实现数据库、缓存的库存最终一致性

🎯 本文介绍了一种使用Canal监听MySQL Binlog实现数据库与缓存最终一致性的方案。文章首先讲解了如何修改Canal配置以适应订单表和时间段表的变化,然后详细描述了通过责任链模式优化消息处理逻辑的方法,确保能够灵活应对不同数据表的更新需求。最后,展示了如何利用RocketMQ消费Canal消息并通过责任链处理器同步更新缓存,从而保证数据的一致性。此方法有效提升了系统的可扩展性和维护效率。
🏠️ HelloDam/场快订(场馆预定 SaaS 平台)

文章目录

  • 前言
  • 修改Canal配置
  • 修改 Canal 消息消费逻辑
    • 识别修改哪个数据表
    • 如何实现根据表执行相应操作
      • 责任链模式框架
      • 常量
      • 具体处理器
      • MQ 消费者调用责任链

前言

在上一篇文章中,使用令牌限流方式来实现时间段的预定。时间段令牌和时间段库存缓存是分离的,因此需要额外对库存缓存进行更新,如何实现数据库与缓存的数据一致性是一个常见问题。本文使用 Canal 监听 Binlog 实现数据库、缓存的库存最终一致性。为什么使用这种方案?不了解的读者可以先阅读文章:https://zhuanlan.zhihu.com/p/408515044

不了解 MySQL Binlog 开启和 Canal 安装与配置的朋友请先阅读小白手把手教程:https://hellodam.blog.csdn.net/article/details/144483823

修改Canal配置

修改instance.properties的过滤规则为canal.instance.filter.regex=^(venue-reservation)\\.(time_period_order_([0-9]|1[0-9]|2[0-9]|3[0-1])|time_period_([0-9]|1[0-5]))$。现在不仅需要考虑订单表time_period_order,还要考虑时间段表time_period,因为现在要保证时间段库存和空闲场号的数据库、缓存一致性

在这里插入图片描述

修改 Canal 消息消费逻辑

之前的实现如下

import cn.hutool.core.util.ObjectUtil;
import com.vrs.annotation.Idempotent;
import com.vrs.constant.OrderStatusConstant;
import com.vrs.constant.RocketMqConstant;
import com.vrs.domain.dto.mq.CanalBinlogDTO;
import com.vrs.domain.dto.req.TimePeriodStockRestoreReqDTO;
import com.vrs.enums.IdempotentSceneEnum;
import com.vrs.service.TimePeriodService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;import java.util.Map;/*** @Author dam* @create 2024/9/20 21:30*/
@Slf4j(topic = RocketMqConstant.CANAL_TOPIC)
@Component
@RocketMQMessageListener(topic = RocketMqConstant.CANAL_TOPIC,consumerGroup = RocketMqConstant.CANAL_CONSUMER_GROUP,messageModel = MessageModel.CLUSTERING
)
@RequiredArgsConstructor
public class CanalBinlogCommonListener implements RocketMQListener<CanalBinlogDTO> {private final TimePeriodService timePeriodService;/*** 消费消息的方法* 方法报错就会拒收消息** @param CanalBinlogDTO 消息内容,类型和上面的泛型一致。如果泛型指定了固定的类型,消息体就是我们的参数*/@Idempotent(uniqueKeyPrefix = "canal_binlog_common:",key = "#canalBinlogDTO.getId()+''",scene = IdempotentSceneEnum.MQ,keyTimeout = 3600L)@SneakyThrows@Overridepublic void onMessage(CanalBinlogDTO canalBinlogDTO) {if (canalBinlogDTO.getOld() == null) {return;}Map<String, Object> alterDataMap = canalBinlogDTO.getData().get(0);Map<String, Object> oldDataMap = canalBinlogDTO.getOld().get(0);if (ObjectUtil.equal(canalBinlogDTO.getType(), "UPDATE") && oldDataMap.containsKey("order_status")) {log.info("[消费者] 消费canal的消息,恢复时间段的库存和空闲场号,时间段ID:{}", alterDataMap.get("time_period_id"));Long userId = Long.parseLong(alterDataMap.get("user_id").toString());Long timePeriodId = Long.parseLong(alterDataMap.get("time_period_id").toString());Long partitionId = Long.parseLong(alterDataMap.get("partition_id").toString());Long courtIndex;if (alterDataMap.containsKey("partition_index")) {courtIndex = Long.parseLong(alterDataMap.get("partition_index").toString());} else {courtIndex = Long.parseLong(alterDataMap.get("court_index").toString());}Integer orderStatus = Integer.parseInt(alterDataMap.get("order_status").toString());Integer oldOrderStatus = Integer.parseInt(oldDataMap.get("order_status").toString());if (orderStatus.equals(OrderStatusConstant.CANCEL) && oldOrderStatus.equals(OrderStatusConstant.UN_PAID)) {// 恢复库存timePeriodService.restoreStockAndBookedSlots(TimePeriodStockRestoreReqDTO.builder().userId(userId).courtIndex(courtIndex).timePeriodId(timePeriodId).partitionId(partitionId).build());}}}
}

因为之前只需要处理订单表即可,现在还需要处理时间段表的更改。所以需要做两件事:

  • 识别是哪个数据表更新了
  • 根据所识别的表,执行相应的业务逻辑

识别修改哪个数据表

首先需要了解 Canal 发送的消息内容格式,从下图可以看到有数据库名,但没有表名。因此为了识别数据表,只能通过表的独有字段名来识别了

在这里插入图片描述

如何实现根据表执行相应操作

一种方式是,直接在onMessage方法中,识别完数据表类型之后,调用相应的方法来处理。这种方式实现简单,但后续如果要处理新的表,需要修改代码,违反了开闭原则。

为了解决这个问题,本文使用责任链模式,即封装多个处理器到责任链上,每个处理器负责识别一个表,并进行相应的业务逻辑。后续使用时就依次调用链上的处理器,如果处理器发现是自己的职责,就执行逻辑,否则直接返回,调用下一个处理器。

责任链模式框架

【抽象处理器】

package com.vrs.chain_of_responsibility;/*** @Author dam* @create 2024/12/11 19:18*/
public interface AbstractChainHandler<T> {/*** 由实现类来实现具体的处理方法*/boolean handle(T param);/*** 名称,用来区分不同的责任链*/String name();/*** 处理器的排序*/int order();
}
  • handler:由具体的处理器来实现,用来实现业务逻辑
  • name:用来标识责任链,返回相同名字的处理器被归到一个责任链中
  • order:用来给同一责任链的处理器排序

【责任链上下文】

该类用来管理不同的责任链。

  • 当Spring启动时,执行run方法,通过获取AbstractChainHandler的实现类来初始化所有责任链,即将处理器按照name划分到不同的责任链中,后面可以通过容器chainContainer来获取。最后对同一链上的处理器按照sort升序排序。
  • 当调用handler方法时,会根据name获取责任链,然后依次调用链上的处理器来进行业务处理,若有任意处理器的handle方法返回 true ,责任链就会中断。如果想要依次执行所有处理器,那所有处理器都返回 false 即可
package com.vrs.chain_of_responsibility;import org.springframework.beans.BeansException;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import java.util.*;/*** @Author dam* @create 2024/12/11 19:20*/
@Component
public class ChainContext<T> implements ApplicationContextAware, CommandLineRunner {/*** 通过 Spring IOC 获取 Bean 实例*/private ApplicationContext applicationContext;/*** key:责任链名称* value:责任链*/private final Map<String, List<AbstractChainHandler>> chainContainer = new HashMap<>();@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}@Overridepublic void run(String... args) {// 从 Spring IOC 容器中获取接口对应的 Spring Bean 集合Map<String, AbstractChainHandler> chainFilterMap = applicationContext.getBeansOfType(AbstractChainHandler.class);chainFilterMap.forEach((beanName, bean) -> {// 判断 name 是否已经存在抽象责任链容器中// 如果已经存在直接向集合新增// 如果不存在,创建对应的集合List<AbstractChainHandler> abstractChainHandlers = chainContainer.getOrDefault(bean.name(), new ArrayList<>());abstractChainHandlers.add(bean);chainContainer.put(bean.name(), abstractChainHandlers);});chainContainer.forEach((mark, unsortedChainHandlers) -> {// 对每个责任链的实现类根据order升序排序Collections.sort(unsortedChainHandlers, ((o1, o2) -> {return Integer.compare(o1.order(), o2.order());}));});}/*** 责任链组件执行** @param name         责任链组件标识* @param requestParam 请求参数*/public void handler(String name, T requestParam) {// 根据 name 从责任链容器中获取对应的责任链List<AbstractChainHandler> abstractChainHandlers = chainContainer.get(name);if (CollectionUtils.isEmpty(abstractChainHandlers)) {throw new RuntimeException(name + "对应的责任链不存在");}// 遍历责任链处理器for (AbstractChainHandler handler : abstractChainHandlers) {if (handler.handle(requestParam)) {// --if-- 如果处理器返回 true,表示已经处理完成,退出责任链return;}}}
}

常量

package com.vrs.constant;/*** Redis缓存Key常量类*/
public class ChainConstant {public static final String RESERVE_CHAIN_NAME = "reserve_chain";public static final String CANAL_CHAIN_NAME = "canal_chain";}

具体处理器

由于修改的要么是订单表,要么是时间段表,因此责任链上面只要有任一处理器成功处理,即返回 true ,就无须调用余下的其他处理器

【时间段库存修改处理器】

当数据库中的库存修改之后,同步修改缓存中的库存

package com.vrs.service.chainHander.canal;import cn.hutool.core.util.ObjectUtil;
import com.vrs.chain_of_responsibility.AbstractChainHandler;
import com.vrs.constant.ChainConstant;
import com.vrs.constant.RedisCacheConstant;
import com.vrs.domain.dto.mq.CanalBinlogDTO;
import com.vrs.service.PartitionService;
import com.vrs.service.TimePeriodService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.Map;/*** 订单超时关闭处理逻辑** @Author dam* @create 2024/12/11 19:43*/
@Component
@RequiredArgsConstructor
@Slf4j
public class TimePeriodStockChangeHandler implements AbstractChainHandler<CanalBinlogDTO> {private final StringRedisTemplate redisTemplate;private final TimePeriodService timePeriodService;private final PartitionService partitionService;@Overridepublic boolean handle(CanalBinlogDTO canalBinlogDTO) {Map<String, Object> alterDataMap = canalBinlogDTO.getData().get(0);Map<String, Object> oldDataMap = canalBinlogDTO.getOld().get(0);if (ObjectUtil.equal(canalBinlogDTO.getType(), "UPDATE") && oldDataMap.containsKey("stock")) {// --if-- 如果是修改操作,且修改了stocklog.info("[消费者] 消费canal的消息,时间段库存修改,同步修改缓存的库存,时间段ID:{}", alterDataMap.get("id"));Long timePeriodId = Long.parseLong(alterDataMap.get("id").toString());Long partitionId = Long.parseLong(alterDataMap.get("partition_id").toString());Integer stock = Integer.parseInt(alterDataMap.get("stock").toString());Long bookedSlots = Long.parseLong(alterDataMap.get("booked_slots").toString());// 更新库存redisTemplate.opsForValue().set(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_KEY, timePeriodId), stock.toString());// 更新位图timePeriodService.initializeFreeIndexBitmap(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_KEY, timePeriodId),partitionService.getPartitionDOById(partitionId).getNum(),bookedSlots,24 * 3600);return true;}return false;}@Overridepublic String name() {return ChainConstant.CANAL_CHAIN_NAME;}@Overridepublic int order() {return 10;}
}

【订单超时关闭处理器】

package com.vrs.service.chainHander.canal;import cn.hutool.core.util.ObjectUtil;
import com.vrs.chain_of_responsibility.AbstractChainHandler;
import com.vrs.constant.ChainConstant;
import com.vrs.constant.OrderStatusConstant;
import com.vrs.domain.dto.mq.CanalBinlogDTO;
import com.vrs.domain.dto.req.TimePeriodStockRestoreReqDTO;
import com.vrs.service.TimePeriodService;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;import java.util.Map;/*** 订单超时关闭处理逻辑** @Author dam* @create 2024/12/11 19:43*/
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderCloseHandler implements AbstractChainHandler<CanalBinlogDTO> {private final TimePeriodService timePeriodService;@Overridepublic boolean handle(CanalBinlogDTO canalBinlogDTO) {Map<String, Object> alterDataMap = canalBinlogDTO.getData().get(0);Map<String, Object> oldDataMap = canalBinlogDTO.getOld().get(0);if (ObjectUtil.equal(canalBinlogDTO.getType(), "UPDATE") && oldDataMap.containsKey("order_status")) {log.info("[消费者] 消费canal的消息,订单超时关闭,恢复时间段的库存和空闲场号,时间段ID:{}", alterDataMap.get("time_period_id"));Long userId = Long.parseLong(alterDataMap.get("user_id").toString());Long timePeriodId = Long.parseLong(alterDataMap.get("time_period_id").toString());Long partitionId = Long.parseLong(alterDataMap.get("partition_id").toString());Long courtIndex;if (alterDataMap.containsKey("partition_index")) {courtIndex = Long.parseLong(alterDataMap.get("partition_index").toString());} else {courtIndex = Long.parseLong(alterDataMap.get("court_index").toString());}Integer orderStatus = Integer.parseInt(alterDataMap.get("order_status").toString());Integer oldOrderStatus = Integer.parseInt(oldDataMap.get("order_status").toString());if (orderStatus.equals(OrderStatusConstant.CANCEL) && oldOrderStatus.equals(OrderStatusConstant.UN_PAID)) {// 恢复库存timePeriodService.restoreStockAndBookedSlots(TimePeriodStockRestoreReqDTO.builder().userId(userId).courtIndex(courtIndex).timePeriodId(timePeriodId).partitionId(partitionId).build());}return true;}return false;}@Overridepublic String name() {return ChainConstant.CANAL_CHAIN_NAME;}@Overridepublic int order() {return 0;}
}

MQ 消费者调用责任链

使用非常简单,直接调用chainContext.handler(ChainConstant.CANAL_CHAIN_NAME, canalBinlogDTO);即可

package com.vrs.rocketMq.listener;import com.vrs.annotation.Idempotent;
import com.vrs.chain_of_responsibility.ChainContext;
import com.vrs.constant.ChainConstant;
import com.vrs.constant.RocketMqConstant;
import com.vrs.domain.dto.mq.CanalBinlogDTO;
import com.vrs.enums.IdempotentSceneEnum;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;/*** @Author dam* @create 2024/9/20 21:30*/
@Slf4j(topic = RocketMqConstant.CANAL_TOPIC)
@Component
@RocketMQMessageListener(topic = RocketMqConstant.CANAL_TOPIC,consumerGroup = RocketMqConstant.CANAL_CONSUMER_GROUP,messageModel = MessageModel.CLUSTERING
)
@RequiredArgsConstructor
public class CanalBinlogCommonListener implements RocketMQListener<CanalBinlogDTO> {private final ChainContext chainContext;/*** 消费消息的方法* 方法报错就会拒收消息** @param CanalBinlogDTO 消息内容,类型和上面的泛型一致。如果泛型指定了固定的类型,消息体就是我们的参数*/@Idempotent(uniqueKeyPrefix = "canal_binlog_common:",key = "#canalBinlogDTO.getId()+''",scene = IdempotentSceneEnum.MQ,keyTimeout = 3600L)@SneakyThrows@Overridepublic void onMessage(CanalBinlogDTO canalBinlogDTO) {if (canalBinlogDTO.getOld() == null) {// --if-- 如果不是修改数据,快速退出,因为我们现在的业务逻辑都是识别出数据修改才有下面的操作return;}// 调用责任链来消费 canal 消息chainContext.handler(ChainConstant.CANAL_CHAIN_NAME, canalBinlogDTO);}
}

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

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

相关文章

postgresql15的停止

PostgreSQL是一个功能非常强大的、源代码开放的客户/服务器关系型数据库管理系统&#xff0c;且因为许可证的灵活&#xff0c;任何人都可以以任何目的免费使用、修改和分发PostgreSQL。介绍过postgresql的启动方法&#xff0c;就很有必要介绍下postgresql的停止方法。 一、停止…

IP协议格式

在传输层中有两个重要的协议&#xff0c;分别为UDP和TCP。UDP追求高效的效率&#xff0c;TCP追求更可靠的传输。但是这两个协议都太极端了&#xff0c;有没有一个协议可以在保证可靠性的同时又不失较高的效率。 针对上面的问题&#xff0c;网络层的IP协议是更好的解决方案。 …

Qt Creator 15.0.0如何更换主题和字体

1.打开Qt Creator 15.0.0 (Community)&#xff0c; 2.点击编辑栏3.点击Preferences... 4.修改主题&#xff0c;点击环境&#xff0c;修改Theme:栏 5.修改字体大小&#xff0c;点击文本编辑器&#xff0c;修改字号栏。&#xff0c;修改Theme:栏

靶机复现-pikachu靶机文件包含漏洞

本篇文章旨在为网络安全渗透测试靶机复现学习。通过阅读本文&#xff0c;读者将能够对渗透pikachu靶场文件包含漏洞复现有一定的了解 原文学习链接 CSDN博主&#xff1a;One_Blanks主页地址 靶机资源下载 PHPStudy pikachu 一、前言 文件包含漏洞是编程中的一种安全隐患&a…

机器学习-线性回归(简单回归、多元回归)

这一篇文章&#xff0c;我们主要来理解一下&#xff0c;什么是线性回归中的简单回归和多元回归&#xff0c;顺便掌握一下特征向量的概念。 一、简单回归 简单回归是线性回归的一种最基本形式&#xff0c;它用于研究**一个自变量&#xff08;输入&#xff09;与一个因变量&…

5.SQLAlchemy对两张有关联关系表查询

问题 例如&#xff0c;一个用户可以有多个收获地址。 定义表如下&#xff1a; 用户表 地址表 一般情况&#xff0c;我们会先查询用户表&#xff0c;拿到用户id后&#xff0c;再到地址表中查询关联的地址数据。这样就要执行两次查询。 仅仅为了方便查询&#xff0c;需要一些属…

【Unity】ScrollViewContent适配问题(Contentsizefilter不刷新、ContentSizeFilter失效问题)

最近做了一个项目&#xff0c;菜单栏读取数据后自动生成&#xff0c;结果用到了双重布局 父物体 尝试了很多方式&#xff0c;也看过很多大佬的文章&#xff0c;后来自己琢磨了一下&#xff0c;当子物体组件自动生成之后&#xff0c;使用以下以下代码效果会好一些&#xff1a; …

springboot基于微信小程序的商城系统

基于Spring Boot的微信小程序商城系统是一种现代化的电商解决方案&#xff0c;它将Spring Boot框架的强大后端能力与微信小程序的便捷前端体验相结合&#xff0c;为商家和用户提供了高效、稳定的在线购物平台。 一、后端框架 &#xff1a;Spring Boot 1. 简介&#xff1a; 2.…

63,【3】buuctf web Upload-Labs-Linux 1

进入靶场 点击pass1 查看提示 既然是上传文件&#xff0c;先构造一句话木马&#xff0c;便于用蚁剑连接 <?php eval($_POST[123])?> 上传木马 文件后缀写为.php.jpg 右键复制图片地址 打开蚁剑连接 先点击测试连接&#xff0c;显示成功后&#xff0c;再点击添加即可 …

【玩转全栈】----基于ModelForm完成用户管理页面

目录 大致效果 添加用户代码 引入ModelForm ModelForm 与一般表单的区别&#xff1a; ModelForm 与传统 Form 的区别&#xff1a; 使用ModelForm制作用户管理 新建用户 编辑用户&#xff1a; 删除数据 完整代码 在学完前面的部门管理案例后&#xff0c;自己独立写出个用户管理应…

AIGC视频生成模型:ByteDance的PixelDance模型

大家好&#xff0c;这里是好评笔记&#xff0c;公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍ByteDance的视频生成模型PixelDance&#xff0c;论文于2023年11月发布&#xff0c;模型上线于2024年9月&#xff0c;同时期上线的模型还有Seaweed&…

深入探究 YOLOv5:从优势到模型导出全方位解析

一、引言 在计算机视觉领域&#xff0c;目标检测是一项至关重要的任务&#xff0c;它在自动驾驶、安防监控、工业检测等众多领域都有着广泛的应用。而 YOLO&#xff08;You Only Look Once&#xff09;系列作为目标检测算法中的佼佼者&#xff0c;一直备受关注。其中&#xff…

Qt —— 控件属性

一、概述 控件有很多属性&#xff0c;我们学习和整理常见和常用的几个属性&#xff0c;由于所有的控件基本都是继承Widget类的&#xff0c;所以前面会先拿Widget类和常见的控件进行示范。 Qt Designer左侧一长条就是Qt给我们内置好的控件&#xff1a; 二、enabled 状态属性 …

会议签到系统的架构和实现

会议签到系统的架构和实现 摘要:通过定制安卓会议机开机APP呈现签到界面&#xff0c;并且通过W/B结构采集管理签到信息&#xff0c;实现会议签到的功能。为达到此目标本文将探讨使用Redis提供后台数据支持&#xff1b;使用SocketIo处理适时消息&#xff1b;使用Flask进行原型开…

WPF实战案例 | C# WPF实现大学选课系统

WPF实战案例 | C# WPF实现大学选课系统 一、设计来源1.1 主界面1.2 登录界面1.3 新增课程界面1.4 修改密码界面 二、效果和源码2.1 界面设计&#xff08;XAML&#xff09;2.2 代码逻辑&#xff08;C#&#xff09; 源码下载更多优质源码分享 作者&#xff1a;xcLeigh 文章地址&a…

JAVA:Spring Boot 实现责任链模式处理订单流程的技术指南

1、简述 在复杂的业务系统中&#xff0c;订单流程往往需要一系列的操作&#xff0c;比如验证订单、检查库存、处理支付、更新订单状态等。责任链模式&#xff08;Chain of Responsibility&#xff09;可以帮助我们将这些处理步骤分开&#xff0c;并且以链式方式处理每一个操作…

stm32单片机个人学习笔记14(USART串口数据包)

前言 本篇文章属于stm32单片机&#xff08;以下简称单片机&#xff09;的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 STM32入门教程-2023版 细…

postgresql15的启动

PostgreSQL是一个功能非常强大的、源代码开放的客户/服务器关系型数据库管理系统&#xff0c;且因为许可证的灵活&#xff0c;任何人都可以以任何目的免费使用、修改和分发PostgreSQL。现在国产数据库大力发展阶段&#xff0c;学习和熟悉postgresql的功能是非常有必要的&#x…

行人识别检测数据集,yolo格式,PASICAL VOC XML,COCO JSON,darknet等格式的标注都支持,准确识别率可达99.5%

作者简介&#xff1a; 高科&#xff0c;先后在 IBM PlatformComputing从事网格计算&#xff0c;淘米网&#xff0c;网易从事游戏服务器开发&#xff0c;拥有丰富的C&#xff0c;go等语言开发经验&#xff0c;mysql&#xff0c;mongo&#xff0c;redis等数据库&#xff0c;设计模…

【前端】CSS实战之音乐播放器

目录 播放器背景旋转音乐封面按钮进度条音量调节音乐信息按钮的效果JavaScript部分播放和暂停音乐切换音乐信息进度条 音量调节避免拖拽时的杂音音量调节条静音和解除静音 自动下一首实现一个小效果最终效果 播放器背景 <div class"play_box"></div>设置…