0.刚开始对海星框架的原理的理解(restemplete和配置文件加载的理解)
刚开始肯定是要对海星框架跑起来
1.基础学习
1.海星框架的配置加载
1.海星框架的配置加载主要是starfish-configuration这个jar包里实现的,项目启动首先进入的是BootstrapListener,然后获取到当前项目的路径。接下来会尝试获取配置文件,首先会进行判断是以jar还是war包的方式运行。通常开发环境都是以jar的方式运行,服务器环境是以war的方式运行,我在自己本地debug的时候发现进入了开发环境,然后会尝试获取项目中的conf/config.properties,因为我开发时候的配置文件放在resources目录下,所以这个方法就会返回空字符串。所以在开发环境下,即使没有config和install这两个配置文件也是不影响运行的,项目的运行完全依赖于spring中的配置文件中的配置项。
2.继续上一步判断,如果此时以war包的方式运行,而服务器上就会有平台生成的配置文件,这是就能正确读取配置文件中的配置项了,并把配置项放到environment。
最后进入postProcessBeanFactory来进行配置项的转化,比如修改全局配置项,对加密密码进行解密,对自定义配置项进行转换处理。
总结:
- 不需要在项目的src/main/resources目录下放配置文件了,放了也没有作用。项目的运行依赖于sping自己的配置文件中的配置项,例如application.properties文件
- 在application.properties文件可以只配置 @bic.bic.ip 就可以,其他核心服务需要的配置项可以省略(前提是部署在同一台服务器上)
- 通过运管中心配置自定义参数,不需要服务重启即可加载的场景中,那么配置后会在config.propertes文件中生成一个配置项。例如ppemweb.1.test=abc,代码中只需要使用@Value(“${test}”)注解即可获取修改后的值,不需要担心配置项中的组件标识和实例号。
- 线上环境有config,install两个配置文件,开发环境是不需要这两个文件的。如果开发时需要使用配置文件中的内容也不是不可以的,只需要把配置文件放到项目的根目录即可。例如项目根路径是D:\工作文档\农行信访项目\code\ppem,那么把conf目录放到根目录里面即可。
- 如果config配置文件和application.properties配置文件中的配置项是相同的,则config中的配置项有效。因为congfig中的配置项被解析后,给予了最高优先级。
2.海星框架的寻址调用
Spring Cloud通过在声明RestTemplate Bean时使用@LoadBalanced注解,在框架bean初始化的时候通过@LoadBalanced寻找到需要被增强的RestTemplate bean,并调用其中的api对其功能进行增强。海星也参照了该原理使用了@HikHttpLoadBalanced注解去声明对RestTemplate的改造。不过海星更进一步在框架内默认声明了被增强的RestTemplate bean,并且使用了 @Primary注解,这样用户在使用的时候就可以直接@AutoWired注入使用了,且因为@Primary注解的关系,如果当前IOC容器内存在多个RestTemplate实例则会优先注入海星寻址调用RestTemplate bean。
海星默认提供的Resttemplate对象不支持普通的resttemplate调用!若需要调用特定IP端口的服务,请声明一个普通的RestTemplate bean即可。但是因为海星内置的RestTemplate Bean使用了@Primary注解,因此即便声明了用户自己的RestTemplate bean,仍然无法仅通过@AutoWire注解完成注入。此时应当为重新声明的RestTemplate bean指定bean name并根据bean name进行指定注入。
1.比亚迪项目中的四个接口,出入口车辆,查询车流量,近七天访客趋势,模拟事件报文下发接口。
1.这四个接口是我在比亚迪项目中自己完成的接口,虽然实现上并不难,不过我在实际编码中学到了代码规范以及其他一些不一样的实现方法。
项目刚开始的时候就给了我一个ui图和车辆信息的数据表,首先根据设计的ui图确定大体编码思路,ui图中设计的是每四小时的进出园车流量的柱状图。于是先定义一个坐标信息(里面有两个list,第一个list是作为x轴信息,比如4,8,12,16,每四小时的时间信息。第二个list存放y轴以及线段名称)。然后开始编写接口,首先要获得当天的日期,然后确定当天的起始时间00到结束时间23.59,对这段时间进行分段处理。主要用到的mapkey这个注解,他能将存放对象的list转化为key值为对象的某一属性的map。我在mapper里编写相应的方法,并且把starttime和endtime,以及出入园状态status作为传入参数,然后mapkey注解把时间作为key。然后编写sql语句,用resultmap返回一个自定义的类,里面有时间和数据条数。这样查询的结果就是key为yyyymmddhh24,value为每个小时对应的数据条数。然后对这个map进行处理,得到每四个小时的车流量数据,最后返回坐标信息。
2.近七日访客趋势
还是根据项目给的ui图,ui图上的横坐标是最近七天的日期,纵坐标是每天的访客数量。还是一样的思路,先定义一个坐标信息(里面有两个list,第一个list是作为x轴信息,比如8.16,8.15,8.14,每天的日期信息。第二个list存放y轴(每天的访客人数)以及线段名称)。首先确定查询的起始时间和结束时间,起始时间是六天前的起始时间,结束时间是当天的最晚时间。然后用到的mapkey这个注解,他能将存放对象的list转化为key值为对象的某一属性的map。key值为格式化的yyyymmdd,value值是通过count查询出的每天的所有游客数据,这样就得到了这个map信息,然后对横纵坐标进行填充和处理就得到了坐标信息类,最后返回坐标信息。
2.比亚迪项目中的事件告警,esc源码,mpc推送
事件订阅整体流程:比亚迪组件先从esc订阅事件,返回结果。然后esc接收到事件后判断是否订阅,如果订阅转发到消息队列中,然后比亚迪项目组件监听这个队列获取事件。
1.比亚迪项目中aciveMq使用的学习与总结,对esc事件服务以及相关服务的学习。1.在queue模式下,avtivemq的配置类中配置JmsListenerContainerFactory进行配置,然后消费者监听队列的时候可以使用JmsListener注解来使用配置的这个ContainerFactory,注解中最主要的就是destination(即监听的队列名称)。消费者这部分很好理解,整个链路的核心是生产者是谁,消费者监听的队列的数据从哪来,对这一部分跟着项目和源码深入学习。
2.首先事件服务esc组件有很多功能,这里用到了事件按需订阅,保证业务组件接收到的是“需要”的事件,降低业务组件处理的压力,比亚迪项目中用到了六个事件,都与温度、交通等有关。代码部分的实现:首先事件订阅的init类先继承CommandLineRunner,以保证订阅事件在容器初始化完成后进行,需要注入escClient和hikDiscoveryClient。核心是用到了esc包中Escclient中的eventRouter方法(里面还有一个类似的subscribeEvent方法,就是对eventRouter方法进行了封装)。eventRouter方法需要传入一个EventRouterDto,这个数据传输类存的主要是需要订阅的事件信息(比如:dstServiceType;//接收事件的组件,这里就是bydmls,以及事件类型,事件等级、事件发送目的URL....),对EventRouterDto里注入了六种项目中需要的事件的信息,然后调用eventRouter方法,这个方法里面调用了esc组件中的/eventService/v1/eventRouter(事件订阅接口),并且header是里面方法生成的token,token生成入参为(安全会话ID和随机数据加密密钥-16进制编码 ),请求的实体就是传入的EventRouterDto的信息,然后返回成功信息。所以就实现了每个事件向哪个队列转发,并且能选择需要的事件向对应的队列转发,然后作为消费者只需要监听对应的队列就可以了。
1.整体链路和问题排查
1.排查问题:项目本地启动的时候消费者监听队列收不到生产者发送的事件报文信息。先在运管中心查看mq的组件,每触发一次事件,mq的入队数和出队数都会增加,而且出队数和入队数是相等的。然后这个项目也部署在服务器上,可能服务器上的组件也在监听这个队列,从而把消息都消费了。后续收到了移动侦测的事件,但是理论上还应该收到温度报警的事件。先查看运管中esc的日志,修改日志的等级,如果ESC接收到事件,会看到recv event后面的事件信息,然后确实看到了131331事件码的移动侦测事件。用clientdemo来添加设备然后布防,可以发现该设备一直在发送移动侦测的事件。触发温度报警后,可以看到报警信息,然后esc日志中出现了温度报警事件的信息。
2.所以无论是周界侦察或者温度报警,ESC都是接收到了事件的。然后继续排查esc组件是否转发了事件,ESC组件接收到事件后,会将事件信息与内部的订阅条目一条一条匹配,匹配规则包括事件源(srcIndex)、事件类别(ability),如果匹配成功,会将事件转发到订阅地址。这边也看到了转发成功了,有send send to dst amq://10.19.218.110:7018/queue/bydspslms.mq.error.event.queue。
3.所以无论是esc还是mq都是没有问题的。于是查看byd组件的日志,调整byd组件日志等级,能看到事件告警信息的成功插入。所以说,本地项目无法消费的原因是因为都被另一个服务器上的组件消费了。
4.修改事件报文的格式,设备上发事件的实际报文和hido上事件类型管理中的事件中的报文格式有区别,对照设备实际发送的报文进行修改。
2.mpc推送
4.代码规范的学习(url中禁止使用变量,)
3.这个项目与pdms的关联
4.mypdbs中车辆态势看板的概要设计(解决问题:接口的并发量(redis缓存,http连接池、异步线程池、completefuture))、定时任务的使用
3.车辆态势看板概要设计文档编写以及开发中接口优化的思考
0.redis缓存
1.http连接池
首先,明确两点:
1.http连接池不是万能的,过多的长连接会占用服务器资源,导致其他服务受阻
2.http连接池只适用于请求是经常访问同一主机(或同一个接口)的情况下
3.并发数不高的情况下资源利用率低下
那么,当你的业务符合上面3点,那么你可以考虑使用http连接池来提高服务器性能
使用http连接池的优点:
1.复用http连接,省去了tcp的3次握手和4次挥手的时间,极大降低请求响应的时间
2.自动管理tcp连接,不用人为地释放/创建连接
使用http连接池的大致流程 :
1.创建PoolingHttpClientConnectionManager实例
2.给manager设置参数
3.给manager设置重试策略
4.给manager设置连接管理策略
5.开启监控线程,及时关闭被服务器单向断开的连接
6.构建HttpClient实例
7.创建HttpPost/HttpGet实例,并设置参数
8.获取响应,做适当的处理
9.将用完的连接放回连接池
2.异步线程池
3.异步任务completefuture
1.看板的设计流程
1.编写概要设计
2.开发接口
3.对接copas
一、账号服务+商品服务开发
1.使用 JWT 以及拦截器实现了用户的无状态登录以及鉴权;
JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.
进行连接形成最终传输的字符串
1.登录先根据手机号查数据库中的信息,然后比对输入的密码。这里数据库的密码是原始密码加盐然后再进行md5加密,所以也要对用户输入的密码进行加盐,然后md5加密后和数据库中的密码进行比对,登录成功后,使用jwt产生token,将包含用户信息的数据作为JWT的Payload(比如账号信息,用户名信息等等),将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串。 然后以后用户登录的时候,可以验证传过来的token,检验成功后就可以得到用户的信息,然后进行其他逻辑的操作。
2.使用线程池和 http 连接池来提高多个核心接口(如短信发送接口)的 QPS;
场景:发送短信的接口qps低
1.短信发送的时候,参考短信服务的商家的文档,里面规定了发送短信的方法规范,方法有三个参数,分别是要发送的手机号,还有模板id,这个模板id由服务商提供,还有一个参数value,存储短信的内容。首先要指定http的请求头,设定相应的keyvalue,用作鉴权,key就是Authorization字符串,value是商户号。然后通过restTemplate对商家指定的url进行post请求来得到相应的响应,如果响应码是2开头就是请求成功。
2.然后接下来对这个过程进行优化,首先这个过程是用户请求短信服务,短信服务通过http请求第三方服务商,然后第三方服务返回响应,然后短信服务告知用户结果,这个是一个同步发送的过程。第一个请求未完成,第二个请求也发不出去,所以采用异步发送的方式,
通过采用@Async注解和自定义线程池的方式来("threadPoolTaskExecutor")实现,采用自定义线程池能自定义线程池的参数,比如maxpoolsize,默认的线程池的maxpoolsize的大小是Integer.max,所以当任务很多的时候,会引起堆内存溢出,或者当阻塞队列中任务很多的时候,这时候如果宕机,就会引起消息丢失,所以自定义线程池中要自定义最大线程数的大小。
3.最后,海量请求下可能阻塞队列里可能无线增长,或者oom。还要提高消费性能,用http连接池
从消费方角度看,阻塞队列里的任务是向第三方http请求,而且每次http请求都要三次握手,四次挥手来开启和关闭,如果能让这个HTTP请求消费的快一点,那么阻塞队列中的任务存储的数量就会变少。所以就采用了http连接池,http连接池也是和线程池一样的池化思想。
1.当有连接第一次使用的时候建立连接
2.结束时对应连接不关闭,归还到池中
3.下次同个目的的连接可从池中获取一个可用连接
4.定期清理过期连接
然后实现http连接池,其实就是通过对restTemplate方法改写,因为他的底层实现httpClient,封装了关于http连接池的一系列方法,而我们只需要通过配置文件的形式设置超时时间,最大连接数、每路由的最大连接数等等即可。而达到复用是通过连接的route(IP+PORT)从每个route的池中获得连接,如若超过数量,则创建或等到,知道超时。
4.最后发送短信验证码就通过异步线程池加上http连接池的方法提高了QPS,我用jmter自己压测的时候,在相同配置下,从最开始采用同步发送和restTemplte未采用http连接池,此时的qps只有400-500左右,最后提高了10倍到了3000-4000左右。而且我自己压测机器和开发运行服务的机器是在同一台电脑上,会有性能损耗,实际的QPS会更高。
3.海量数据下采用惰性策略更新和发放流量包;
业务场景:因为我们做的是短链平台,用户需要使用流量包来创建短链,创建短链完要扣减流量包的使用次数。流量包也有过期时间,需要删除过期的流量包,同时流量包有付费流量包和免费流量包,免费流量包每天会赠送使用次数,比如每天免费使用5次。
海量数据下采用惰性策略更新维护流量包
因为创建短链会消耗流量包中的次数,流量包可创建短链的次数需要每天更新,比如每天5次,所以流量包的使用次数需要更新维护。
关于更新维护的方案最容易想到的就是,定时任务-xxljob,可以每天用xxl-job调度中心定时执行账号微服务中的流量包更新服务
但是在海量数据下就会有问题,如果用户量大,会有更新延迟。比如每天0点定时更新,数据量一多,肯定会有更新延迟。所以就采用惰性思想来更新流量包,这里也借鉴了redis淘汰过期key的思想。不用每天更新全部流量包,只需要在用流量包的时候更新就可以了。使用的时候查询用户的所有流量包,然后根据流量包里的更新时间来判断流量包是否需要更新。如果没有更新,那么就增加使用次数,然后再进行扣减流量包。比如创建短链,然后这个流量包是三天前用的还剩3次,但是每天可以用5次,这时候就可以在流量包扣减之前,把流量包的使用次数先更新到5次,然后再进行扣减。
Redis过期key淘汰策略
主动策略:给过期的key加定时器,当时间到达过期时间的时候会自动删除key,等于cpu不友好,会占用很多的cpu。
惰性策略:访问一个key的时候判断这个key是否达到过期时间了,过期了就删除。
定期删除:
隔一段时间,就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除,
定期删除可能会导致很多过期 key 到了时间并没有被删除掉
Redis 会每秒进行十次过期扫描,过期扫描不会遍历容器中所有的 key,而是采用一种特殊策略
从容器中随机 20 个 key;
删除这 20 个 key 中已经过期的 key;
如果过期的 key 比率超过 1/4,那就重复步骤 1;
惰性删除 :
当某个客户端试图访问key时,发现该key已超时会把此key从内存中删除
如果Redis是主从复制
从节点不会让key过期,而是主节点的key过期删除后,成为del命令传输到从节点进行删除
主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key
指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在
Redis服务器实际使用的是惰性删除和定期删除两种策略
通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
问题
如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?
如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,就需要走内存淘汰机制
步骤
- 查询用户全部可用流量包
- 遍历用户可用流量包
- 判断是否更新-用日期判断(要么都更新过,要么都没更新,根据gmt_modified)
- 没更新的流量包后加入【待更新集合】中
- 增加【今天剩余可用总次数】
- 已经更新的判断是否超过当天使用次数
- 如果没超过则增加【今天剩余可用总次数】
- 超过则忽略
- 没更新的流量包后加入【待更新集合】中
- 判断是否更新-用日期判断(要么都更新过,要么都没更新,根据gmt_modified)
- 更新用户今日流量包相关数据
- 扣减使用的某个流量包使用次数
4.使用简单工厂加策略模式实现多通道支付对接,并更新微信 V3 支付版本;
首先定义了和支付有关方法的strategy策略接口,这个接口有支付、查询订单状态、退款等方法,其中有具体策略类实现,比如这两个具体的实现类就是支付宝支付方式和微信支付方式的具体实现,里面有对应方式的退款、查询订单状态、以及支付方法。
然后定义一个context上下文类,这个类的成员变量就是就是stratgy这个接口。在使用的时候就是实例化这个context类,并且传入比如微信支付策略实现类,或者支付宝支付策略实现类。
然后这样还是不方便,还是要自己实例化创建对象。
又用了简单工厂模式来进一步优化。简单工厂就帮忙实例化相应的对象,支付方法中传入的支付信息数据传输类,根据里面的支付方式来确定context这个对象初始化的时候构造函数传入的具体实现类,然后调用相应方法。
所以说最后只要根据传入的payinfo信息,使用支付工厂的pay方法,就能创建context对象,并且执行相应方式的支付方法。
而且采用这种设计能减少代码的耦合性,便于以后的修改,比如增加一种支付方式,就是增加一个具体的strategy类。
https://blog.csdn.net/yk614294861/article/details/122885700
微信 V3 支付版本
1.加密算法我们整体可以分为:`可逆加密`和`不可逆加密`,可逆加密又可以分为:`对称加密`和`非对称加密`。
其中微信支付用的是rsa非对称加密这种方法。非对称加密的过程就是甲方生成公钥和私钥,然后甲方把公钥发给乙方,如果乙方发送消息的时候,就用这个公钥加密消息,然后甲方收到的时候就用这个私钥解密消息。
------------
然后结合微信支付更具体的流程是
这里有四个密匙,分别是商户的公钥私钥和微信平台的公钥和私钥。
其中商户有自己的私钥和公钥,可以使用相应接口下载微信平台的公钥。
这里有两套规则,一套是用来保证步骤 【统一下单】时的信息安全
另一套是用来保证支付平台回调时的信息安全
1.用户请求到相应的接口,然后用M(私钥)计算签名,微信支付那边用M(公钥)验证签名.。W(私钥)计算签名,然后同步返回给商户,用W(公钥)验签。
2.微信支付成功后要回调通知商户,微信支付用W(私钥)计算签名,商户用W(公钥)验证签名。
-------------------------------
具体开发:
开发的前置工作需要申请appid和商户号id,这些申请需要资质,这里是老师帮我申请的。然后下载获取自己商户的私钥和公钥。
然后以下单接口的开发为例:
natice支付过程:
1.商户的后台系统生成订单,保存到数据库,然后调用微信的统一下单接口,这时候需要传很多参数,比如签名等等。请求过去之后,微信支付系统那边会生成预支付交易的信息,也会写入相应的数据库,然后返回预支付的code_url,然后后台根据code_url生成相应的二维码连接。
2.接下来这一步是用户和微信支付系统的交互,和我们后台系统没有关系。用户扫二维码,微信支付验证此次交易链接是否有效,以及用户确定支付密码授权,完成支付交易。
3.接下来的过程都是并行处理。用户这边,微信支付完先发送短信和微信消息提醒给用户。商户这边,微信支付会回调通知给商户支付结果,商户返回支付通知接受的情况,这个过程会重试几次。如果没有收到微信支付的回调通知,也可以自己调用查询订单状态的api,来返回支付状态。
4.最后发货,也就是增加流量包的次数。
-------------
加密算法我们整体可以分为:可逆加密
和不可逆加密
,可逆加密又可以分为:对称加密
和非对称加密
。
不可逆加密:hash算法,MD5,SHA
对称加密:
就一个密钥,用来加密和解密
DES: 全称:Data Encryption Standard,现已被破解
3DES:全称: Triple Data Encryption Algorithm, 暂时未被破解
解释: 3DES 是在 DES 基础算法上的改良,该算法可向下兼容 DES 加密算法,但计算性能不高,暂时还未被破解
AES: 全称:Advanced Encryption Standard,暂未被破解
非对称加密:
流程:
1.接收方生成公私钥对,私钥由接收方保管。
2.接收方将公钥发送给发送方
3.发送方通过公钥对明文加密,得到密文
4.发送方向接收方发送密文
5.接收方通过私钥解密密文,得到明文
--------------------
流程:
1.甲方生成公私钥对,私钥由甲方保管。
2.甲方将公钥发送给乙方
3.乙方通过公钥对明文加密,得到密文
4.乙方向甲方发送密文
5.甲方通过私钥解密密文,得到明文
- 对称加密:操作比较简单, 加密速度快, 秘钥简单如果泄露就危险
- 非对称加密:安全性更高,加解密复杂,但是加解密速度慢
https://blog.csdn.net/qq_45901741/article/details/119223513 https加密
支付宝和微信支付加密方式:采用RSA非对称加密+对称加密混合使用
https传输过程
1.客户端根据https请求到服务端,有公钥和私钥,并且为了防止第三方截取公钥,来冒充代替服务器的公钥,所以先要验证ssl证书(CA机构颁发的数字证书)
2.然后服务器把有资质的crt公钥发给客户端
3.如果客户端验证成功,产生随机的key,通过公钥加密发送给服务端
4.服务端通过自己的私钥解密拿到key
5.然后就用这个key来进行对称加密传输信息
第三方支付平台举例:
提供了两套 RSA 加密
- 一套是用来保证步骤 【统一下单】时的信息安全
- 另一套是用来保证支付平台回调时的信息安全
具体过程
微信签名和验签的交互图(商户证书(M)和平台证书(W)的使用说明)过程:
1.用户请求到相应的接口,然后用M(私钥)计算签名,微信支付那边用M(公钥)验证签名.。W(私钥)计算签名,然后同步返回给商户,用W(公钥)验签。
2.微信支付成功后要回调通知商户,微信支付用W(私钥)计算签名,商户用W(公钥)验证签名。
其中商户有自己的私钥和公钥,可以使用相应接口下载微信平台的公钥。
微信支付V3开发前工作
1.配置参数
1.申请APPID:
由于微信支付的产品体系全部搭载于微信的社交体系之上,所以直连商户或服务商接入微信支付之前,都需要有一个微信社交载体,该载体对应的ID即为APPID
2.申请mchid:
商户平台有商户号mchid
3.绑定APPID及mchid:
APPID和mchid全部申请完毕后,需要建立两者之间的绑定关系。
直连模式下,APPID与mchid之间的关系为多对多,即一个APPID下可以绑定多个mchid,而一个mchid也可以绑定多个APPID。
2.配置API key
API v3密钥主要用于平台证书解密、回调信息解密,具体使用方式可参见接口规则文档中证书和回调报文解密章节
3.下载并配置商户证书
商户API证书具体使用说明可参见接口规则文档中私钥和证书章节
商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
平台证书是指微信支付负责申请的证书,包含微信支付平台标识、公钥信息的证书
商户在调用API时使用自身的私钥签名,微信支付使用商户证书中的公钥来验签。微信支付在相应的报文中使用自身的私钥签名,商户使用平台证书中的公钥来验签名。
- apiclient_cert.pem:商户证书,封装了公钥
- apiclient_key.pem: 商户证书,封装了私钥
支付策略接口开发
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml 开发指引
统一下单接口(unifiedOrder)
natice支付过程:
1.商户的后台系统生成订单,保存到数据库,然后调用微信的统一下单接口,这时候需要传很多参数,比如签名等等。请求过去之后,微信支付系统那边辉生成预支付交易的信息,也会写入相应的数据库,然后返回预支付的code_url,然后后台根据code_url生成相应的二维码连接。
2.接下来这一步是用户和微信支付系统的交互,和我们后台系统没有关系。用户扫二维码,微信支付验证此次交易链接是否有效,以及用户确定支付密码授权,完成支付交易。
3.接下来的过程都是并行处理。用户这边,微信支付完先发送短信和微信消息提醒给用户。商户这边,微信支付会回调通知给商户支付结果,商户返回支付通知接受的情况,这个过程会重试几次。如果没有收到微信支付的回调通知,也可以自己调用查询订单状态的api,来返回支付状态。
4.最后发货
开发具体过程
1.设置参数配置类,WeChatPayConfig
#商户号
pay.wechat.mch-id=1601644442
#公众号id 需要和商户号绑定
pay.wechat.wx-pay-appid=wx5beac15ca207c40c
#商户证书序列号,需要和证书对应
pay.wechat.mch-serial-no=7064ADC5FE84CA2A3DDE71A692E39602DEB96E61
#API V3密钥
pay.wechat.api-v3-key=adP9a0wWjITAbc2oKNP5lfABCxdcl8dy
#商户私钥路径(微信服务端会根据证书序列号,找到证书获取公钥进行解密数据)
pay.wechat.private-key-path=classpath:/cert/apiclient_key.pem
#支付成功页面跳转
pay.wechat.success-return-url=https://xdclass.net
#支付成功,回调通知
pay.wechat.callback-url=http://3xr665.natappfree.cc/api/callback/order/v1/wechat
然后再开发一个PayBeanConfig类加载私钥:apiclient_key.pem
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient/blob/master/README.md
httpclient依赖,微信在httpclient的基础上封装了签名和验签的过程。可以定时获取微信签名验证器,自动获取微信平台证书(证书里面包括微信平台公钥), 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新。
2.native下单代码编写
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml api文档
/**
* 快速验证统一下单接口,验证密钥是否过期
1.生成商户32位内部订单号
2.生成post请求,设置传输json数据,json对象中有mchid、out_trade_no、appid、notify_url还有一个amount的json对象
3.设置请求头和请求体,然后通过封装的httpclient发送请求(其中自带签名,验签过程),返回响应对象,获取响应码和响应体code_url
* @throws IOException
*/
@Test
public void testNativeOrder() throws IOException {
String outTradeNo = CommonUtil.getStringNumRandom(32);//商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
/**
* {
* "mchid": "1900006XXX",
* "out_trade_no": "native12177525012014070332333",
* "appid": "wxdace645e0bc2cXXX",
* "description": "Image形象店-深圳腾大-QQ公仔",
* "notify_url": "https://weixin.qq.com/",
* "amount": {
* "total": 1,
* "currency": "CNY"
* }
* }
*/
JSONObject payObj = new JSONObject();
payObj.put("mchid",payConfig.getMchId());
payObj.put("out_trade_no",outTradeNo);
payObj.put("appid",payConfig.getWxPayAppid());
payObj.put("description","小滴课堂海量数据项目大课");
payObj.put("notify_url",payConfig.getCallbackUrl());
//订单总金额,单位为分。
JSONObject amountObj = new JSONObject();
amountObj.put("total",100);
amountObj.put("currency","CNY");
payObj.put("amount",amountObj);
//附属参数,可以用在回调
payObj.put("attach","{\"accountNo\":"+888+"}");
String body = payObj.toJSONString();
log.info("请求参数:{}",body);
//wechatPayClient,之前封装的httpclient,自带签名,验签功能
StringEntity entity = new StringEntity(body,"utf-8");
entity.setContentType("application/json");
HttpPost httpPost = new HttpPost(WechatPayApi.NATIVE_ORDER);
httpPost.setHeader("Accept","application/json");
httpPost.setEntity(entity);
try(CloseableHttpResponse response = wechatPayClient.execute(httpPost)){
//响应码
int statusCode = response.getStatusLine().getStatusCode();
//响应体
String responseStr = EntityUtils.toString(response.getEntity());
log.info("下单响应码:{},响应体:{}",statusCode,responseStr);
}catch (Exception e){
e.printStackTrace();
}
}
2022-05-08 13:47:42.179 INFO 25680 --- [ main] net.xdclass.biz.WechatPayTest: 请求参数:{"amount":{"total":100,"currency":"CNY"},"mchid":"1601644442","out_trade_no":"6BvSHMCXr212tZi6Xsg3mXGCE9NDZ5Bx","appid":"wx5beac15ca207c40c","description":"小滴课堂海量数据项目大课","attach":"{\"accountNo\":888}","notify_url":"http://3xr665.natappfree.cc/api/callback/order/v1/wechat"}
2022-05-08 13:47:42.574 INFO 25680 --- [main] net.xdclass.biz.WechatPayTest : 下单响应码:200,响应体:{"code_url":"weixin://wxpay/bizpayurl?pr=hqA5QErzz"}
5.使用 AOP+自定义注解,解决网络延迟导致重复支付订单问题;
下单时候,前端按钮重复点击,或者网络原因,用户重复刷新,最后造成订单创建多次
常见方案
1.前端JS控制点击次数,屏蔽点击按钮无法点击,但是前端可以被绕过,前端有限制,后端也需要有限制 。
2.数据库或者其他存储增加唯一索引约束,需要想出满足业务需求的唯一索引约束,比如注册的手机号唯一
3.服务端token令牌方式
下单前先获取令牌-存储redis 下单时一并把token提交并检验和删除-lua脚本
分布式情况下,采用Lua脚本进行操作
https://www.cnblogs.com/dw3306/p/9615197.html aop实现自定义注解
自定义注解防重提交
先创建一个自定义注解,自定义注解需要元注解来注解,元注解就是注解注解的注解,最常见的就是,
@Target(ElementType.METHOD)//注解用在方法上
@Retention(RetentionPolicy.RUNTIME)//保留到虚拟机运行时(最多,可通过反射获取)
两种方式
1.令牌形式防重提交
在用户请求的时候可以生成一个token,就用uuid生成一个32位的随机数,提交订单的时候就获取token并且校验一下,然后删除,接下来如果重复请求就找不到token就失败了
2.参数形式防重提交,ip+类+方法方式
用户请求到某个接口,可以拿到类名家方法名加IP,把这些信息当作key存在redis中,并且设置过期时间,比如5秒,如果5秒内有重复请求,那么redis发现有相同的key,就请求失败了。
使用aop+自定义注解可以解耦,比如想在哪个接口那边增加防重提交,只需要在那个方法上增加自己的自定义注解就可以了。把这个注解增加在controller层。
aop就是面向切面编程,开发的步骤就是原始类,额外功能,切入点,组装切面这四个过程。其中后面三个过程可以通过aspect注解定义一个切面类,它的主要作用就是实现额外功能和切入点。实现的额外功能就是刚才说的防重提交的功能,通过@Pointcut和@Around联合注解,pointcut定义切入点就是注解,around注解定义pointcut的方法,然后在这个方法里写入防重的业务逻辑。最后哪个controller接口需要防重提交那就应用这个注解就可以了。
二、短链服务开发
1.基于 MurmurHash 算法生成和解析短链,并设计了库表位对短链数据进行分库分表;
长链转为10进制数,然后转为62进制数
1.首先短链的应用场景,最常见的就是短信发送短链,如果网址太长可能会增加短信的服务费。短链组成就是https协议://短链域名/短链码。还可以对一些商品进行推广以及数据统计。一个长连接可以对应不同的短链接,所以可以用来统计不同渠道推广的短链的点击次数,比如在抖音推广使用a短链,在微信推广使用b短链,然后可以统计每个渠道的访问量。
2.短链码生成有很多种方案,
1.比如利用数据库的自增ID来产生,设定一个ID的初始值,比如10w开始自增,然后把这个id值转化为一个62进制的数,他的优点是不会重复,但是缺点也很明显,比如短链的长度不固定,然后存在【业务数据安全】问题,比如有一个人今天获得一个短链码,第二天继续获得一个短链码,因为是自增的,他只要计算差值,就能大概知道公司的数据量是多少了。
2.或者把长连接进行md5加密,但是这样数据一多会有重复的可能。
3.我使用的是MurMurHASH,这是一种非加密的哈希,他把长链转化为一个十进制数,它是32bit的能表示最大接近43亿的十进制数,然后把这个十进制数改为62进制。
比如10进制:1813342104 转62进制:1YIB7i 常规短链码是6~8位数字+大小写字母组合
除了生成的这个短链码,比如123abc,还要对短链码进行处理,用作下一步的分库分表,在短链码的前面加上库位,在最后面加上表位,便于后续对短链的数据进行分库分表。
5.比如对123abc这个短链,先确定有几个库,比如三个库,0,1,a,四个表0,1,2,3.然后生产这个短链的hash值,对库的数量和表的数量进行取模,来得到相应的库表位。这样库表为也是为了后续数据能命中相应的库表。然后使用sharingjdbc自定义精准分配策略,把短链码当作分片键,然后通过短链码的首位库位和最后一位表位,获取数据库表。比如0123abc1,这个短链就是进入数据库0,表1中。如果短链码有三个前缀,两个后缀,这样就有六个表。
.这样的好处就是在数据量增加的时候,扩容避免可以迁移数据,可以直接增加前缀和后缀来增加数据库和表。
4.短链生成之后,通过访问短链,先从数据库中找到原始的URL,然后响应http 状态码301或者302重定向到目标地址,然后目标地址对用户响应。
这个过程涉及到对重定向状态码的选择,301 是永久重定向,会被浏览器硬缓存,第一次会经过短链服务,后续再访问直接从浏览器缓存中获取目标地址,302 是临时重定向,不会被浏览器硬缓存,每次都是会访问短链服务。选择301的话会减少服务器的压力,但是无法统计点击短链的次数以及其他数据,所以还是选择302状态码。在代码实现中只要把http响应头中的location属性改成原始的URL,这个属性表示这个短链网址的跳转网址,也就是重定向网址。
5.所以这个就是短链码的应用场景,生成方案的选择,以及短链码跳转的实现。
2.通过冗余双写方案解决在分库分表的场景下多维度(用户端和商家)查询问题;
背景:分库分表的时候应该有碰到⼀个问题,⼀个数据需要根据两种维度进⾏查询,但是我们在进⾏分库分表是只能根据⼀种维度进⾏。⽐如:⽤户购买了商品产⽣了订单,当⽤户⾮常多的时候,我们会选择订单根据下单⽤户的ID进⾏分库分表。但是这⾥⾯存在⼀个问题就是作为卖家要如何查询我卖出的所有订单呢?因为订单是根据⽤户id规则进⾏分库分表的,卖家订单查询接⼝物理上是⽆法⼀次查询出当前的订单的,应该他分别分布在不同的订单库或者订单表中的,有可能我查询⼀个订单列表,需要跨⾮常多的表,这个样性能是⾮常低效的。
同样的,我对短链信息表进行了分库分表,用户可以根据短链信息表进入相应的库表中进行查询,但是如果商家想查询某一分组下的短链,每个短链信息都是不同用户的,也会有跨表操作,所以在创建短链的时候生成两份短链信息。在数据库分别创建用户短链表和商家短链表,商家短链表是给B端用的,访问量少,单表数据可以大一点,用账号的唯一标识account_no分库,用group_id来分表,然后商家查询自己某一组的全部短链数据的时候,就可以通过group_id和account_no作为分片键来查看库表了。
在dcloud_0和1库中分别创建group_code_mapping _0和1的表,这个表和short_link是一样的,只是给b端用的,访问量少,单表数据可以大一点,分库的partitionkey是account_no,ds$->{account_no % 2}分表PartitionKey:group_id.
group_code_mapping_$->{group_id % 2}
用户短链信息表就是对短链来进行分库分表
比如请求创建短链的接口,传入需要创建的短链的相关信息
这时候并不是直接创建短链,而是用到冗余双写的思想,往商家短链数据库和用户短链数据库写两份数据。这个过程是通过rabbitmq实现的,如果满足创建短链的条件,那么发送一个带有指定路由key的消息,然后消息进入交换机,再进入两个队列,一个是要在用户数据库创建短链信息表的队列,另一个是要在商家短链信息表创建信息的队列。最后在编写两个相应的消费者监听这两个队列的消息,来执行向数据库双写的逻辑。
处理消息失败的问题:在消费者端还设置了重试次数和重试时间,如果有异常消费失败,那么会每隔5秒重试一次,最多能重试四次。如果重试次数超过了阈值,那么先手工确认一下这个消息,然后把这个消息转发到异常交换机,再到异常队列,然后由异常队列监控服务消费,向相应的研发人员发送短信或者邮件来告警排查问题。
1.这个过程确保了最终一致性,比如一个创建短链的消息,分别到了B端的消费者和C端的消费者,如果C端的消费者消费成功,往用户短链数据库插入了数据,但是C端的消费者消费失败,商家的短链数据库没有数据,那么这次冗余双写只写进去了一个数据库。不过这时候消费失败的那个消息还是在消息队列里面,这个消息重试到一定次数就可以通过异常交换机通过告警服务让开发人员排查原因,解决问题之后确保数据库的一致。这个过程不能实现强一致性,但还是保证了最终的一致。常规情况下,这种情况很少,都会消费成功,但是以防万一也预留了解决方案。
总结:通过发送mq消息队列,来往数据库中写入数据,性能很高,削峰。这是弱一致性,如果需要强一致性的场景,比如往两个数据库插入,要么同时成功,要么同时失败,这样的话就不适用。不过这个业务允许这种场景。
主键重复问题
3.创建短链时高性能扣减流量包,采取 redis 实现预扣减方案且保证了数据一致性;
首先创建creatshortlink接口创建短链,然后这时候在redis里对流量包进行预扣减,redis中存储的是用户今日剩余的流量包,如果流量包次数不足,那么直接返回流量包不足。如果redis不存在这个key,那么等后续更新流量包的时候设置key,value。
(String script = "if redis.call('get',KEYS[1]) then " +
"return redis.call('decr',KEYS[1]) " +
"else return 0 end";)
如果流量包足够,那么发送mq消息,经过b端,c端消费(冗余双写),创建两种类型的短链。然后消费者rpc调用扣减流量包的请求,扣减流量包中用到的是查询全部流量包,更新今日流量包,还有再扣减使用的流量包。然后把流量包的过期时间(到当天晚上12点过期),次数放到redis
4.采用 MQ 延迟队列+本地 Task 解决了短链码和流量包扣减的分布式事务问题;
短链服务创建短链成功的时候会在短链数据库中创建短链相关的数据,同时在创建短链的时候,也会调用流量包服务对相应的流量包使用次数进行扣减。这时候如果短链创建失败了,但是流量包次数又扣减了,就会有短链库和流量包库数据不一致的分布式事务问题。
我采用的解决方案是在流量包服务扣减库存前保存一个task任务,记录扣减的流量包, 扣减更新流量包和保存task任务也要放在同一个事务下面。如果这时候流量包扣减了,但是创建短链失败了,可以通过分布式调度轮训task表,检查超时未完成的任务,比如超过十分钟记录,这个记录未更新,就要检查短链是否存在,如果不存在,那么就恢复流量包次数进行回滚。
详细说明:task表中有accuntno,traffic_id 流量包id,use_time,使用次数,lock_state 用于记录状态,有lock,finish,cancel状态,biz_id 短链码。在扣减流量包的时候先会往task表中插入相应的数据,比如流量包的id,最新的使用次数,然后锁定状态,然后会发送一个mq消息,可以延时5分钟,或者十分钟,然后时间到了之后流量包消费者执行相应的逻辑:
检查task表中的数据是否存在
如果存在就去检查短链是否成功
如果不成功,则恢复流量包的使用次数
删除task (也可以更新task状态,定时删除就行)
所以,这样就保证了短链库和流量包库最终都能成功创建以及使用次数扣减,或者全部失败。也就实现了数据的最终一致性。
三、Flink 实时计算服务+数据可视化服务开发
1.对接短链服务数据埋点,基于数仓分层模型设计 Flink 实时计算处理链路;
ODS:原始数据层
1.在linkservice下的resource里创建logback.xml文件,指定某个类单独打印日志。
2.创建LogService以及实现类
3.public void recordShortLinkLog(HttpServletRequest request, String shortLinkCode, Long accountNo) 方法
4.在LinkApiController的短链路径里使用这个方法来打印日志。在访问的时候,可以获取ip,以及全部的http请求头,在请求头中获取一些有用信息,比如user-agent,referrence,把这些信息可以通过控制台打印出来。然后把这些信息发送到kafka。(kafka需要创建topic)
5.在通过logservice中将信息发送到kafka的ods_link_visit_topic中,也就是说,用户点击一个短链,在controller层写入了一个日志服务,记录用户的ip,还有http请求头等信息,然后把这些信息封装好发送到kafka的topic中,然后在这个消息队列中的数据作为flink中的初始数据来源来作下一步的处理,包括数据仓库分层,数据可视化处理等等。
2.Flink 数据分层开发,通过对 ODS/DWD/DWM/DWS层处理,对访问短链用户的来源、设备信息、地理位置等信息做出分析
DWD层
将之前通过logservice日志服务中获取的信息发送到kafka的topic1中,将这个topic1作为flink的输入souce,然后对原始数据处理,抽取,转换、加载出数据中的新老访客信息。
唯一设备标识:要通过用户访问得到的信息(比如ip地址,http的user agent,或者cookie等),生成一个唯一标识,这里使用的就是ip+useragent(操作系统、浏览器类型等信息)。
具体步骤:
1.获取原始的json字符串,使用flink中的算子flatmap来将初始的json字符串转化为添加了设备唯一标识uuid、跳转来源referance的jsonobj。
2.然后用keyBy算子来根据uuid对这些对象进行分组。
3.分组完之后,根据同一个uuid来进行处理,然后根据获取当前访问时间(初始数据中有访问时间的时间戳),如果之前有访问时间老访客,如果没有就是新访客,这里的时间维度为一天
存储到kafka的topic2
DWM层
ODS和DWD和业务关系不大,DWM、DWS和业务关系就大
需要得出短链访问的终端设备分布情况(浏览器类型分布)、(操作系统分布)、(设备类型分布)等,做出【宽表】
1.通过解析User-agent来获得设备信息
2.这里用了高德api来根据ip获得地理位置信息,获得宽表,将之前的信息全都放进来
存到topic3
接下来为数据统计pv和uv的,pv是点击量,同个用户刷新都会增加。uv统计的是日活跃用户。
1.对用户设备的唯一标识分组,利用keyby函数,将用户的设备唯一标识作为key,将所有设备标识相同的用户分为一组,然后进行去重操作。
2.flink中有无状态流和有状态流,就比如之前的map算子,每有一个用户点击短链,就生成相应的信息,这是无状态流。而这里的pv,uv,会根据每条输入进行更新,同一个用户一天内多次点击uv不会变,pv会增加。
3.对设备标识分组后的去重操作,用到了ValueState,key的状态。设置键状态的过期时间,这里统计天活跃用户,就把过期时间设为一天。所以把访问时间设置过期时间为一天,如果过期这个值就会被清理变成null,每次判断只要这个值不为空,且这次时间与之前的日期值相等,就不计入uv值,做到了去重。
存到topic4
PV(Page View):页面浏览量或点击量,用户每次刷新即被计算一次。
UV是统计日活的UV,只要访问过短链的就算日活跃用户,大体思路是1.需要知道用户的唯一ID2.需要知道访问时间3.如果是同一天访问的就可以去重
对用户设备的唯一标识分组,利用keyby函数,将用户的唯一标识作为key,将所有设备标识相同的用户分为一组,然后进行去重操作。
flink中有无状态流和有状态流,就比如之前的map算子,每有一个用户点击短链,就生成相应的信息,这是无状态流。而这里的pv,uv,会根据每条输入进行更新,同一个用户一天内多次点击uv不会变,pv会增加。
所以对设备标识分组后的去重操作,用到了ValueState,key的状态。Flink为每个key维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中。
具体实现是可以设置键状态的过期时间,这里统计天活跃用户,就把过期时间设为一天。所以把访问时间设置过期时间为一天,如果过期这个值就会被清理变成null,每次判断只要这个值不为空,且这次时间与之前的日期值相等,就不计入uv值,做到了去重。
DWS层
对DWM进行处理,多流合并,形成最后的宽表。
因为DWM层对数据进行了两次操作,第一次将DWD层的数据提取出了一些用户信息,以及user=agent里的浏览器,以及增加了地理位置,第二次又判断了是不是uv。所以这里就有两个数据流,便于下一步在DWS层继续处理。
新建一个另一个宽表的do类,这里是对DWM进行处理,形成进一步的主题宽表。
使用flink的union算子进行多流合并,数据会按照先入先出且不去重的方式合并。
1.dwm里的两个sink输出流存储在kafka中,可以直接获取这两个个数据流,然后再做数据对齐,因为DWM层设计的宽表的字段是没有pv和uv的,新增的宽表添加了一些新的字段,将已有的字段直接填入新的宽表,这两条数据流中,一条做pv统计,uv置为0,一条做uv统计,pv置为0.然后用union算子进行多流合并。
2.设置WaterMark Flink的WaterMark详解
3.设置多维度、多个字段分组,比如某个省的pv,uv,某个操作系统的pv,uv。还是用keyby来进行分组。
进行分组的时候把需要分组的字段存储在tuple元组里面,这时候可以根据元组里的字段来进行多维度分组。
(元组和列表list一样,都可能用于数据存储,包含多个数据;但是和列表不同的是:列表只能存储相同的数据类型,而元组不一样,它可以存储不同的数据类型,比如同时存储int、string、list等,并且可以根据需求无限扩展。)
6.然后用滚动窗口来对数据划分统计,这里开窗的方式用了tumbling,也就是无重叠数据的窗口,窗口时间设置为10秒,就是每隔10秒统计pv,uv数
7.聚合统计,通过flink的reduce算子,对滑动窗口中的数据(比如pv,uv)进行聚合累加,window算子之后的reduce,其实计算的是window窗口内的数据和,每次窗口触发的时候,才会输出一次结果。也就是每一个时间窗口(10秒)就把之前的pv,uv相加,就能得到实时的pv,uv了。
8.将所有字段输出保存到Clickhouse
3.ADS 层数据存储 ClickHouse,对接数据可视化服务多维度分析图表 sql 开发
从ClickHouse中读取数据,根据需求进行筛选聚合,可视化展示
根据web可视化报表统计需求,从ClickHouse聚合统计
简单来说就是编写sql语句从clickhouse中的宽表来进行筛选统计
1.分页实时查看访问记录
2.7天内的访问趋势图(pv,uv)
3.访问来源Top10统计开发
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)