单机环境下一人一单

优惠券秒杀

添加优惠卷

店铺发布优惠券又分为平价券和特价券, 平价券可以任意购买而特价券需要秒杀抢购(限制数量和时间)

tb_voucher(平价券): 优惠券的基本信息

在这里插入图片描述

tb_seckill_voucher(秒杀券): 有voucher_id字段表示具有优惠卷的基本信息,此外还有库存,开始抢购时间,结束抢购时间等特殊字段

在这里插入图片描述

VoucherController提供了一个接口方法用来添加普通优惠券

/*** 新增普通券* @param voucher 优惠券信息* @return 优惠券id*/
@PostMapping
public Result addVoucher(@RequestBody Voucher voucher) {// 直接将普通券的信息保存到普通券表中voucherService.save(voucher);return Result.ok(voucher.getId());
}

VoucherController提供了一个接口方法用来添加秒杀券,在VoucherService中的addSeckillVoucher方法实现添加秒杀券的业务逻辑

/*** 新增秒杀券* @param voucher 优惠券信息,包含秒杀券信息(库存,生效时间,失效时间)* @return 优惠券id*/
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());
}
// 新增秒杀券就是在数据库中新增普通卷和秒杀券的信息
@Override
@Transactional// 因为是操作两张表所以需要添加事务
public void addSeckillVoucher(Voucher voucher) {// 将秒杀券的基本信息保存到普通券表中,如果没有指定Id会自动生成save(voucher);// 保存秒杀信息SeckillVoucher seckillVoucher = new SeckillVoucher();// 关联普通券idseckillVoucher.setVoucherId(voucher.getId());// 设置库存seckillVoucher.setStock(voucher.getStock());// 设置开始时间seckillVoucher.setBeginTime(voucher.getBeginTime());// 设置结束时间seckillVoucher.setEndTime(voucher.getEndTime());// 保存信息到秒杀券表中seckillVoucherService.save(seckillVoucher);
}

添加秒杀券: 由于没有后台管理页面,使用Postman模拟发送POST请求http://localhost:8081/voucher/seckill来新增秒杀券(截止日期要超过当前日期否则不显示)

{"shopId":1,"title":"100元代金券","subTitle":"周一至周五可用","rules":"全场通用\\n无需预约\\n可无限叠加",// 数据库中金额的单位是分"payValue":8000,"actualValue":10000,"type":1,"stock":100,"beginTime":"2022-01-01T00:00:00","endTime":"2023-10-31T23:59:59"
}

实现秒杀下单

当我们点击抢购时会触发右侧的请求,我们只需要在VoucherOrderController编写对应的Controller处理请求即可

在这里插入图片描述

当用户开始进行下单我们应当提交优惠券Id去查询优惠卷信息,然后判断是否满足秒杀条件即秒杀是否开始和库存是否充足

在这里插入图片描述

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {@Autowiredprivate IVoucherOrderService voucherOrderService;@PostMapping("/seckill/{id}")public Result seckillVoucher(@PathVariable("id") Long voucherId) {return voucherOrderService.seckillVoucher(voucherId);}
}public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);
}@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactional// 操作两张表应该加上事务public Result seckillVoucher(Long voucherId) {//1.查询优惠券的基本信息//SeckillVoucher seckillVouche = seckillVoucherService.getById(voucherId)LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId);SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper);//2.判断秒杀时间是否开始if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) {return Result.fail("秒杀还未开始,请耐心等待");}//3.判断秒杀时间是否结束if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) {return Result.fail("秒杀已经结束!");}//4.判断库存是否充足if (seckillVoucher.getStock() < 1) {return Result.fail("优惠券已被抢光了哦,下次记得手速快点");}//5.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id",voucherId).update();if (!success) {return Result.fail("库存不足");}//6. 创建订单	VoucherOrder voucherOrder = new VoucherOrder();//6.1 设置生成的订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//6.2 设置用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);//6.3 设置代金券idvoucherOrder.setVoucherId(voucherId);//7. 将订单数据保存到订单表中save(voucherOrder);//8. 返回订单idreturn Result.ok(orderId);}

库存超卖问题

当遇到高并发场景时会出现超卖现象,我们可以用Jmeter开200个线程来模拟抢100张优惠券的场景,结果优惠券库存为负数表示出现了超卖现象

  • 添加请求的信息头管理器携带我们登录的token(可能会过期),然后发起POST请求http://localhost:8081/voucher-order/seckill/voucher_id

在这里插入图片描述

在这里插入图片描述

乐观锁的两种实现方式

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁(悲观锁或乐观锁)

  • 悲观锁: 悲观锁比较适合插入数据,简单粗暴但是性能一般
  • 乐观锁: 比较适合更新数据, 性能好但是成功率低(多个线程同时执行时只有一个可以执行成功),还需要访问数据库造成数据库压力过大

在这里插入图片描述

版本号法: 给数据库表增加一个版本号version字段,每次操作表中的数据时会查询版本号,修改数据时再次验证版本号有没有变化,没有变化才可以更新数据

在这里插入图片描述

CAS(Compare-And-Switch): 首先查询要修改字段的值,在修改数据时再次验证字段值有没有发生变化(或满足某种条件),没有变化(或满足条件)才会更新字段的值

在这里插入图片描述

乐观锁解决超卖问题

使用stock来充当版本号,VoucherOrderServiceImpl在扣减库存时比较查询到的优惠券库存和实际数据库中优惠券库存是否相同

  • 假设100个线程同时都拿到了100的库存, 但是100个人中只有1个人能扣减成功(其他的人在扣减时库存发现库存已经被修改过了,所以不再执行扣减操作)
boolean success = seckillVoucherService.update().setSql("stock= stock -1") //set stock = stock -1.eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ?

使用stock>0 充当判断条件, 在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作

boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); where voucher_id = ? and stock > 0

单机环境下一人一单(悲观锁)

需求:修改秒杀业务要求同一个优惠券一个用户只能下一单

  • 如果时间和库存都充足,还需要根据优惠卷id和用户id查询是否已经下过这个订单,如果下过这个订单则不能再下单

在这里插入图片描述

在VoucherOrderServiceImpl中库存和时间都充足时即将扣减库存之前再增加一人一单逻辑

一个用户开了多个线程抢优惠券,在判断库存充足之后和执行一人一单逻辑之前间如果进来了多个线程,此时它们都在数据库中查询不到订单然后都会执行扣减操作

// 4. 判断库存是否充足
if (seckillVoucher.getStock() < 1) {return Result.fail("优惠券已被抢光了哦,下次记得手速快点");
}// 5.根据用户id查询用户对应的订单是否存在
Long userId = UserHolder.getUser().getId();
int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
if (count > 0) {// 用户已经下过单return Result.fail("您已经抢过优惠券了哦");
}
// 6.使用stock>0充当判断条件,在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作
boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); //where voucher_id = ? and stock > 0
if (!success) {return Result.fail("库存不足");
}

把一人一单逻辑之后生成订单记录的代码都提取到一个createVoucherOrder方法中(ctrl + alt + m)然后加悲观锁synchronized(悲观锁适合插入数据)

  • 不管哪一个线程运行到这个方法时都要检查有没有其它线程正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程结束
  • 把锁加在createVoucherOrder方法上锁的范围太大(粒度太粗)会导致每个线程进来都会锁住,锁的对象是this所有用户都公用一把锁串行执行会导致效率很低
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {// 5.根据用户id查询用户对应的订单是否存在Long userId = UserHolder.getUser().getId();int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();// 判断用户是否下过单if (count > 0) {return Result.fail("您已经抢过优惠券了哦");}// 6.使用stock>0充当判断条件,在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); //where voucher_id = ? and stock > 0if (!success) {return Result.fail("库存不足");}//7. 创建订单VoucherOrder voucherOrder = new VoucherOrder();//7.1 设置订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2 设置用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);//7.3 设置代金券idvoucherOrder.setVoucherId(voucherId);//8. 将订单数据保存到表中save(voucherOrder);//9. 返回订单idreturn Result.ok(orderId);
}

要完成一人一单的业务应该把这个锁只加在单个用户上(用户标识可以用userId), 如果我们直接使用userId.toString()每次锁住的都不是同一个String对象

方法名功能
String intern()从常量池中拿数据,如果字符串常量池中已经包含了一个等于这个String对象的字符串(由equals方法确定)将返回池中的字符串
如果没有则将此String对象添加到池中并返回对此String对象的引用
public static String toString(long i) {if (i == Long.MIN_VALUE)return "-9223372036854775808";int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);char[] buf = new char[size];getChars(i, size, buf);return new String(buf, true);
}
@Transactional
public Result createVoucherOrder(Long voucherId) {Long userId = UserHolder.getUser().getId();// toString的源码是new String所以userId.toString()拿到的也不是同一个String对象即不是同一个用户/不是同一把锁synchronized (userId.toString().intern()) {// 5.根据用户id查询用户对应的订单是否存在int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();// 判断用户是否下过单if (count > 0) {return Result.fail("您已经抢过优惠券了哦");}// 6.使用stock>0充当判断条件,在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); //where voucher_id = ? and stock > 0if (!success) {return Result.fail("库存不足");}//7. 创建订单VoucherOrder voucherOrder = new VoucherOrder();//7.1 设置订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2 设置用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);//7.3 设置代金券idvoucherOrder.setVoucherId(voucherId);//8. 将订单数据保存到表中save(voucherOrder);//9. 返回订单idreturn Result.ok(orderId);}//执行到这里锁已经被释放了但是可能当前事务还未提交,如果此时有线程进来不能确保事务不出问题
}

createVoucherOrder方法被Spring的事务控制,如果你在方法内部加锁可能会导致当前方法事务还没有提交但是锁已经释放了,此时新增的订单还没有写入数据库

  • 在seckillVoucher方法中将createVoucherOrder方法整体包裹起来, 保证事务提交之后才会释放锁确保数据库中有订单存在,同时控制锁的粒度
@Override
public Result seckillVoucher(Long voucherId) {LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();//1. 查询优惠券queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId);SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper);//2. 判断秒杀时间是否开始if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) {return Result.fail("秒杀还未开始,请耐心等待");}//3. 判断秒杀时间是否结束if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) {return Result.fail("秒杀已经结束!");}//4. 判断库存是否充足if (seckillVoucher.getStock() < 1) {return Result.fail("优惠券已被抢光了哦,下次记得手速快点");}// 获取用户idLong userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {return createVoucherOrder(voucherId);}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {// toString的源码是new String所以userId.toString()拿到的也不是同一个String对象即不是同一个用户/不是同一把锁synchronized (userId.toString().intern()) {// 5.根据用户id查询用户对应的订单是否存在int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();// 判断用户是否下过单if (count > 0) {return Result.fail("您已经抢过优惠券了哦");}// 6.使用stock>0充当判断条件,在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); //where voucher_id = ? and stock > 0if (!success) {return Result.fail("库存不足");}//7. 创建订单VoucherOrder voucherOrder = new VoucherOrder();//7.1 设置订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2 设置用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);//7.3 设置代金券idvoucherOrder.setVoucherId(voucherId);//8. 将订单数据保存到表中save(voucherOrder);//9. 返回订单idreturn Result.ok(orderId);}//执行到这里锁已经被释放了但是可能当前事务还未提交,如果此时有线程进来不能确保事务不出问题
}

由于seckillVoucher方法没有加事务注解,所以调用createVoucherOrder方法是this.的方式调用的,this此时是VoucherOrderServiceImpl没有事务功能

  • 事务想要生效需要利用VoucherOrderServiceImpl的代理对象,所以我们需要获得原始的事务对象来操作事务

  • 使用AopContext.currentProxy()获取当前对象的代理对象(具有事务功能),然后再用代理对象调用方法底层需要使用aspectjweaver依赖

  • 获取事务的代理对象需要在IVoucherOrderService中创建createVoucherOrder方法

  • 在启动类上加上@EnableAspectJAutoProxy(exposeProxy = true)注解

public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);Result createVoucherOrder(Long voucherId);
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);
}
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}
}

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

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

相关文章

软考机考考试第一批经验分享

由于机考的特殊性&#xff0c;考试环境与传统笔试环境有所不同。下面是与考试环境相关的总结&#xff1a; 草稿纸&#xff1a;考场提供足够数量的草稿纸&#xff0c;每位考生都会分发一张白纸作为草稿纸。在草稿纸上需要写上准考证号。如果不够用&#xff0c;可以向监考老师再次…

Mac部署Odoo环境-Odoo本地环境部署

Odoo本地环境部署 安装Python安装Homebrew安装依赖brew install libxmlsec1 Python运行环境Pycharm示例配置 Mac部署Odoo环境-Odoo本地环境部署 安装Python 新机&#xff0c;若系统没有预装Python&#xff0c;则安装需要版本的Python 点击查询Python官网下载 安装Homebrew 一…

实验:BGP配置

1.实验目的&#xff1a; 本实验旨在掌握BGP协议的基本概念和配置方法&#xff0c;以及使用Packet Tracer模拟网络环境进行BGP配置的方法。 2.实验要求&#xff1a; 理解BGP协议的基本概念和原理&#xff1b;掌握BGP协议的配置方法&#xff1b;能够使用Packet Tracer模拟网络…

2019年第八届数学建模国际赛小美赛C题预测通过拥堵路段所需的时间解题全过程文档及程序

2019年第八届数学建模国际赛小美赛 C题 预测通过拥堵路段所需的时间 原题再现&#xff1a; 在导航软件中&#xff0c;行程时间的估计往往是一个重要的功能。现有的导航软件往往通过出租车或安装了该软件的车辆获取实时GPS数据来确定当前的路况。在交通拥堵严重的情况下&#…

【深度学习初探】Day32 - 三维点云数据基础

【深度学习初探】Day32 - 三维点云数据基础 文章目录 【深度学习初探】Day32 - 三维点云数据基础一、点云的定义二、点云的获取三、点云的属性四、点云的存储格式4.1 pts4.2 LAS4.3 PCD4.4 .xyz4.5 .pcap 五、三维点云的表示方法5.1 二维投影5.2 三维体素5.3 原始点云5.4 图 六…

Node.js创建一个简单的WebSocket接口,实现通信交互

Node.js创建一个简单的WebSocket接口&#xff0c;实现通信交互 一、为什么使用WebSocket&#xff1f; WebSocket&#xff0c;最大特点就是&#xff0c;服务器可以主动向客户端推送信息&#xff0c;客户端也可以主动向服务器发送信息&#xff0c;是真正的双向平等对话&#xf…

IIS + Axios 跨域设置

1、服务器端设置IIS &#xff08;web.config) 即可&#xff0c;不需要对django settings.py做配置&#xff08;python manage.py runserver 才需要settings.py配置跨域&#xff0c;IIS在iis上配&#xff09; 网站根目录的web.config中加上这段&#xff1a; <httpProtocol&…

基于开源的JAVA mongodb jdbc 驱动 使用教程

基于开源的JAVA mongodb jdbc 驱动 使用教程介绍 介绍 本文介绍一款开源的基于JAVA的 Mongodb JDBC 驱动使用教程 开源地址 https://gitee.com/bgong/jdbc-mongodb-driver功能价值 与mybaits融合&#xff1a;复用mybatis的功能特性&#xff0c;如:缓存,if动态判断标签等特…

亚马逊云科技 re:Invent 大会 - ElastiCache Serverless模式来袭

亚马逊云科技 re:Invent 大会 - ElastiCache Serverless模式来袭 本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 亚马逊云科技开发者社区, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道。 文章目录 亚马逊云…

GoLang 学习 (入门)

go run 1.go 执行命令 go build 1.go 打包为exe 快速 并且无依赖 在开始项目 需要 生成 go.mod go mod init mod 终端执行 go: creating new go.mod: module mod go: to add module requirements and sums:go mod tidy go的基本目录结构 src ------gocode ------------项…

日期问题(C语言蓝桥杯2017年题目G)

分析&#xff1a;我们输入的AA/BB/CC有三种情况&#xff0c;所以我们编写一个函数&#xff0c;来判断三个数字作为 年 月 日是否合法&#xff0c;合法就输出&#xff0c;不合法就终止&#xff0c;还要查重&#xff0c;如果有相同的时间&#xff0c;就不重复打印&#xff0c;…

如何看待「前端已死论」?

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 仓库主页&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 欢迎点赞…

微信小程序 实现上传图片前裁剪功能

前言 技术支持&#xff1a; wx-cropper 裁剪 总体思路是&#xff1a;安装完wx-cropper之后就它当成组件使用。在使用页面的地方引入组件就行。上传图片的逻辑不变&#xff0c;在 通过wx.chooseMedia() Api 拿到图片之后传递给子组件&#xff0c;子组件在拿到图片进行裁剪处理等…

路由器原理

目录 一.路由器 1.路由器的转发原理 2.路由器的工作原理 二.路由表 1.路由表的形成 2.路由表表头含义 直连&#xff1a; 非直连&#xff1a; 静态 静态路由的配置 负载均衡&#xff08;浮动路由&#xff09; 默认路由 动态 三.交换与路由对比 一.路由器 1.路由器…

快速多列查找匹配关键字

实例需求&#xff1a;根据第一列专业名称&#xff0c;在“专业分类指导目录”中&#xff0c;针对三个学历层次&#xff08;研究生、本科生、专科生&#xff09;分别查找对应专业类别&#xff0c;填写在对应位置&#xff0c;即截图中的黄色区域。 需要注意如下两点&#xff1a; …

Linux完成mysql数据库的备份与恢复

背景&#xff1a; 在进行数据报表的测试过程中&#xff0c;为了让我们的测试数据更加真实&#xff0c;因此我们需要同步生产数据到测试环境。方式有很多种&#xff0c;我这里介绍的是通过Linux完成数据同步。 备份数据&#xff1a; 执行命令&#xff1a;mysqldump -uxxx -pxxx…

PyCharm连接远程服务器

要求&#xff1a;PyCharm专业版才支持远程服务 一、创建远程连接 先建立本地与远程服务器之间的SSH连接 1、配置连接 2、建立SSH连接&#xff0c;选择文件传输协议 SFTP 3、设置服务器名&#xff08;可以随意命名&#xff09; 4、配置 SSH连接 点击 172.18.1.202 配置…

bugkuctf web随记wp

常规思路&#xff1a; 1&#xff0c;源码2&#xff0c;抓包3&#xff0c;御剑dirsearch扫后台检查是否有git文件未删除4&#xff0c;参数 本地管理员&#xff1a;1&#xff0c;cu看源码&#xff0c;sci看源码有一串东西2&#xff0c;base64解码后是test123猜测是密码3&#x…

ChatGPT热门项目

1.智能GPT 项目地址&#xff1a;智能GPT&#xff1a;你只要提供OpenAI的API Key&#xff0c;那么它就可以根据你设定的目标&#xff0c;采用Google搜索、浏览网站、执行脚本等方式 主要语言&#xff1a;Python 推荐理由&#xff1a;这是由开发者Significant Gravitas推出的项目…

103基于matlab的极限学习机(ELM)和改进的YELM和集成极限学习机(EELM)是现在流行的超强学习机

基于matlab的极限学习机&#xff08;ELM&#xff09;和改进的YELM和集成极限学习机(EELM)是现在流行的超强学习机&#xff0c;该程序是三者的方法比对。 包括学习时间&#xff0c;训练精度和测试精度的对比。数据可更换自己 的&#xff0c;程序已调通&#xff0c;可直接运行…