面试官您好,我叫XXX,有个两年的java开发经验,掌握的技术栈有SpringBoot、SpringCloud;数据库的话MySQL,中间件Redis、MQ都有掌握;最近在做的项目是智能货柜,该项目主要部署在商圈、小区,用户通过扫码-开门-获取商品-关门的自助式购买,我在项目中主要负责设备服务订单服务,其他的服务也有一定的了参与,请问面试官您有什么想问的吗?

设备服务:存储设备的信息,设备通过MQ与其他服务进行通信

订单服务:扣款结单

微服务划分通信服务设备服务订单服务账户服务通道服务

一、参与设计

介绍:我之前做的是一个智能货柜的项目,部署在在商场、小区等场所。该货柜使用的流程是,用户通过扫码确认免密支付后,货柜打开,用户拿完东西后关闭货柜,自动扣减钱

Leader和运维搭建了一个可视化平台,用于检测堆内存,以及内存泄漏的情况

RocketMQ、Redis、MySQL使用的是阿里云的产品

回答问题先从业务整体来说,问到细节再说细节,没问到也好


有几个端?作用是什么?

  1. 运营管理后台:运营管理后台是给我们运营公司的管理人员用的,主要作用有基础数据的管理和统计分析功能。
  2. 供应商后台:供合作商使用。主要作用是合作商查看自己点位的分成【某个摆放货柜的地点(学校/小区/写字楼/门店)卖出去的订单,赚到的钱要按约定比例分给合作方】
  3. 运营移动端:供运营企业的运维和运营人员使用。主要作用是处理工单业务,比如说补货、售后处理。
  4. 用户小程序端:供C端用户使用。消费者扫描售货机上的二维码可以打开此端,我们研发负责小程序前端和后端接口一起配合完成整个购物流程。
  5. 设备端:没有界面。安装在每个售货机中,主要作用是接收服务端发来的出货请求,调用硬件完成出货。

系统一共分为哪几个库?有没有做分库分表?大概多少张表?

所有业务数据都在同一个 MySQL 库中,没有做分库分表,我们采用的是TIDB的方式,主库+历史库。

核心的表大概有30多张。


你有没有参与过表设计?如何进行表设计?

设计数据库表,我的经验就是先从产品原型中提炼出表名称,比如产品原型中,有一些基础数据的维护,那么这些大概率都是一些基本表。这类的表设计相对比较简单,就是从产品原型中提取输入和输出项,此外,我还会加上逻辑删除字段、状态字段、创建日期和更新日期这样的字段。 另外还有一些经验,虽然理论上我们要遵循三大范式,但是实际上是要有变通的,比如空间换时间。


项目有多少个接口,多少个页面?

【没有固定答案,合理即可】

项目接口大概有80多个接口。页面总共大概40个页面左右。


你们项目组有多少人?人员构成是什么? 开发周期怎么定?

image-20251223091042390

我们项目采用敏捷开发,分三个阶段:

  • 第1阶段:4个月,实现项目基础业务功能
  • 第2阶段:2个月,实现项目增强功能。
  • 第3阶段:维护期,每周都会更新迭代一些功能,周四发版。

你们项目的开发流程是什么?

项目采用前后端分离的模式开发。

  1. 项目启动后,由产品经理用了大概一周时间设计了第一版的产品原型,召开两次需求评审会来确定最终的需求。

  2. 接下来设计组开始进行效果图设计,开发组进行技术研讨会确定技术选型,并确定接口文档,与前端人员确认。

  3. 接口确认好,前端就按照效果图、接口文档进行前端代码的开发,而后端就按照产品原型、接口文档进行后端代码的开发

  4. 前后端开发完成后,前端和后端进行前后端联调。

  5. 最后测试、上线部署。


微服务之间通信

  • 正常服务之间就是用的 fegin
  • 异步的操作就是用 mq
    • 设备订单计算好商品,mq 通知账户服务结算订单,还要通知补货

负载

外部:nginx,外部请求进来以后,打到哪台入口机器

内部:Feign 负责发请求,具体选哪个实例由 LoadBalancer(旧的是 Ribbon)决定。


设备服务与设备之间的通讯

我们使用的是mqtt,用的是阿里巴巴的封装好的sdk

image-20251224145421647


项目中有哪些难点、挑战

设备难点

难点:设备的命令的不确定性;设备的不稳定性。比如 不发开门指令open,不发关门指令close,心跳的自我保护机制。

难点:设备的一些网络状态、命令的不稳定性。

因为我们是做软件开发的,公司还会有采购,他们不会和我们沟通要采购那家公司的设备,所以我们开发的过程中很无语的,有的设备坏了,有的设备数据上传不了,有的设备好好的突然坏了,莫名其妙。还有就是前后端联调也是有很大的困难的。

前端后联调

当接口测试没有问题后,需要进行前后端联调。前后端联调中如果遇到问题,首先根据http状态码判断无法联调成功的原因,404表示地址不对,500表示后端报错。如果404就要检查是前端和后端,哪一个没有按照接口文档编写地址导致无法对接成功。如果500,就要查看后端控制台的报错信息,根据异常信息找到报错的语句。这种情况大多数是因为参数传递不正确。

根据上述方式,基本可以断定是前端的问题还是后端的问题。如果是后端的问题,根据报错信息无法找到原因的,可以再尝试在关键代码上打上断点,逐条运行,观察变量的内容是否和预期结果一致。


二、核心流程梳理

image-20251223151750859

image-20251223151818096

image-20251223151849015

image-20251223151920753

image-20251223151940616

具体购买流程

image-20251107134217733

用户扫码,后端会对参数进行校验,判断用户是不是在黑名单中;校验成功后,通过加分布式锁将订单落库,再请求到支付宝/微信做一个预约下单,授权预约下单后会给前端发送一个签约的package包,用户同意后,我们会收到支付宝/微信的回调,之后会调用设备服务开门,在开门之前会记录所有货仓的重量,用户卖完东西后,再次记录所有货仓的重量,用这两个做差值得出用户买了什么商品;通过MQ发送消息调用账户服务结算,释放掉分布式锁,在账户服务中会涉及到优惠券、积分、余额的处理,同时这个消息会发送给一个触发补货的服务,这个服务会去查看设备是否需要补货,如果需要,通知调度人员去补货

更具体地流程

image-20251107135258460

image-20251224145617887


数据库表

image-20251107153321968


设备上线

image-20251107140226600

设备服务收到设备登陆服务器的请求,会查看是否有该设备:

  • 如果有该设备:修改设备表的设备状态为在线,更新设备缓存状态,更新设备服务中设备通信信息,更新通信服务中设备、Channel信息
  • 如果没有该设备:添加该设备,修改设备表的设备状态为初始化,添加设备缓存

设备的管理,设备的维护

  • 设备有仓门,仓门有货道,如果是重量柜,每个货道卖一样的商品

  • 每个设备都有对应的仓库,每个仓库有负责的补货人员

  • 每个仓库有各自仓库的库存管理,设备每个货道可以配置补货阈值,低于阈值触发补货调度任务


心跳/下线流程

最开始的设计是

  • 设备每30s向通讯服务发送心跳报文,通讯服务解析后向Redis存入一条消息,key:sn码,value:null,过期时间设置为60s
  • 在设备服务中开一个定时扫描任务,如果key过期了,就判断设备下线

但这存在一个问题:设备可能因为网络波动频繁下线


于是我们又引入了心跳保护机制,为了避免因为网络波动问题,导致的大面积设备下线,造成误判

  • 设备每30s向通讯服务发送报文,里面包括了心跳时间,重量等设备信息,通讯服务解析后向Redis存入一条心跳消息①{设备sn码:heartbeat,当前时间戳}
  • 在设备服务开一个定时任务,通过redis命令zrangebyscore查找①超过60s并且没有心跳的设备,如果有这样的设备,就会判断是否满足心跳保护机制,决定设备是否下线
  • 心跳保护机制:我们的设备有2500台左右,一分钟正常能收到5000次心跳,我们设置了一个阈值80%,如果一分钟收到的心跳总数小于5000 * 80% = 4000 次,就会认为是服务器故障,不会让设备下线,通知运维人员检查;
  • 记录一分钟心跳总数,我们按分钟分桶:key 里带分钟时间(比如 ②{heartbeat:cnt:时间,数量})。心跳到达通讯服务时 INCR 该 key,设备服务定时任务每分钟 GET 上一分钟 key 的值即可,并设置过期避免堆积。
  • 如果不满足心跳保护机制,将该服务下线,更新数据库设备的状态,更新缓存状态

{heartbeat:cnt:时间,数量}里面的数据可能:{heartbeat:cnt:202512241106,4800}{heartbeat:cnt:202512241107,4500}

Redis 的 INCR key 有个特性:

  • 如果 key 不存在,Redis 会把它当成 0,然后执行 +1,结果变成 1
  • 所以下一分钟的 key 不需要提前创建第一条心跳到来时就自然创建了

重量

image-20260104164633670

  • 仓门没有打开之前是每30s上传一次重量信息,在仓门打开5s之后,每300ms上传一次重量
  • 设备向通讯服务发送报文,里面包括了心跳时间,重量等设备信息
  • 服务器接收到数据后,更新缓存,发送MQ,目的是进行削峰,通过MQ再落库
  • 我们还有一个服务式定期删除这些数据,只会保留七天的重量信息记录到TIDB中

使用重量柜产生的问题

🚀因为选择了重量柜这种性价比的柜子,有些问题是无法解决的

同一个货道,用户拿走,又丢一个相同重量的东西,不扣款

  • 当用户拿走一个商品,检测到重量的减少会记录扣款,我们的货柜是每300ms记录一次重量,很容易检测到。
  • 对于丢了一个相同重量的东西,由于钱已经扣除了,因此不会造成我们的损耗

同一个货道,用户拿走,又丢一个更加重的东西

  • 当用户拿走一个商品,检测到重量的减少会记录扣款,我们的货柜是每300ms记录一次重量,很容易检测到,因为支付宝/微信一旦结单后就不能够进行扣款操作了。
  • 当用户丢了一个更加重的东西时,货道也会有一个重量检测,告警处理,订单设置异常
  • 最终,用户会因为拿了一个商品而扣钱

用户从货道 1 拿出东西,不想买了又放到货道 2 里了

  • 货柜上贴有标志:请不要随意打乱货柜内商品的货道

  • 货道 1 的重量还是会降低,货道 2 的重量会变高,我们货道也会有一个重量检测,比如货道 2 重量增加的很多,会给货道 2 标记可能异常告警,通知人员去排查

  • 用户关门,还是会扣货道 1 减少的商品,该扣还是扣,用户发现扣钱后会投诉联系客服,管理人员会进行排查,通过每个货道的摄像头,看描述是否一样,货道 1 是否重量减少,货道 2 重量是否增加,没有问题进行退款


怎么计算用户买了什么商品?

我们是通过记录开门前后的重量对比计算,通过误差阈值控制,如果超过了阈值,客户联系才进行退款,如果在阈值之内,就认为是是资损


货柜已开门但是设备下线了怎么办?

首先开门前还会检验一下设备状态,此时下线后直接将该订单改为异常

如果开门后设备下线,不会影响结单,只是将设备的状态改为了下线,但是还是可以发送关门指令的

如果设备开门了并且发送不了关门消息,订单也会设置为异常,可以人工结单


设备补货、调度流程

image-20251110105942344

  • 每一次设备服务向MQ发送消息结单,对商品的库存扣减的同时,会触发货道是否需要补货
  • 如果达到补货的阈值,会通过飞书通知补货人员前去补货
  • 补货人员先检查设备状态,再通过h5页面扫描二维码调用补货的接口,加分布式锁,创建补货订单并落库,设备服务调用开门,缓存开关门的重量来判断商品数量,增加库存,更新补货订单状态

对账流程

image-20251110134913575

外部对账

】支付宝次日9-10点和微信次日10点会给我们 结算账单 + 交易账单,结算账单是对“钱”,交易账单是对“订单”,交易账单我们会从 OSS 流式读取,按 5000 条一批做分片对账。

【了解】结算账单的一个excel表格,结算账单会告诉我们昨天结了多少钱,什么时候到账,到账到哪里,那个支付公司都会给我们。

【了解】交易账单的excel表格,需要我们每一笔都去对,主要是交易订单和系统订单的订单状态,看支付公司和我们数据库记录的状态是否一致;对订单金额对他收取的手续费是否一致


内部对账

每天晚上0点看一次重量,算出库存量,加上白天的补货计算出【库存的减少量】与【订单的商品数】是否一致,如果不对,会调取监控、查看一些重量变更记录


生成报表流程

报表就是账单

  • 通过线程池 + CountDownLatch,生成不同维度的报表,每个线程处理一个维度,等每一个线程都处理完,再进行汇总,处理接下来的任务

不同的维度

  • 以商品数据
  • 以地区统计数据
  • 不同时间的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ExecutorService pool = Executors.newFixedThreadPool(3);
CountDownLatch latch = new CountDownLatch(3);

// 以商品数据
pool.submit(() -> {
// ...
latch.countDown();
});

// 以地区统计数据
pool.submit(() -> {
// ...
latch.countDown();
});

// 不同时间的数据
pool.submit(() -> {
// ...
latch.countDown();
});

latch.await();
// 等待latch都执行完了后,再执行对应的任务


营销体系

image-20251110141238203

  • 优惠券分类
    • 优惠券大概分为三种,满减券、现金券、团购券;满减券就是达到多少金额就可以减一部分钱;现金券就是无门槛卷;团购券是购买指定商品不需要钱
  • 优惠券的获取方式
    • 我们可以通过活动,秒杀、新人用户、拉新人的方式获取优惠券
  • 优惠券的使用规则
    • 优惠券的使用规则可以简单配置,比如团购券可以一次性多使用几张,而现金券、满减券的配置规则只能一次使用一张

优惠券秒杀

image-20251110142317867

优惠券的秒杀功能主要使用Redis减少数据库的压力,mq异步消峰处理落库

在秒杀之前,向Redis中加一个set结构的数据做缓存预热,key就是秒杀券id,value就是库存量

前端会采用滑动验证防止用户连续点击,后端采用Redis的decrement命令扣减Redis的库存,返回值如果 $\ge 0$表示抢购成功,同时往Redis的Set集合中加一条用户抢到的记录,限制一人一单,发送mq进行后续业务操作。


充值流程

image-20251110143009175


退款流程

image-20251110143133453

一般退款都是因为商品质量问题,或者说拿了别人放错货架的商品,导致扣款扣多了,系统仅支持七天内退款(数据库只会存七天内的重量信息),退款需要客户联系客服,我们后台进行退款

  • 解决超退
    • 一个订单多次退款会有限制:where 条件 已退金额 + 本次退金额 $<$ 支付金额
  • 退优惠券
    • 我们采用的是先退优惠券再退钱
  • 退积分
    • 我们会按照比例扣除积分,如果积分已经被用户使用了,那就只能算给用户的补偿,因为大多数退款都是因为商品的问题

扫码登陆流程

小程序如何获取用户唯一标识(openid)?

前端组装appid调用微信接口得到jscode,把jscode给后端,后端用jscode请求wx可以得到用户的唯一标识(openid)

支付宝文档

image-20251110144610337

微信文档

image-20251110144644939


支付宝、微信支付回调的幂等性是什么做的?

image-20251223110246846

如果支付宝连续回调两次怎么办?

做接口幂等

  • 就是分布式锁,避免两条东西同时修改

  • MySQL 的行锁,唯一索引保证唯一

  • 通过乐观锁,update ··· where 支付状态 State = 支付中,通过这种一个更新成功,一个更新失败

如果两次结果不同怎么办?第一次支付失败,第二次支付成功

  • 这种都会做告警处理
  • 然后我们都会再查支付状态,微信支付宝都会提供订单查询的接口,查询一下到底是怎么回事

支付宝和微信 有啥区别?

最大的区别:

  1. 支付金额的单位不同

    • 微信的单位是 分 (且数额≥0)

    • 支付宝的单位是 元 (数额≥0.01)

  2. 微信需要知道支付详情,支付宝不需要知道。

  3. 支付宝下单只能有1次,比如支付宝有未支付的一笔订单那么就不会开启下一次订单

    • 微信则有3次
  4. 支付宝退款是同步接口,微信退款是异步接口


区域价格设置

分散剂来存储商品价格

  • 货柜价格表(一级):货柜sn码——货道id——商品id——价格
  • 区域价格表(二级):区域——商品id——价格
  • 商品价格表(三级):商品——价格

查询价格时,先从一级价格表查询,如果没有该商品的价格的话再去二级商品区间表查询,如果还没有的话,就去查询三级价格表(三级价格表式一定有的)


通讯服务宕机怎么解决?

image-20251224160341073

设备——>通讯服务

我们给通讯服务配置了一个固定的入口:mqtt.xxx.com,这个域名的DNS会被设备解析为某个通讯服务的IP(主服务器)比如mqtt.xxx.com——>1.1.1.1

设备通过这个域名去访问通讯服务,实际连的是解析域名得到 IP -> TCP 连接 -> 建立会话

如果主通信服务挂了,设备会尽快/在 TTL 窗口内把这个域名切到另一个备用通讯服务上

这就是域名切换(DNS 切换)

  • 原来:mqtt.xxx.com -> 1.1.1.1(主)
  • 故障后:mqtt.xxx.com -> 2.2.2.2(备)

设备服务——>通讯服务

设备服务:通常是调用一个服务名(比如 comm-service)→Feign 负责发请求,具体选哪个实例由 LoadBalancer(旧的是 Ribbon)决定


有用到RocketMQ吗/为什么要使用RocketMQ?

我们的项目是一个分布式项目,而RocketMQ支持分布式事务消息,分布式消息事务是通过 半消息 + 本地事务 + 事务回查 来实现的

  • 通信服务 通过 消息队列 和其他外部服务进行信息交互,没有接口的提供,只有接收消息、发送消息跟其他服务进行交互

  • 关门 设备服务 收到商品详情的时候,通过消息队列发送给账户服务去结单,另外一个发送给设备服务的补货 通知它补货

  • 所有发短信,就是发送短信通知运营人员校准或者补货,发短信通过通道服务的消息队列去发送短信

  • 所有延时任务:签约查询、查询开门等


有用到Redis吗?

缓存设备信息:货柜——>通讯服务mqtt——>设备服务,在设备服务这里进行Redis缓存(Redis中key是device_id(设备id),value是一个大对象序列化成JSON(设备sn唯一编码,商品信息,仓门信息,state,重量、last_heart_time、通道服务id))

分布式锁:开仓门下单的时候,利用Redis做分布式锁。同时补货、换品、改价也用的是这同一把锁


有用到分布式事务吗?

下单时订单库、账户库都有写操作,我们用 Seata AT 做分布式事务:一阶段各分支本地提交并写 undo_log,二阶段统一提交/需要时用 undo_log 回滚。

为什么用 Seata AT 模式?

技术选型的时候考虑到它是阿里的产品,同时侵入小:基本不改业务代码,入口加 @GlobalTransactional;AT模式下的最终一致性也符合我们的需求。吞吐量高


分布式锁

我们在下单的时候会用到分布式锁,因为我们要保证同一时间一台设备只能有同一个人购买,所以这里使用了分布式锁

使用了 Redis 的 原生锁,SET key value NX PX ttl,key使用的设备维度lock:device:{Sn},value使用的随机token

  • NX:只有当 key 不存在时才设置成功
  • PX ttl:给 key 设置过期时间,单位是毫秒。

加锁成功后,创建订单落库,把token存到订单表中的lock_token字段

解锁的时候,通过订单号查询Snlock_token两个字段,执行Lua解锁

1
2
3
4
5
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end

没有使用Redisson看门狗机制,它要求JVM是在一个线程中(面试时不说,问了再说)


MySQL和Redis的双写一致性

1、Cache-Aside(旁路缓存):写更新库+删缓存,读缓存未命中查库回填,简单常用

2、延迟双删方案:先删除缓存,再更新数据库,延迟再删除一次缓存

3、canal订阅MySQL binlog日志 + MQ + 重试缓存删除


MQTT协议

image-20251110145027292

设备服务调用通信服务给设备发指令:

image-20251110145049429

设备给通信服务发送报文:

image-20251110145109942


设备发送命令不稳定处理

image-20251110145150934


xxl-job兜底

用户扫码登录确认授权的同时会发送延时消息,一分钟后检查该订单是否授权,还没授权就视为取消订单, mq 延迟消息发失败了怎么办

  • xxl-job 定时任务兜底,看哪些订单长时间未授权,取消掉。

使用了什么设计模式?

单例模式

线程池中使用了单例模式,通过单例模式,确保线程池的唯一性,一般的业务的使用同一个单例的线程池,共享连接池,便于管理和监控

模板模式

BaseException:定义基础的异常抽象类,然后不同类型的异常再去实现基础异常类

工厂+策略模式

  • 像我们的支付服务的支付方法,我们会把所有支付渠道中的公共代码抽取出来,定义一个抽象类
  • 然后根据不同的支付渠道,定义多个实现类,策略服务
  • 然后对这些不同的 payService 服务,使用工厂去统一管理,存到工厂中的 payServiceMap 中,当使用不同的渠道服务时,就调用它的 getPayService

接口限流有哪些方式?

  • 漏桶:控制消费速度,来了先放链表后面,前面的慢慢消费
  • 令牌桶:一秒就放 10 个令牌,能拿到就往下走
  • 线程池:线程数量控制
  • 信息量,semphore:但是只是单机的,集群限制 50 个该怎么做?Redis 中有一个集群化的semphore

使用Redisson的分布式信号量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 初始化
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7000");
RedissonClient redisson = Redisson.create(config);

// 使用信号量
RSemaphore semaphore = redisson.getSemaphore("mySemaphore");
semaphore.trySetPermits(50); // 设置总许可数

// 获取许可
if (semaphore.tryAcquire()) {
try {
// 执行业务代码
} finally {
semaphore.release();
}
}

MySQL调优

建立索引

我们并不是一股脑加索引,而是使用慢SQL日志抓最慢 SQL,找到哪些SQL语句执行的比较慢,尽量做覆盖索引,避免 filesort 和回表。

像货柜订单列表这种高频接口,我们会围绕设备 sn、订单状态、创建时间建联合索引,例如 (device_sn, status, create_time),让最近订单查询从扫描全表变成走索引。


主库/从库 TIDB

我们只能货柜项目这个项目开始的数据量不大,就没有考虑过分库分表,但上线一年的时间里,目前已经有2个亿的数据了,主要的数据在订单表、订单详情表中

当数据量太大影响查询性能的时候,我们采用了tidb,主库+历史库的方式,tidb我们的leader也比较熟悉

对于订单表,我们现库保留三个月的数据,差不多1000w左右,历史库通过Cloadcanal伪装成MySQL的从库监听bin log实时同步到tidb中(具体的是dba做的)

tidb呢,作为分布式数据库,查询单表2亿的数据,性能可以到秒级

由于我们需要MySQL的事务能力,因此我们的主库还是MySQL,查询是去找tidb的历史库


建立唯一索引

我们支付回调和 MQ 消费链路都可能重复触发,防止将多条脏数据存入数据库中,所以我们在数据库层做幂等。原本的方式是先判断有没有支付回调这条记录,没有的话再插入。但是后来发现如果回调量上来以后,数据库就会多很多”无意义的读“操作

1
2
3
4
SELECT id FROM pay_callback WHERE out_trade_no = ?;
-- 不存在再 INSERT
INSERT INTO pay_callback(out_trade_no, pay_status, raw_body, update_time)
VALUES(?, ?, ?, ?)

所以后来我们就换了一种sql写法,首先将支付回调的Id,out_trade_no设置为唯一索引,用的是INSERT ... ON DUPLICATE KEY UPDATE,它会先判断数据库里没有这个 out_trade_no,没有正常插入一条新记录,有的话不报错!转而执行 UPDATE,把这条记录更新掉

1
2
3
4
5
6
INSERT INTO pay_callback(out_trade_no, pay_status, raw_body, update_time)
VALUES(?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
pay_status = VALUES(pay_status),
raw_body = VALUES(raw_body),
update_time = NOW();

结果就是用一条 SQL完成“首次写入”+“重复来的更新”,并且是原子的。


JVM调优

做过。

  1. 最初设备就100台

  2. 后来设备越来越多,到了600台,通过排查发现持续发心跳 发重量 会导致通讯服务新生代的Eden区持续发生空间担保机制,将整个Eden区放到老年代(其实是不用放到老年代的,因为不是长期存活的对象)

  3. CPU处理速度变慢或导致频繁的full GC

怎么解决?

  1. 首先把每个通信服务我们最多让它连接300台~500台设备,新增的设备连到新的通信服务上。

  2. 通信服务做了集群,一共5台,缓存记录了每台设备连接的是哪些通信服务

  3. 正常情况下,老年代 : 新生代 = 2:1 将其调成 老年代:新生代 = 1:1

-XX:NewRatio=2 代表 老年代:新生代 = 2:1(默认常见值之一)

-XX:NewRatio=1 代表 老年代:新生代 = 1:1

  1. 再将新生代的 Eden : Survivor : Survivor = 8 : 1 : 1 改成 Eden : Survivor : Survivor = 3 : 1 : 1

-XX:SurvivorRatio=8 代表 Eden : Survivor = 8 : 1(两个 Survivor 各 1)

-XX:SurvivorRatio=3 代表 Eden : Survivor = 3 : 1

大部分的JVM调优,都是调堆里面的新生代和老年代的比例,还有新生代 Eden区和Survivor区的比例。还有一个就是大对象的阈值。


三、数据库

设备表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CREATE TABLE `device_info` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`uid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '设备id',
`device_sn` varchar(64) NOT NULL COMMENT '设备sn',
`device_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '设备code',
`device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '设备名称',
`device_status` char(1) NOT NULL COMMENT '设备状态 0在线 1离线 2异常 3初始化',
`device_type` char(1) NOT NULL COMMENT '1可耐 2益诺 3mqtt',
`heating_switch` char(1) NOT NULL DEFAULT '1' COMMENT '加热开关0开 1关',
`heating_threshold` int DEFAULT NULL COMMENT '开启加热阈值温度',
`close_heating_threshold` int DEFAULT NULL COMMENT '关闭加热阈值温度',
`province` varchar(32) DEFAULT NULL COMMENT '所在省',
`city` varchar(32) DEFAULT NULL COMMENT '所在市',
`county` varchar(32) DEFAULT NULL COMMENT '所在县',
`community` varchar(32) DEFAULT NULL COMMENT '所在社区',
`address` varchar(255) DEFAULT NULL COMMENT '详细地址',
`device_desc` varchar(255) DEFAULT NULL COMMENT '设备描述',
`lgt_on_time` varchar(255) DEFAULT NULL COMMENT '灯箱开启时间段',
`addr_location_gd` varchar(255) DEFAULT NULL COMMENT '高德经纬度',
`addr_location_tx` varchar(255) DEFAULT NULL COMMENT '腾讯经纬度',
`device_version` varchar(64) DEFAULT NULL COMMENT '设备版本号',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_date` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_date` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标记0不删 1删',
`remarks` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备详情表';