我的物联网项目(一)开端
我是一名java程序员,我很喜欢用这个称呼来描述自己的职业,因为简单易懂。我从事软件开发工作行业也有十余年,和大多数的同行一样,经历过很多公司,也做过不同的岗位,其实我给自己的评价是到目前为此,我的骨子里依然是一名技术人员,回忆当初穿着整齐的职业装,安静的坐在工位上,默默无闻由上司分配功能模块,闷头开发模块,然后看着自己的模块被用户使用心里有种欣喜若狂的感觉,现在回想起来那才是我珍贵的程序员生涯,在那里我每天像海绵似吸取大量的技术知识点,在那里我认识很多可以在工作中讨论技术的同事,甚至下班后一群人可以偷偷在公司里面玩一把魔兽,最后一起坐晚上八点的班车回家,那段时间我虽然单身但不孤独,所以我在我后面的职业生涯里面带团队,我会非常认真并耐心的对新入行业的新手多多关照,因为我知道这段时间对他们后面的职业生涯相当难忘,我想让他们记住我(哈哈)。这种生活过了两年,中途由于部门各种情况再加上自己两年的成长觉得自己完全可以独当一面,说简单点,我想出去看看外面的世界,吸取一些新的技术知识点,所以我离开了这家让自己成长起来的公司。
后面连续三,四年时间里面,我的关注点依然在新的框架,新的技术,新的架构上面,每去一家公司,用最快的时间把公司所有的代码浏览完毕,把写代码的风格,封装的技巧,架构的模式,疑问问题的解决方案牢记于心并一一记录在本子,我那个时候给自己的定位是我要做一名技术架构师,所以觉得自己技术知识点掌握的越广,越多,尽量越精就足够了。现在想来,着实好笑,其实真正的做技术架构骨子里面还是在做业务架构。当然,也是当初前面的五年里面关注点不一样,所以也造就了我现在在技术这条路上要走这么远,回想当初也见过很多做了三,四的技术同事转型到纯项目管理,多年过去了,也不知道现在怎么样了。
再后面二,三年来,我依然对新的架构,没用过的中间件,开源的技术欣喜若狂,希望能在项目中用起来并加深对它的一些理解,但是我更多的是基于业务方向去做分析,基于业务的可行性去做技术架构,我开始不太关注下面的人代码的实现方式,我更多的是去做一些平台的建设,平台的规划,我开始写一些PPT来表达我的想法,尽量将PPT弄漂亮点重要的是人人都看的懂,当然我每天还得花2个小时来写代码,一来人手不够我自己也乐意写一些核心代码,二来有些东西我必须亲自通过代码来验证我的想法,然后再完善我的设计。最重要的是做技术不能光有想法,而且还需要有能力去实施,我要保持这种能力。
再到现在最近三,四年里面,我的工作主要在业务分析驱动型技术架构,后台服务器架构,团队建设这些方面,这段时间里面成就依然平平,唯一感到能说的出口的那就是自己利用了3年时间阅读了大量的心理学方面的书籍,主修了大学应用心理学的所有的教材,并考了心理咨询师相关从业证,中途还有帮别人做过一段时间心理咨询,我收费不便宜,具体详聊(哈哈)。
以上算是我个人的简单介绍,也算是我写这个物联网项目系列的博客开端,我主要是想把一个完完整整的平台项目从无到有,中途遇到的所有的技术问题,业务问题,甚至人际关系问题等等都一一分享出来,也欢迎大家提出宝贵的意见,一起聊聊技术,聊聊人生。
我的物联网项目(二)初建团队
今年三月初也是机缘巧合下接触这个项目,说实话也是非常看好这个项目,以前做过企业级项目,互联网项目,但是物联网项目也是第一次接触,相关资料了解了下何为物联网,原来是物物相连的互联网,与传统行业相结合的互联网。首先简单描述下这个项目的主要业务:
摇摇车又称摇摆机,摇摆车,其实就是儿童投币那种摇摇车,是游乐设备中的一种,大多在路边商家门店,比如超市,母婴店较多,在一些商场也经常见到也有它的存在。存在即是合理,这里不过多讨论经济学的东西,我们其实要做到就是在传统的摇摇车身上赋予更多的使用方式,并连接互联网,用互联网的思维创造更多的价值。好啦!就说这么多,大概就是这么个事情,我来项目的第一天所了解的东西也是从这里开始。我来这个项目,里面已经有个硬件经理和产品经理,硬件经理已经在研究摇摇车本身信号启动的原理,就差软件这边发送协议对接,而产品经理一直在研究用户APP的使用功能,但是整个平台的架构,业务流程,基础设施几乎没有开始,开发团队也在陆续招聘中,那么我的工作该如何开展,打造这个平台前期工作应该做什么成为我马上要去考虑的重点。
首先从业务需求开始,一切的软件平台建设,业务需求分析工作是重点之重。创业型项目,尤其是新项目业务分析由于时间,成本,市场的客观原因,即不能天马行空,也不能精雕细刻,所以我需要在之间找到一个平衡点,至少能找到一个让大家对这个平台有信心,能立马见效的东西出来,所以平台整体规划,主干流程,人员角色功能才是前期软件整个体系的工作重点,一旦这些梳理清楚,马上可以搭建平台,数据库设计,开发工作进行了,具体的业务尤其是页面上的细节功能,可以在后续慢慢完善不影响到大局。
其次就是技术架构选型,还是由于时间,成本的因素,在技术选型上既不能有门槛,还要开发,测试,部署简洁方便迅速,(后面章节我会详细描述这个项目的前期搭建)主要为后续招人降低要求。
最后就是技术人员招聘,打造团队人数规模。如果我说如果,有更多的资金,当然每个领域角色招聘一个牛人是最好的,沟通写代码速度也快,如果考虑到成本,那就招一些有过1,2年左右工作经验的也可以,前提是需要一个有经验的人来规划好每一步。尽量不要招实习的没有任何项目经验的人员,毕竟项目和团队不成熟,每个角色又只有1,2个,沟通和实施成本非常高,会影响到后续的整体布局,优秀的毕业生例外。
以上三点大概花了二周左右时间完成,心里也有了底气,梳理了大概主干流程如下:
- 平台生成二维码->工厂组装->系统设备入库
- 地推人员线下推广->商家意向合作,注册商家账号->后台客服人员审核->投放摇摇车数量->运维人员收到工单,运送摇摇车到商家->运维人员将摇摇车和商家绑定
- 用户下载APP->注册账号->免费玩三次(充值,多冲多送)->扫码启动摇摇车->商家和平台劈帐(每一笔订单商家分成和平台分成)->商家提现
最后总结下配套的软件平台功能。
- APP。C端,主要给用户使用。APP包括android和ios,主要功能有我的账号(包括账号注册,累计消费次数,余额,充值,赠送等),我的用车卷,历史记录,扫码启动,周围投放车辆这些核心模块。
- 微信公众号(B端)。B端,主要给商家使用,也给运维,地推使用。主要功能就是商家中心(包括交易明细,设备列表,结算管理,账单中心,银行卡等),运维中心(投放订单,解绑订单,更换设备等),地推中心(客户管理,订单管理,结算管理等)。
- 运营平台。主要给平台管理员使用,主要核心功能包括客户管理,商家管理,订单管理,结算管理,绑定解绑,生产管理,平台管理等。
我的物联网项目(三)平台架构
介绍下目前整个软件开发团队的配套成员
技能 | 人数 |
---|---|
android | 1 |
ios | 1 |
前端 | 1 |
美工 | 1 |
java | 2 |
以上就是我们这个项目的人员搭配,我除了项目上的管理,更多的是在做业务需求,系统架构,平台建设,然后也顶多算半个开发人员参与在写代码。
一 系统架构
从前面的业务需求描述,到现在的人员配置,再加上时间进度上要求二个月出产品,在技术架构选型上基本首先考虑到是单体模式架构,采用传统的MVC开发模式,java开发人员是整个项目的核心担当,项目管理使用maven,运营平台UI采用的是容易上手的jquery easyui,后台采用的是springMVC+mybatis,数据库毫无疑问用的是mysql,app用的是原生语言开发,微信前端页面由美工设计(包括app),前端切图成html,交给java开发,整个分工大概就这样。
项目结构如下:
项目名 | 描述 |
---|---|
orange-parent | 定义整个项目基本群的基本信息,依赖插件信息,以及自定义信息 |
orange-framework | 定义spring curd,rest服务,MVC等基类和模板 |
orange-core | 核心项目(dao,entity,mapper) |
orange-utils | 丰富的常用工具包 |
orange-redis | 分布式缓存redis组件 |
orange-serviceweb | 业务接口,主要对接app |
orange-wechat | 微信平台 |
orange-adminweb | 运营平台 |
orange-mqtt | mqtt协议组件 |
二 物理架构
所有基础设施服务器首选阿里云,我不是在替阿里云打广告,如果非要觉得我在打广告,麻烦各位在阿里云买服务器的时候选择下我的幸运卷(哈哈),后续在服务器选型和部署有相关问题可以找我一起探讨,幸运卷地址:
言归正传,像这种创业型互联网项目首选阿里云是比较好的选择,我指的不单单是阿里云提供的ECS服务器,更多看重的是阿里云的整套解决方案。比如我们要做tomcat集群,可能要装nginx,nginx要高可用,就得通过keepalive主从双机热备自动切换,就光这么一套配置下来,你少说也要好几台服务器吧,而且还不一定稳定,更加没有可视化控制台监控各项指标,但是在阿里云你根本无需花这些时间来弄这些,去买个SLB负载均衡即可搞定,而且又便宜。再比如数据库mysql,前期就算不做集群,你也要做主备高可用吧,如果二台mysql做高可用,你还得需要keepalive或者haproxy,这么一套配置下来,成本也不低。说实话,我觉得阿里云数据库RDS前期配置在同等的情况下更加便宜方便,毕竟RDS mysql本身还自带了控制台可以方便监控各项参数排查问题,阿里云数据库RDS在后期的配置升级的确是成本昂贵,这个时候自己用一些廉价的服务器搭建数据库集群是可取的,当然这个是后话了。还有文件服务器就算采用fastdfs搭建也需要一台服务器,总的算下来,还是不划算。
平台ECS的配置:
项目名 | 描述 | ECS服务器 | SLB负载均衡 |
---|---|---|---|
orange-adminweb | 运营平台后台 | 1台 [4核8G5M | |
orange-mqtt | mqtt中间平台 | 2台 [4核8G5M] | 1个[主备5M] |
orange-serviceweb | 核心业务层 | 2台 [4核8G5M] | 1个[主备5M] |
orange-wechat | 微信业务 | 1台 [4核8G5M] |
平台数据库配置: 高可用通用型4核8G
平台redis配置: 4G主从版
平台文件存储: OSS买流量包
平台mqtt配置: 按连接数上限购买,前期买2000,后期继续升级(说实话,这个和RDS一样越往后面升级配置越贵)
我的物联网项目(四)订单系统
订单系统是整个平台的独立的核心业务流程,它本身并不复杂,最初的原始需求如下:
1.用户打开app,登录进入主界面。
2.点击扫码,扫码摇摇车身上的二维码。
3.app显示扣费,摇摇启动。
4.用户订单中心显示消费明细,商家订单中心显示收入明细。
长期写代码形成的思维习惯,脑袋里面立马意淫起来,浮现出一幅画面:
路边有辆摇摇车,用户张三打开app,迫不及待的扫码摇摇车,系统提示,未登录,张三输入手机号码和密码,登录成功,可以扫码了,这个时候系统又提示钱不够,张三只能通过app充值,充值成功后,继续扫码摇摇车,终于app提示扫码成功,消费1元,1秒钟后,摇摇启动了,用户可以在用户中心看到每次都消费明细,商家可以通过微信登录商家中心,查看每天都收入,到达一定的资金量后,可以申请提现。
从这个需求里面,大概知道一个整体流程应该怎么走,下面要做到就是根据具体的业务场景做功能设计,依然是程序员的惯性思维首先想到的是: 用户点击扫码摇摇车,然后摇摇车启动这个过程怎么交互?
其实我前面有说到过,摇摇车信号启动这个过程,已经有人在研究并打通,软件这边只需要发送相关协议即可。由于硬件这边是C语言,两边的通信采用什么方式需要考虑。当然,物联网项目现在也比较成熟,相关的解决方案也大把,我们用的是阿里云的平台,阿里云这边已经提供物联网套件的整个解决方案。我们采用的MQTT协议作为软件和硬件的一个交互中心,光听MQTT名字就大概知道MQTT属于消息中间件了,它和传统的MQ系列产品(如RabbitMQ,ActiveMQ等)稍微不一样,MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议,主要用于小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量,像摇摇车(摇摇车身上装了个智能盒子,盒子里面有个2G流量卡,盒子发信号给摇摇车)这里面用的就是2G的流量卡这种业务就非常适合这种。
大概的通信过程就是如此简单: 软件应用层发送协议到MQTT中间件,摇摇车身上的盒子订阅消息,一旦收到消息,盒子就发送启动命令给摇摇车,摇摇启动。
所以订单系统的第一次发版流程如下:
备注: 由于涉及到平台的一些机密,订单系统不可能详细描述到传某个参数,怎么去数据库查询,甚至贴核心代码上来,如果有感兴趣的技术朋友,可以单独探讨!
整体功能性开发差不多,核心主干流程一旦打通(到目前第一版总共差不多花了1个半月左右时间),当然迫不及待地投入到市场进行模式验证,从投入到第一辆摇摇车开始,运营平台后台陆陆续续在产生订单数据,这个过程绝对是大幅度提高士气的,大家看到了希望,前面大概2,3周到时间里面,老板们整天盯着运营平台的订单页面不断的刷新订单记录,每看到一个用户充值的订单,都是欣喜若狂啊。
创业的路都是艰辛的,其实每个创业者真正拼的并不是纯粹的市场盈利模式,也不仅仅是管理策略,我一直认为创业公司能走多远,就看每个人在这个路上想走多远。同时,只有走的更远才能看的更远。所以一切从简,一切利益最大化,效率最大化为原则。
行业内本身经验的欠缺,时间的仓促等各方面原因立马检验出第一版上线订单系统有如下的BUG:
1.用户app扫码需要判断当前车辆(设备)是否在线,如果在线,就用户扣费,然后发送协议到MQTT。这个版本里面的判断设备在线用的是阿里云提供的一个公共接口,而且阿里云也后来指出这个接口不能用于主干核心业务流程,尤其使用很频繁的业务场景,当初没看到,结果造成很多设备其实不在线,但是用户继续扣费,发送协议到MQTT,当然盒子因为没在线,也不会发生启动指令给摇摇车,摇摇车没启动,这样造成了大量的用户投诉,说乱扣费,结果那段时间客服在后台一个个的退费(退费有依据的,看我上面的流程图就知道,盒子要是订阅到消息,会通过订单号修改数据库状态就知道盒子有没有发送启动指令了)。
2.正常情况下,就是设备真的在线整体流程走下来,但是到了设备那一端(看我图),盒子接受启动指令,发送指令给摇摇车,这个过程并不是摇摇车百分之百就一定启动,原因就是摇摇车本身硬件的不成熟。这么一来就彻底尴尬了,设备在线,用户扣费,发送MQTT协议,设备订阅消息,并修改数据库状态表示发送启动指令给摇摇车了,但是摇摇车就是没启动,简直无从查起,毫无证据保留,那段时间唯一的解决方案就是尽量送用户一些用车卷进行低调安抚了。
于是,经过讨论,修改如下,在确定设备有返回的时候在去下订单,即在MQTT应用层这一层去下订单。
这个算是订单流程系统的第二次发版,又过了大概一周,重新优化,原因是依然有个问题,那就是盒子发送指令到摇摇车依然不是百分之百靠谱能启动,这样导致用户要是扫码没启动,虽然没扣费了,但是平台也没下订单,没有订单依据,用户体验也不好。当初由于硬件这边条件上的限制,在设备这边流程上不能做到:盒子接受启动指令,发送指令给摇摇车,摇摇车收到指令回复盒子,盒子这个时候再返回信息到MQTT。
目前的硬件条件暂时还做不到这种流程,订单系统再次进行第三次发版,主要改动点如下:
1.用户扫码摇摇车,如果设备在线,账号余额充足,先下一个预扣费的订单,然后发送协议到MQTT。
2.盒子监听MQTT消息,收到消息,发送启动指令到摇摇车,然后上报消息(发送指令到MQTT)。
3.MQTT应用端监听上报消息,收到消息,并核对流水号凭证,这个时候才去做订单具体业务,包括用户扣费,商家分成,修改订单状态等操作。
这次改版虽然改进了用户扫码后没有反应,有数据追查的凭证,相对提高了用户的体验,但是盒子发送启动指令到摇摇车和盒子上报消息到MQTT依然是个并行的操作,依然存在盒子发送启动指令到摇摇车有可能不能正常启动的可能(包括摇摇车本身的问题导致),所以后续在摇摇车本身的质量上包括启动技术实现方案上需要不断的去改善改进。
我的物联网项目(五)下单渠道
app扫码方式启动摇摇车从一开始就是公司主推并且唯一的下单渠道,目的就是要改变传统的投币方式,甚至改变人们的消费习惯和使用习惯。通过app进行消费,一来和用户保持很强烈的粘度,随着注册用户的增加到后面的大用户数据,可以利用用户数据做更多的增值产品服务,公司本来的定位就是后期打造母婴系列品牌平台。二来改变传统摇摇车投放商和商家的多种合作方式,收入透明化以及传统投放商管理摇摇车硬币和抗着麻袋到处收硬币的痛点。
市场是检验产品的真理,想法谁都有一箩筐,关键看用户愿不愿意买单。摇摇车不断投放出去的一个多️时间里面,注册用户平均每天有上百个,当然这个里面也依赖与平台本身打造的新用户注册免费玩三次的口号,绝大多数用户奔着免费玩三次的好处来下载app并使用,所以前期后台订单看到大部分的类型属于新人卷。当然也有用户充值消费的,多冲多送嘛。关于充值,也有部分商家自己注册app用户冲值一些金额,然后收用户玩一次的钱,帮用户扫码,当然这种方式商家相关自己也赚的稍微多点。
我一直认为,投放在路边商家小店门口的摇摇车,对于商家来说,虽然是一笔自己额外的收入(摇摇车不需要交保证金,车子顺坏也不需要负责,甚至车子比城管拉走也无所谓不需要赔钱的,所以只要插电,晚上拉进去,早上开门在拉出来,基本不需要管事),但是这笔收入对于大多数商家来说可有可无(当然有比没有要好),所以商家想着法子怎么在摇摇车身上多赚点钱稍微花点心思在摇摇车身上的真心不多。也正是因为这种事实存在的客观因素经常听到有商家门口的摇摇车被城管拉走了一事,甚至听到更多的下雨天摇摇车被雨淋坏的事情(深圳那段时间一周下好次雨),就是因为商家懒得拖进去。
公司要快速起步跑,加快各种融资渠道和速度,除了验证本身的盈利模式外,最终还的拿数据说话,订单就是最好的证明。
app下订单作为主推的并且唯一的渠道,注定走不快。这个里面因素很多。1.摇摇车的诞生已经很多年了,人们使用投币方式已经根深蒂固,投币简单方便,而且小朋友拿着硬币投进去甚至觉得有种快感,再说大多老年人带小孩的用app确实门槛很高。2.我们现在是在抢占传统投币摇摇车的市场和地盘,对于用户来说,选择度很清晰,要么下载app扫码玩我们的,要么拿着硬币玩隔壁的,所以我们的摇摇车等于是孤立了自己,不能做到在我们的摇摇车身上让用户没法选择消费方式,要做到什么消费方式都可以,比如微信支付,投币都可以。3.对于有些场所,比如人流量大的,可能app方式消费有点慢,人家只想玩一次就走了,不像有的小区经常在附近玩的,所以需要灵活快递的消费模式。
app作为主推并且唯一的消费模式大概经历了2,3个月左右,公司决定开发更多的消费模式,就是通过订单渠道来增加摇摇车的使用次数,同时也是正式面对面和传统投放商亮剑准备收割领地。
一 微信扫描订单渠道
主要针对玩一次,随到随完,想简单消费的用户。使用很简单,打开微信,扫描摇摇车身上二维码,弹出微信支付页面,支付完毕,扣钱成功后即可启动摇摇车。微信支付订单是用户支付的钱直接到平台微信账号,商家可提现账户增加分成金额,如用户微信支付1元,按照55分成的话,这个时候商家可提现账户增加0.5元,其实和app扫描流分成一样的逻辑,这个不多细说。
二 投币启动摇摇车
投币渠道订单与app和微信完全不同,app和微信是通过扫描方式,并且钱是首先到了平台,然后商家再从平台提现。而且投币是钱首先到了商家(摇摇车钱箱钥匙给商家的,商家保管钱箱拥有取现权),所以这个订单里面商家还需要支付一部分钱给平台。如用户投了个硬币,商家钱箱有1元,按照55平分规则,商家还需要给平台5毛,怎么给?直接从商家线上可提现账户扣5毛就是,如果商家可提现账户没钱是0,那么账户就是-0.5元,这个时候商家需要通过商家中心自帮助充值来平账,如果长期不平,后台可显示客户会处理。
我的物联网项目(六)推广策略
我参与公司最开始构造的推广策略蓝图,经过不断修改调整,整个流程如下:全部推广人员由外面兼职或者全职人员通过微信注册页面注册登记进来,后台客服人员审核资格后,这些推广人员开始去外面的各种店开始推广,如果有感兴趣的商家愿意投放摇摇车,推广人员将商家手机号码,姓名,详细地址,意愿投放摇摇车数量在微信端页面录入到后台系统,这样后台的客服人员可以看到这些等待投放的商家和订单,然后客服再次打电话给商家确定投放意愿,数量和地址,确定无误后,将投放订单正式下给公司运维派单,运维在微信端运维中心收到工单,开始运送摇摇车到商家地址,然后现场投放,并将摇摇车和商家绑定,这个时候摇摇车产生的收益商家开始分成。
这个里面从最开始推广入口来说,现在想来其实有点意淫,也正是我前面所说,路要走过才知道,我们现在就走趟雷。推广人员全部由外面兼职或者全职做为主力军,当初给他们的政策就是成功推广一辆摇摇车提成50元(听说有时候具体情况也有提成100元的),很多推广人员看到这么高的利润,着实抡起衣袖实干了一把。注意,这个里面我所说的成功推广,并不是你注册一个商家账号,也不是帮商家下个投放订单,更加不是公司运维人员拖一辆摇摇车过去,绑定商家就完事了算可以拿到50或者100元了,当初公司制定的规则是:摇摇车投放成功并且7天或者多久时间要有多少使用次数(规则后天可以设置)才算投放成功才有提成,公司制定这种规则,目的就是不能让推广人员恶意刷单,就是为了拿每两摇摇车的提成不管三七二十一到处注册商家,见地方就扔摇摇车过去,这样公司会浪费很多摇摇车资源,并没产生实际效率(那个时候公司仓库摇摇车数量也不多,还处于模式验证阶段,每辆摇摇车都是宝贵资源)。
这种推广模式下,前面一周投放订单数量很多,真正投放出去后,摇摇车使用次数很少很少,当然到后面很多摇摇车又重新拖回来,其实那段时间估计没有几个推广人员能达标的,所以有些推广人员可能看不到拿提成的希望,再加上兼职的也很多,爱干不过,积极性慢慢降下来,一二周后商家注册少了,投放订单数也少了。再加上刚才我说过的,很多推广人员为了刷单,乱七八糟根本没有人流的地方投了不少车,公司还需要对这些很久没有产生数据的摇摇车出现优化拖回来再次投放。还是那句话,我们在趟雷,在为了下次做的更好,走的更远在买单。
这个里面我要强调下,当初的做法也是公司定位一个轻互联网公司,就只做平台,不想把自己搞的太重。自己的路自己走,自己的游戏自己去玩,这个世界就是这样,要想别人认同并愿意和你玩,你自己首先也玩好自己的。公司清楚到这点,接下来要做的就是打造一个标本,一个榜样那就是直营。直营就是自己做自己的,当作一门生意来做,不但要打造一个自己全职的地推团队,甚至还要打造自己的供应链,仓库物流,做到整个环节的闭环。
我的物联网项目(七)前期线上事故
一 MQTT连接数报警
项目上线一个月左右,投放出去的摇摇车数量大概在200量左右,平均每天在线数(听说有些商家精打细算,有小孩需要坐车了才插电,平时都不插电,还有些干脆一直仍在角落懒的管)也就维持在100左右,当时在阿里云购买的MQTT配置是连接数上限2000(MQTT是按连接数购买的),像目前的摇摇车投放数用当时的配置绰绰有余了,连续一个月以来,都是正常化(现在想来,当初的推广策略不成熟,每天投放的摇摇车数量也是要么一天3,4台,要么连续好几天才推广3,4台),所以问题并没有暴露出来,不过出来混迟早要还的。
有天下午快下班的时候,突然MQTT不断报警,手机上5秒一次收到报警短信,提示MQTT连接数已经超标(用阿里云的产品感觉这块的预警功提示的还是蛮及时),因为当初也有一些摇摇车在做测试,频繁的使用到MQTT,所以当时也没太在意,叫测试人员先停一下在做测试(这个里面很尴尬,摇摇车扫码启动用到的测试环境和线上环境是同一个MQTT,这个后面再详情描述原因)以为过一会连接数会释放下去,但是手机收到报警短信越来越猛,当时第一时间想到的整体事故点应该在MQTT业务应用层这一端(手机扫码是通过http请求到MQTT应用层,MQTT应用层再扔消息到阿里云MQTT服务器),当初是2台MQTT应用层在做负载均衡集群,我登录2台服务器,分别用top命令查看两台服务器一台CPU在80%左右,另外一台CPU在60%左右,顿时觉得很诧异,这么点摇摇车数据请求不至于导致应用服务器承受不了,当时第一时间想到的是看看TCP的目前的连接数,结果一查吓了跳(使用命令:netstat -natp|awk ‘{print $7}’ |sort|uniq -c|sort -rn),两台服务器的当前连接数都接近快1W多,还在持续上升(因为当时投放出去的摇摇车还在不断有人在使用消费),这个时候基本定位问题:手机扫码发送http请求到MQTT应用层,MQTT应用层每次仍消息到阿里云MQTT服务器,都需要建立连接。所以问题很有可能是没有释放连接,由于当初的代码逻辑比较简单,所以直接找到写这个代码的开发人员,一起喵了眼代码,果然如此,修改代码后,重新发包一切正常,手机报警短信立马停了。
public void sendMsgMqtt(String productId, String deviceId, String scontent, String topic){
String subTopic = getSubTopic(productId, deviceId, topic);
String clientId = getClientId();
MemoryPersistence persistence = new MemoryPersistence();
try {
final MqttClient sampleClient = new MqttClient(GlobalConstant.BROKER, clientId, persistence);
final MqttConnectOptions connOpts = getConnOpts(clientId);
log.info("Coin Connecting to broker: " + GlobalConstant.BROKER);
sampleClient.setCallback(new MqttCallback() {
public void connectionLost(Throwable throwable) {
log.info("mqtt connection lost");
throwable.printStackTrace();
while(!sampleClient.isConnected()){
try {
sampleClient.connect(connOpts);
} catch (MqttException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
log.info("coin messageArrived:" + topic + "------" + new String(mqttMessage.getPayload()));
}
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
log.info("coin deliveryComplete:" + iMqttDeliveryToken.getMessageId());
}
});
sampleClient.connect(connOpts);
try{
scontent = scontent.replace("[", "").replace("]", "");
final MqttMessage message = new MqttMessage(scontent.getBytes());
message.setQos(1);
log.info("pushed at "+new Date()+" "+ scontent);
sampleClient.publish(subTopic, message);
log.info("-------send end---------");
}catch(Exception ex){
ex.printStackTrace();
}finally{
sampleClient.disconnect();
log.info("-------client disConnect()---------");
}
}catch(Exception ex){
ex.printStackTrace();
}
}
二 数据库CPU100%
必须要先说下,创业项目初期,当初的业务都是很不清晰的,基本属于那种摸着石头过河,走一步算一步,再回头想想甚至看看市场的反应,然后再修改,所以这个阶段更多的是检验模式,优化业务,当然资金也有限,而且要求开发迭代要求迅速,所以数据库当初没有做集群,就是一台,玩单机,也正是因为玩单机,所以一些在集群环境下没有这么快爆发出来的问题很容易在单机里面爆发出来。
晚上大概7点左右(晚点7点到9点是摇摇车的使用高峰,白天一般订单数据较少,到了晚上数据成倍的增涨),收到反馈,说平台系统打开很慢很慢,而且手机也不断收到MQTT短信预警,说堆积了大量未消费的订单,第一时间的反应迅速登录云数据库管理平台看到数据库CPU这项指标严重标红,并显示使用率达到100%。当初数据库用的是通用型4核8G的,订单表数据将近40W,打开数据库性能管理界面查看(如果没有这种管理界面,也可以通过命令show processlist来查看正在执行的SQL和explain来分析执行计划查看慢SQL),发现积累了大量的慢SQL,有些SQL的平均执行时间超过将近1分钟,很明显做了数据库的全局扫描,再加上这段时间的高峰时期,慢SQL堆积,导致CPU资源顺序消耗完。
当时也正是业务的使用的高峰期,再加上客户投诉和上面很多人在盯着这个事情,所以我这边要在短时间内恢复数据库的正常,当初有几种选择可以迅速恢复,将数据库切换到备数据库(数据库是高可用的)或者kill掉一些慢SQL(通过show processlist查看state为Sending data的列,然后kill id),这些都有可能会影响到现在正在使用的业务,万不得已的情况的不会第一时间去做。我看了下排在前面的几条慢SQL,其中有些是第一时间可以迅速处理的,比如优化索引,我抱着试试的方法,将之前有些索引重新优化了下(后面会详细描述),过了几分钟,CPU的使用率慢慢的降了下来(没有优化索引之前大概3秒执行一个记录,优化索引后1秒可以执行上千个记录),业务正常了,给我后面做数据库的优化有了大把时间思考,所以这次事件给我最大的感触就是切勿头脑发热,需要冷静最小化处理问题。
这次SQL优化也总结了些经验并做了相关优化如下:
1.添加索引和优化索引,特别小心索引隐式转换
一个表里面如果只是设置了主键,然后其它索引一律不建不管,简单业务如只涉及到按照主键查询的业务是没问题,但是设计到其它字段的查询,在数据量稍大又加上业务高峰期,这种导致表全局查询的SQL肯定会积累大量慢SQL,最终导致CPU持续上升,如果有条件的话,测试最好做一些大数据量的压力测试是可以测试出来的,另外,建立了索引,也要注意到索引失效这种情况。如:select *from order where phone=13772556391; 平时写代码粗心大意,不仔细检查再加上压力测试没测试到位,在高并发数据量稍微大点的业务场景里面搞不好就出问题。数据库表phone字段用的字符串类型,但是这个SQL里面没有加上引号,所以像这种情况下,索引是无效的。
2.分页查询优化
select * from orderwhere oid=100 limit 100000,5000,这种普通limit M,N的翻页写法,在越往后翻页的过程中速度越慢,原因mysql会读取表中的前M+N条数据,M越大,性能就越差。像这种SQL如果只是平时查询看看记录,感觉不到异常,就算稍微慢点,也忍了,但是在我刚才说的业务场景里面,也会导致慢SQL查询积累。优化写法:select t1.* from order t1,(select id from order oid=100 limit 100000,5000) t2 where t1.id=t2.id,这种效率会高很好。
3.分表
虽然做了上面的优化,执行效率比之前高了很多台阶,但是单表达的压力依然存在。订单表当时没有做分表,尽管目前的条件没有用分片集群,但是为解决燃眉之需也需要做物理分表(随着投放出去的摇摇车越来越多,当时每天订单大约1万到2万)。
SQL优化是一个长期的过程,最好是结合具体业务场景来做优化效率会更好一些,后面我会继续罗列在这个项目实战中出现的一些关于SQL的坑和做的相关优化。
我的物联网项目(八)简单分布式调度
定时调度基本在任何平台或多或少的要用到,实现定时调度的功能很简单,我做过的项目中用到更多的是spring quartz或者spring task,它们在单机上使用定时任务配置是非常简单的,但是在集群环境中就需要面临一个必须解决的问题:如何限定只有一台机器在执行定时任务?
其实spring quartz也可以实现此功能,它是由数据库的数据来确定调度任务是否正在执行, 正在执行则其他服务器就不能去执行该行调度数据,所以需要数据库的11张表来执行此种功能,总的来说成本较高,操作起来也比较复杂。另外一些开源的分布式调度平台也有一些,如当当网的elastic-job,淘宝的TBSchedule,包括阿里云也有SchedulerX,这些分布式调度平台在一定程度上也可以满足集群环境下的功能需求。我当初在技术选型上,只想简单无门槛,不想有太多的学习成本在里面,尤其针对目前阶段的项目,想用一种简单的方式来实现目的就行,所以尽量基于目前的代码和技术。
一 实现思路
主要利用Redis的(Redis用的云集群,暂时不需要考虑单点故障或者不稳定的情况)函数setNX()来实现分布式锁,大概流程是首先是某个集群环境的单边服务器将某一任务标识名(简单来说就是key)作为键存到redis里,并为其设个过期时间,如果这个时候另外的单边服务器也请求过来,先是通过setNX()看看是否能将任务标识名(同一个标识名)插入到redis里,可以的话就返回true,不可以就返回false,如果返回false,说明这次的任务调度别的服务器已经在做了,不需要执行这次任务。如果返回true,说明这次任何调度是由自己来执行。
这个里面由于集群环境下的每台服务器到了时间点都会去执行一遍,当然肯定只有一台才能执行成功,这个里面需要注意两个事情:
- 定时调度的策略应该上一个任务完成到下一个任务开始的时间间隔,这样的话才能保证集群环境下其它的服务器下次抢占锁的机率,如spring task的fixedDelay。
- 调度时间循环间隔设置当然以具体业务场景为准,但最好算好大概的每次业务执行的时间长短,然后根据这个时间长短来设置定时调度的循环间隔时间。比如说如果小于1s的调用,由于使用redis会有10几毫秒的运算耗费,因此不能保证在1s以下的时间间隔比较均匀。所以尽量保证每台服务器的均匀分布来执行计划任务。
二 代码实现
1.锁对象
public class Lock {
private String name;
private String value;
public Lock(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
2.分布式锁工具类
public class DistributedLockService {
private final Logger log = LoggerFactory.getLogger(getClass());
private final static long LOCK_EXPIRE = 10;//单个业务持有锁的时间10s,防止死锁
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 尝试获取全局锁
*/
public boolean tryLock(Lock lock) {
return getLock(lock,LOCK_EXPIRE);
}
/**
* 操作redis获取全局锁
*/
public boolean getLock(Lock lock,long lockExpireTime){
if (StringUtils.isEmpty(lock.getName()) || StringUtils.isEmpty(lock.getValue())) {
return false;
}
// SETNX成功,则成功获取一个锁
if (setNX(lock.getName(), lock.getValue(),lockExpireTime)) {
return true;
}else {// SETNX失败,说明锁仍然被其他对象保持
log.info(lock.getName()+" lock is exist!" + dateFormat.format(new Date()) + "###");
return false;
}
}
/**
* @Title: setNX
* @Description: 设置锁
*/
private boolean setNX(final String key, final String value, final long expire) {
return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
@SuppressWarnings("unchecked")
public Boolean doInRedis(RedisConnection connection) {
byte[] keyBytes = ((RedisSerializer<String>) redisTemplate.getKeySerializer()).serialize(key);
boolean locked = connection.setNX(keyBytes, ((RedisSerializer<String>)redisTemplate.getValueSerializer()).serialize(value));
if(locked){
connection.expire(keyBytes, expire);
}
return locked;
}
});
}
/**
* @Title: get
* @Description: 根据key获取value
*/
public Object get(final String key) {
return redisTemplate.execute(new RedisCallback<Object>() {
@SuppressWarnings("unchecked")
public Object doInRedis(RedisConnection connection) throws DataAccessException {
byte[] bs = connection.get(((RedisSerializer<String>)redisTemplate.getKeySerializer()).serialize(key));
return redisTemplate.getDefaultSerializer().deserialize(bs);
}
});
}
/**
* 释放锁
*/
public void releaseLock(Lock lock) {
if (!StringUtils.isEmpty(lock.getName())) {
redisTemplate.delete(lock.getName());
}
log.info(lock.getName()+" lock is unchecked!" + dateFormat.format(new Date()) + "###");
}
}
3.定时调度实现
public class ScheduledTasks {
@Autowired
private DistributedLockService distributedLockService;
private final static Logger log= Logger.getLogger(ScheduledTasks.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
//5秒执行一次
@Scheduled(fixedDelay = 5000)
public void doJob() {
log.info("###sync start:"+ dateFormat.format(new Date()) + "###");
Lock lock = new Lock("xxlock" , "xxx");
if(distributedLockService.tryLock(lock)){
log.info("Gets the lock!" + dateFormat.format(new Date()) + "###");
//做具体业务......
distributedLockService.releaseLock(lock);
}
log.info("###sync end:"+ dateFormat.format(new Date()) + "###");
}
}
三 继续优化
上面将做具体业务的代码耦合到了定时调度ScheduledTasks里面,这块需要优化下,后面我们将具体的业务代码单独抽离出来做成一个rest服务,ScheduledTasks里面通过接口请求去执行业务逻辑即可。
定时调度这块后续我们还在继续优化,主要有如下:
1.将调度时间间隔,调度http请求接口,调度的动态开启和关闭,查看目前的调度任务和执行的日志做成后台可视化界面,方便统一管理和运维。
2.集群环境下的服务器做到分片,负载均衡。其实现在的并没有严格做到负载均衡,其实集群环境下每台服务器都在执行,只是没有执行具体业务而已,所以后续这块自己将用代码实现。
我的物联网项目(九)久违的死循环
旧业务不断的调整,新的需求不断的开发,版本不断的迭代,这个是当前项目的一个暂时不可改变的现状。再加上每个开发人员写代码的风格和层次不一样,所以有很多本来可以在写代码过程中避免的问题非要通过线上的报警才能发觉。
最近两天线上linux服务器发现java进程CPU不断的飙升,新发的包过一会儿CPU就慢慢上涨,感觉很奇怪,之前没有这种情况,应该开发人员新写的代码所导致,排查问题如下:
一 使用top命令
使用top命令
top
结果如下:
过了一会儿,继续top查看
二 查询具体的线程CPU占用率
使用命令 ps -mp 5910 -o THREAD,tid,time
ps -mp 5910 -o THREAD,tid,time
结果如下:
可以看到5910号进程产生了大量的线程,继续查看该线程的具体执行情况。
先通过 printf “%x\n” 18082拿到十进制
printf "%x\n" 18082
结果是46a2
三 使用jstack查看具体线程
使用命令 jstack 5910 | grep 46a2-a 100
jstack 5910 | grep 46a2-a 100
发现如下:
找来开发人员一起仔细想想写的代码在什么地方用到了线程池之类的,果然真有这样的代码,一起喵了眼代码,发现了问题。
注:以下代码是模拟代码
@Scheduled(fixedDelay = 1000)
public void doJob() {
System.out.println("###sync start:"+ dateFormat.format(new Date()) + "###");
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < Integer.parseInt(threadnum); i++) {
executorService.execute(createTask("test"+i));
}
System.out.println("###sync end:" + dateFormat.format(new Date()) + "###");
}
private Runnable createTask(final String key) {
return new Runnable() {
public void run() {
while (true) {
try {
disposeOrder(key);
} catch (Exception e1) {
System.out.println("ScheduledTasks disposeOrderException:" + e1);
e1.printStackTrace();
}
}
}
};
}
private void disposeOrder(String key){
System.out.println("key="+key);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
问题其实就在createTask的方法里面while (true)进入了死循环,不断的产生线程导致(前面的线程没释放,后面的不断在产生)。
其实事情到这里还未完,我看到代码里面有用到newCachedThreadPool,它是创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。也就是说如果主线程提交任务的速度高于线程池中处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU资源。所以这块也需要注意先备注下后面再优化。
我的物联网项目(十) 线下之战
摇摇车这个行业在中国至少已经存在了7,8年以上,这期间也越来越多的投放商加入到这个队伍里面,说明这个行业本身是刚性需求,不要小看这一块钱现金流,如果投放的数量达到一定程度,每天的现金收入是非常可观的。这么来算(粗略的算),投放100辆车出去,每辆车每天消费15次也就是说每天赚15块钱,每天总收入有1500块钱,一个月总收入4万5。一辆车按照最低成本600一辆,100辆摇摇车成本大概在6万左右,按照这么算下来,100辆车投放出去2个月就回本 。当然投放出去放在商家店门口,商家也需要分成,就算按照对半55分成,这笔利润也是可观的 。传统的投放商有大有小,大的本身和工厂有关系甚至就是工厂本身,所以摇摇车的成本可以更低,投放出去和商家谈判分成更加有利对商家来说也更加有诱惑。所以,针对人流量大的一些点位置基本被一些大的投放商给占领。一些稍微远点的街道小区小的投放商偏多些,虽然这块蛋糕分的差不多了,不管怎么样,只要摇摇车能投放出去,大家依然有钱赚。
我们现在再来做摇摇车这个行业,而且前期是以投放商的身份在做,我个人觉得如果公司一来就比摇摇车本身的质量,摇摇车本身的外形外观等硬件设施,无疑是陷入到了一个死坑,因为对于别的已经入行这个行业很多年的投放商来说,你很嫩而且你也不是工厂研发背景出生,所以公司前期必须借助其它的硬件研发团队和合作工厂来保证持续的供应(后期要想做大,必须拥有自己的供应链生产工厂,形成整个闭环,这个是后话),当前前期也可以花一小部分精力针对硬件是上某小部分做些创新也是可以的,当然这部分创新也是配合新的模式而做的。
所以作为一个新兴的投放商,是以新的模式来进军传统摇摇车市场,那就是物联网模式。摇摇车联网,对于平台来说,有黏度的用户,有可视化的数据,有业务的扩展性。对于商家来说,有透明的盈利,有便利的结算,有更多的合作方式。对于用户来说,有更多的优惠,有新鲜玩法,有丰富的内容。对于每个角度来说,都是传统摇摇车所不能比的。可以简单的来形容,传统摇摇车就是单机,物联网摇摇车好比集群,谁更好,估计一目了然。作为一个创业性公司,前期胃口决对不能太大,有想法有目标就行。不能什么都想做NO1,虽然两双都要抓,肯定有一手是抓得更大,那就是平台,通过互联网软件平台来发挥自身的优势。
线下投放模式很清醒,相比传统摇摇摇车外面的商家也更加乐意来接受,虽然每投放一辆摇摇车差不多都是在传统摇摇车战场上厮杀抢过来的,但是一旦站稳阵脚,传统摇摇车投放商要是再抢过来,那是难上加难了。所以,前期和传统投放商之间的矛盾迟早会慢慢转变成和商家之间的矛盾。
1.平台软件要稳定更加简单强大。
2.机器故障要减少更加放心使用。
3.商家对平台越依赖要求会越高。
4.平台后勤运维客服效率要更高。
我的物联网项目(十一) 单数据库事务也需谨慎
单体架构模式下的数据库基本都是单数据库,所以应用层通过spring事务控制的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。通过spring实现事务的方式也有声明式事务和编程式事务两种,不管哪一种实现起来都比较简单。像一般的业务,类型下面这种方式编程就行:
1.配置文件
<!-- 事务控制 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 配置事务传播特性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="*" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 配置参与事务的类 -->
<aop:config>
<aop:pointcut id="interceptorPointCuts" expression="execution(* com.orange.adminweb.service.*.*(..))"/>
<aop:advisor pointcut-ref="interceptorPointCuts" advice-ref="txAdvice" />
</aop:config>
2.com.orange.adminweb.service包下的java代码
public void updateBinding(String userId,String merchantId,Order order) throws Exception {
//删除用户
userService.delete(userId);
//删除商家
merchantService.delete(merchantId);
//更新订单
orderService.update(order);
}
像简单事务类似上面编程只需注意两个事情:
1.上面xml配置的save*,update*,delete*表示通配符以save,update,delete开头的方法(com.orange.adminweb.service包下)都是启用事务的。
2.以save,update,delete开头的方法(com.orange.adminweb.service包下)必须继续将异常往外抛。
所以,很多刚开始入行的同事基本按照这种方式写代码,也没什么问题,但是最近测试有人反映,有个业务(类似上面)数据不一致,简单来说就是几张表的数据原子性不一致,我找到方法类,打开看了,场景确实和上面的稍微不太一样,以下为模拟代码。
private void saveSubscribe(){
StringBuilder clientBuilder = new StringBuilder();
clientBuilder.append(GlobalConstant.GROUPID);
clientBuilder.append("@@@");
clientBuilder.append("ClientID_");
clientBuilder.append(UuidUtil.get32UUID());
String clientId=clientBuilder.toString();
MemoryPersistence persistence = new MemoryPersistence();
try {
final MqttClient sampleClient = new MqttClient(GlobalConstant.BROKER, clientId, persistence);
final MqttConnectOptions connOpts = new MqttConnectOptions();
System.out.println("Connecting to broker: " + GlobalConstant.BROKER);
String sign=MacSignature.macSignature(clientId.split("@@@")[0], GlobalConstant.SECRETKEY);
final String[] topicFilters=new String[]{GlobalConstant.TOPIC + "/#"};
final int[]qos={1};
connOpts.setUserName(GlobalConstant.ACESSKEY);
connOpts.setServerURIs(new String[] { GlobalConstant.BROKER });
connOpts.setPassword(sign.toCharArray());
connOpts.setCleanSession(false);
connOpts.setKeepAliveInterval(100);
sampleClient.setCallback(new MqttCallback() {
public void connectionLost(Throwable throwable) {
log.info("mqtt connection lost");
throwable.printStackTrace();
while(!sampleClient.isConnected()){
try {
sampleClient.connect(connOpts);
sampleClient.subscribe(topicFilters,qos);
} catch (MqttException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void messageArrived(String topic, MqttMessage mqttMessage){
try {
saveOrder(new String(mqttMessage.getPayload()));
} catch (Exception e) {
e.printStackTrace();
}
}
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
log.ifo("deliveryComplete:" + iMqttDeliveryToken.getMessageId());
}
});
sampleClient.connect(connOpts);
sampleClient.subscribe(topicFilters,qos);
}catch(Exception ex){
ex.printStackTrace();
}
}
操作三张表的saveOrder方法如下:
private void saveOrder(String message) throws Exception{
//修改用户
userService.updateUser(......);
//修改商家
merchantService.updateMerchant(......);
//下订单
orderService.saveOrder(.......);
}
因为业务本身原因,当saveOrder方法里面的修改用户,修改商家,下订单任何一个方法出现异常时候,saveSubscribe方法并没有回滚数据。
重点看saveSubscribe方法里面的代码片段:
sampleClient.setCallback(new MqttCallback() {
public void connectionLost(Throwable throwable) {
log.info("mqtt connection lost");
throwable.printStackTrace();
while(!sampleClient.isConnected()){
try {
sampleClient.connect(connOpts);
sampleClient.subscribe(topicFilters,qos);
} catch (MqttException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void messageArrived(String topic, MqttMessage mqttMessage){
try {
saveOrder(new String(mqttMessage.getPayload()));
} catch (Exception e) {
e.printStackTrace();
}
}
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
log.ifo("deliveryComplete:" + iMqttDeliveryToken.getMessageId());
}
});
这个里面有setCallback回调,它里面的异常是没法接着往外抛的,所以不会回滚数据,简单来说saveSubscribe方法就是一个没有事务控制的方法。
其实这种业务场景有点类似我们之前的业务需求:
有个AService和BService都配置了事务,AService调用了BService,BService需要记录日志,但是当BService出现异常的时候,发现没有记录日志,原因是AService和BService配置事务的时候有个参数propagation,默认都配置了REQUIRED
<tx:method name="save*" propagation="REQUIRED"/>
使用这种策略时BService将使用Aservice的事务,所以AService回滚将整个方法体内的任何东西都回滚了。所以解决这种业务场景就需要BService配置独立的事务,不管业务逻辑的Aservice是否有异常,BService日志都应该能够记录成功。
所以解决上面setCallback回调不抛异常出去的问题,配置修改成saveOrder配置独立事务可以解决问题。
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="saveOrder" propagation="REQUIRES_NEW" rollback-for="java.lang.Exception"/>
通过这次问题的解决回顾,说到底还是对Spring事务类型并没有引起重视,具体的业务场景应该使用不同的事务类型,而并不是一味的使用REQUIRED,最后贴下Spring的七种事务传播行为类型:
PROPAGATION_REQUIRED–支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORTS–支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY–支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW–新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED–以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER–以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED–如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
我的物联网项目(十二) 单体应用架构不行?
单体应用架构在创业型项目里面是非常合适的,毕竟它主要的担当还是在验证创业模式以及迅速功能实现,所以它从开发到部署,在少量开发人员的基础上能非常减少成本,主要是门槛低,开发效率也非常高。到目前为此,这个物联网项目从开发开始到现在线上运行大概经历了5个月左右的时间,订单数据从日订单几百到现在的七八万,在应用层本身来说并没什么压力瓶颈,中间主要升级了数据库RDS的配置,由原来的4核8G升级到了8核16G,对数据库稍微做了些优化,依然跑到很稳定。公司从实施想法开始,到目前半年的时间里面,不断的总结创业思路和改变策略,所以开发的业务变化由不断的“试误型”开始趋向于明确“清晰型”,而且业务垂直方向也越来越深,由自己做投放商急迫需要转型做招商平台城市合伙人,而且广告内容,和线下门店信息推广也在融合,其实和我之前的电商O2O很类似,一开始自己做个简单交易网站卖东西,做着做着就做B2C,B2B平台,其它的商家也进入这个平台开网店,将线下的信息广告推广也融合进来,大概套路都一样,无非是前期自己先验证模式,觉得可行就做平台告诉别人应该怎么怎么做。公司在半年的时间里面也各种融资好几轮,达到了好几千万级别的融资,接着而来的对软件平台的需求和要求也越来越高。想法谁都有,关键是要去实施,互联网游戏的规则就是这样,技术实施永远跟着产品想法的屁股在后面追。
单体应用架构在项目这几个月业务变化频繁,不断迭代的过程中,的确也出现了很多问题。最开始,业务比较简单,你写同一个工程里面写代码,我也在同一个工程里面写代码,测试完毕后,我这边就直接打成个war包(因为线上部署在tomcat的ROOT目录里面,我有时候直接在本地tomcat的ROOT里面解压缩成zip包),丢到线上服务器,简单方便,速度快的很,这种发包方式我们简称“全量部署”,哪怕你这次改一点点需求只动了一个类里面的一行代码,我也是将所有的代码打包一次,简单业务简单方法处理是没有问题,随着业务需求的累加,并且不断的迭代,这种打包方法隐患慢慢多了起来,有几次线上发包“全量部署”,出现之前OK的功能代码不OK(其实这次发布不涉及到这些功能),细查之,知道开发人员提交了没测试的代码,就算后面非常谨慎和宁愿操作麻烦想保持SVN代码的“纯洁性”,但是风险问题依然存在,后面一段时间,我改用了“增量部署”,就是这次改掉了哪几个类,哪几个文件,在发包文档里面一 一描述,发包的时候只去测试好的测试环境拿下来这几个文件,传到正式环境里面,稍微控了下风险。
当然,单体应用架构本身对应用服务器的性能消耗也没有做到很好的水平扩展,如果后期线上量大,我也只能单台的去升级配置,越到后面升级配置越贵,当然,我们目前的业务还没有这么夸张,但是随着业务的扩展,肯定无法避免这些东西。另外,数据库随着业务的水平扩展,业务的粒度细分,也不可能所有的表都在同一个数据库里面,这个也要求后期提前做好分库分表的架构准备。
我们生在一个微服务四处横行的年代,基本上属于那种不玩单体应用架构,就玩微服务架构,不玩微服务架构,就玩单体应用架构。如果再倒退10年,可能就玩SOA面向服务企业架构,虽然和微服务最终实现的目标差不多,但是SOA表现的方式更多是以系统级别的形式表现,系统与系统的交互,所以更多的是通过传统的webservice的调用来满足按照业务的拆分。好在当今有微服务众多的成熟框架,实现起来更加灵活简单,而且入门也相当简单,更主要的开发起来比SOA更加轻量级。并且这些成熟框架本身集成了微服务中需要解决的基础共用的东西在里面,比如微服注册与发现,路由网关,客户端负载均衡,传输协议等等。话虽如此,但是真正用微服务从头到尾开发过几个项目的开发人员并不多,我们连续好几个月也一直在招聘对微服务,服务化相对熟悉的人,也寥寥无几,来面试的人可能也就是在书上看了些资料,搭建个简单的框架,但是真正沉淀在实际业务场景用微服务开发的人员一直没遇到。
经过再三考虑,我们决定使用springcloud来开发V2.0版本,业务拆分全部微服务化实现,V1.0单体应用架构在这段时间里会小范围维护,集中精力大概用2,3个月时间完成V2.0版本的迭代,脑袋一蒙,后面的大把的坑在等着我们,我们也准备好了打场硬仗。
我的物联网项目(十三) 2.0平台架构体系
准确的来说,1.0平台的单体应用架构没有互联网项目架构一说,传统的MVC开发模式,简单的小作坊操作流程,对于每个开发人员来说,只需要关注业务的功能模块实现而已。在1.0平台运营的半年时间里面,除了业务本身的需求爆炸性的增长,要求开发的迭代迅速,并且每次升级都不应该伤筋动骨,只是模块化的累加或者在原有的框架里面局部的更新,除了这些,我们还看到了1.0平台本身的基础性运营配套设施也迫切需要投入进来,以提高平台的运营效率,如日志平台,监控平台,调度平台,报表平台,甚至权限和单点登录也很需要,所以对于2.0平台的整体规划以上的都应该包含在里面。
一 平台整体能力规划
主要将一些公共的东西全部从原有的业务层里面拆离出来,以平台化软件包的模式运营。
\1. 统一调度平台在项目中很多业务会经常用到,但是1.0平台将调度工作和业务执行工作全部糅合在代码里面,造成大量的调度工作后期的维护非常不便,而且调度没法监控目前在运行的调度和距离下次需要执行的调度。统一调度平台可以解决这方面的问题,可以有效的通过管理界面来维护平台的所有调度工作,从设计角度简单来说,调度的工作归调度平台,业务的执行归个各自业务平台(微服务)。
2.统一接口平台主要解决前端应用系统通过统一接口平台获取数据,不直接与外部系统接口打交道。统一接口平台通过多种方式与外部系统联接获取数据并向各前端应用系统提供各种数据格式包,将外部系统有效地隔离在业务系统之外。前端应用系统需要请求的外部接口需要在统一接口平台注册,开放。每次访问都会被有效的记录,实行监管。在前后端分离架构系统里面(其实微服务就是这种),统一接口平台也担当跨域,和负载均衡的职能在里面。
3.统一日志平台前期主要解决需要登录到linux服务器,开各种账号权限查看log日志的麻烦,可以让开发人员通过管理界面管理日志,查看日志,以提高工作效率,中后期会将ELK日志分析工作引入到平台。
4.统一权限平台主要针对众多的各种平台 (如上面的调度平台,接口平台,日志平台等)的权限统一管理,这块配合SSO单点登录一起使用,如果按照传统的每个平台分配一个账号,操作起来比较麻烦,统一权限平台+SSO单点登录就是解决分配一个账号,分配权限,可以进入各大平台操作。
5.统一消息平台主要针对内部业务系统的MQ中间件平台管理,和统一调度平台类似,业务的执行归业务,MQ只负责异步中转,可通过多种协议接入如http,接入更加坚定快捷,维护更加方便。
\6. SSO单点登录,只需要登录一次就可以访问所有相互信任的应用系统,而不用重复登录。
更多的平台还在规划中,后面的文章也会一 一涉及到,并分享遇到的一些坑和做的一些优化。
二 业务领域拆分
2.0架构体系的业务拆分是个很头疼的事情,说实话,按照目前具体的项目团队情况和服务器规划,既不能拆的太细,也不能拆的太粗,只能按照阶段去拆去优化,所以我的打算是先按照业务功能稍微粗的来拆,等微服务框架平台整体流程稳定后,再针对每个业务功能细化拆分。
业务领域一旦拆分,意味着也要分库,由原来的单库拆分成多库。
从数据库架构上来说,考虑到服务器成本,前期还是单机高可用,后期再做分片集群。
三 服务化架构
前端请求到后端大概整体流程(具体以项目实际为准):
1.访问html前端页面,(每个页面引入checkCookie.js),如果cookie失效,跳转到sso单点登录页面。
2.sso认证用户和密码成功后,生成token写入redis和cookie。
3.sso跳转到需要访问的html前端页面,(每个页面引入checkAuth.js),html前端页面请求(post请求)统一权限平台,权限平台通过从cookie中获取token,从token中获取用户信息,查询用户角色和资源菜单具体权限返回给html前端。
4.html前端根据当前用户角色拥有的具体资源菜单展示不同的div块显示(如果页面布局麻烦,也可以不改变原先页面,只是将没有权限div块内容屏蔽或者显示无权限查看)
5.html前端div块里面的内容展示,通过请求统一接口平台API接口,统一接口平台请求具体java业务平台API接口(微服务),API网管拦截,验证token是否在redis存在并合法,如果合法放行,通过客户端负载均衡到具体微服务获取具体数据,返回html前端并展示。
我的物联网项目(十四) 分布式事务
2.0平台服务化架构,必然分库,分库又必然面临一个分布式事务处理问题,所以无论是设计还是编码远远比1.0单体应用架构的工作量要大。不过做任何事情,重点不在实施,而是在思路,所以要解决分布式事务问题,还得先想清楚屡清楚怎么去做才是重点之重。
分布式事务处理方法其实大把,无需担忧找不到解决方法,关键是要找到满足自己业务场景,适合自己业务场景的方法,我之前做的项目涉及个分布式事务处理的方法也有好几个,其中记忆较为清晰的是一个移动充值的业务,大概业务就是用户通过银行在线支付完成后,然后给自己的手机号码充值,这个里面就涉及到银行充值和手机话费充值两个一致性的事务,当初用的就是通过每天凌晨的对账来处理这种事务问题,实现思路就是手机话费充值这端每天凌晨会上传一个订单文件到一个FTP服务器,然后通知银行那边去拿这个对账单文件来对账,然后再实行差补各种结算。当然具体问题还得具体分析,如果我们现在的业务场景直接也生搬硬套用这种方式来处理会不会也行呢?肯定不行!
我们的业务场景流程:用户通过APP扫码摇摇车,消费1块钱,针对每条订单马上所有参与方进行算账。
\1. 用户账户扣1块钱。
\2. 平台分2毛钱。
\3. 城市合伙人账户分4毛钱。
\4. 商家账户分4毛钱。
备注:前面在2.0架构体系里面已经说过分库分成用户库,平台库,城市合伙人库,商家库。
所以像这种业务场景,我们这边目前采用的是基于日志(事件)的最终一致性来实现分布式事务,当然里面涉及到知识点也较多,包括数据库单机事务,MQ消息通知,定时调度,异步等。
在详细介绍基于日志(事件)的最终一致性之前,必须先稍微说下柔性事务和刚性事务,其实这些概念没那么复杂,所谓的刚性事务是指严格遵循ACID原则的事务, 例如单机环境下的数据库事务,就是之前我们1.0平台spring事务实现那种。柔性事务稍微复杂点,是指遵循BASE理论的事务, 常见的实现方式有很多种: 两阶段提交(2PC),TCC补偿型提交,基于消息的异步确保型,最大努力通知型等。
我说这么多其实就是想说:大事务=小事务(原子事务)+异步(消息通知)。
这个里面每个库里面都有两个事件表,eventPublish(待发布事件表)和eventProcess(待处理事件表),表设计如下,也可以针对自己业务场景具体设计。
好啦,举个简单例子,APP扫码启动摇摇车(涉及两个微服务)。
\1. 用户减少资金(数据库A)
\2. 商家增加资金(数据库B)
基于日志(事件)的最终一致性实现思路如下:
1.用户服务在接收到用户请求后开启事务, 减少资金, 并且在eventPublish表创建一条status为1(待发布)的记录, payload记录的是事件内容, 提交事务。
2.用户服务中的定时器扫描,方法里面开启事务, 然后查询eventPublish是否有status为1的记录,查询到记录之后,拿到payload信息,通过MQ将消息发布商家(商家端有MQ实时监听)。
3.商家服务监听到MQ传来的用户减少资金事件(先判断下数据库是否已经存在,如果存在,通过MQ发消息给用户,向用户端返回接收成功的消息), 在eventProcess表创建一条status为1(待处理)的记录,payload记录的是事件内容, 如果保存成功, 向用户端(用户端有MQ实时监听)返回接收成功的消息。(目的是用户端将eventPublish的status为2,下次扫描就不会扫描到了)
4.用户端监听到接收成功后的消息,将eventPublish的status为2(已发布),下次扫描就不会扫描到了,要不然会持续往商家那边发消息,告诉商家那边增加资金。
5.商家服务中的定时器扫描,方法里面开启事务, 然后查询eventProcess是否有status为1(待处理)的记录,查询到记录之后, 拿到payload信息,处理业务给商家增加资金. 处理成功之后修改数据库中eventProcess的status为2(已完成),,最后提交事务。
大概实现思路就是这样,这种处理方式网络请求吞吐量是比较高的,基本都是分段式异步处理的。
当然我们的业务场景比这个更加复杂些,一个流程涉及到的原子库更多,但是大概的思路都是类似处理,整个设计如下:
我们的具体业务情况整个流程还有两块也在优化中。
1.异常容灾处理,比如分段式进入到中间某段由于各种原因无法进行下一步,而且连续不断通过MQ和调度在执行多次,这个时候我们会检测,如果尝试重试多次无效,会进入到二级消息队列,并可以后台人工参与处理。
2.单链路方式的灵活性再加强,比如用户的下一段并不是到商家,也有可能是城市合伙人,或者其它,这个时候可以通过MQ的传递参数进行判断,下一段的到达点。
总之基于MQ分布式业务的处理场景,瓶颈稳定性在于MQ本身,所以这块必须保证它的持续高可用和稳定性,后续在项目中碰到的细节问题我会继续分享。
我的物联网项目(十五) 微服务业务拆分
微服务架构无论从业务层面,还是技术层面,要思考和解决的问题很多,其中有三大问题只要用到了微服务架构就必须要面对的,那就是拆分,事务,和查询。
当初规划这个2.0平台用微服务架构本身的目的是将平台以业务模块为中心,分而治之,摆脱1.0平台单体应用架构牵一发而动全身的痛点。每个业务模块独立管理,垂直发展,提供更多的对外API来满足平台逐渐复杂的业务需求。
团队是原先1.0的团队,开发人员依旧不变,同时没有复杂的跨部门,跨业务的沟通,项目管理依然是以技术负责人,架构师为指导,以业务模块为切分,各开发人员领取相关模块,开发模块和维护模块。同时也要考虑服务器数量和成本,项目开发的时间进度,综合各个方面,2.0平台微服务业务拆分以下九个模块:
业务拆分和数据库分库本身也是密不可分的,所以对应的数据分别如下:
我的物联网项目(十六) 项目工程
项目工程总共分三大块。前端系统用的是vue框架,业务系统用的是微服务框架springcloud,运营系统用的是传统的springMVC框架。
一 前端系统
前端系统主要分为PC端和移动端,分别有商家中心,城市合伙人以及运营平台。
二 业务系统
业务系统主要按模块划分,最终生成不同的jar包分别部署。
三 运营系统
主要包括统一接口平台,统一日志平台,统一权限平台,监控平台,调度平台,单点登录等系统。
我的物联网项目(十七) 系统调用流程
以商家进入商家中心的简单例子来阐述整个系统之间的调用流程。
1.打开商家中心,商家中心页面会检测cookie是否失效,如果失效跳转到sso单点登录页面。
2.sso认证用户和密码成功后,生成token写入redis和cookie。
3.商家中心从cookie获取权限菜单,如果没有,请求统一权限平台。
4.统一权限平台从cookie中获取token,从token中获取用户信息,查询当前商家角色和资源菜单存入cookie,并返回给html前端。
5.商家中心根据cookie中的权限展示相关的菜单。
6.菜单内容获取,请求统一权限平台。
7.统一权限平台通过负载均衡请求网关,API网管拦截,验证token是否在redis存在并合法,如果合法放行。
8.网关放行请求商家客户端负载均衡,客户端负载均衡请求商家微服务,查询商家数据库。
9.一级级返回到前端,前端展示内容数据。
(img-ExQLnLP3-1654313176760)]
业务拆分和数据库分库本身也是密不可分的,所以对应的数据分别如下:
[外链图片转存中…(img-N13wZrs8-1654313176762)]
我的物联网项目(十六) 项目工程
项目工程总共分三大块。前端系统用的是vue框架,业务系统用的是微服务框架springcloud,运营系统用的是传统的springMVC框架。
一 前端系统
前端系统主要分为PC端和移动端,分别有商家中心,城市合伙人以及运营平台。
[外链图片转存中…(img-Q2npV1Q7-1654313176763)]
二 业务系统
业务系统主要按模块划分,最终生成不同的jar包分别部署。
[外链图片转存中…(img-UK8VPPfA-1654313176764)]
三 运营系统
主要包括统一接口平台,统一日志平台,统一权限平台,监控平台,调度平台,单点登录等系统。
[外链图片转存中…(img-bQGzhNgb-1654313176766)]
我的物联网项目(十七) 系统调用流程
以商家进入商家中心的简单例子来阐述整个系统之间的调用流程。
[外链图片转存中…(img-o9R8UQMb-1654313176767)]
1.打开商家中心,商家中心页面会检测cookie是否失效,如果失效跳转到sso单点登录页面。
2.sso认证用户和密码成功后,生成token写入redis和cookie。
3.商家中心从cookie获取权限菜单,如果没有,请求统一权限平台。
4.统一权限平台从cookie中获取token,从token中获取用户信息,查询当前商家角色和资源菜单存入cookie,并返回给html前端。
5.商家中心根据cookie中的权限展示相关的菜单。
6.菜单内容获取,请求统一权限平台。
7.统一权限平台通过负载均衡请求网关,API网管拦截,验证token是否在redis存在并合法,如果合法放行。
8.网关放行请求商家客户端负载均衡,客户端负载均衡请求商家微服务,查询商家数据库。
9.一级级返回到前端,前端展示内容数据。