自我介绍

我叫余瑜。来自于河南省南阳市。干java这行已经将近四年了,大学刚毕业的时候在郑州工作,后来转战到了北京,

介绍下最近的项目

我现在所在的是埃森哲项目组,这个项目属于一个和埃森哲的顾问一同开发的自研项目。
项目主要包括三个子系统:主数据平台、销售平台、结算平台;

  1. 主数据平台维护数据配置信息
  2. 销售平台对接erp系统,仓储系统等外部系统
  3. 结算平台生成最终结算信息

我所在的是销售平台,项目使用springboot搭建,主要分为四个模块, mall模块,middle模块,rule模块,tran模块。每个模块都独立出一个rpc包,方便使用dubbo进行调度。

mall

mall模块主要负责对接外部系统,比如erp系统,仓储系统,国际站等。外部系统与mall之间使用rocketmq做通信,这样可以有效的解耦,并且降低了并发。

消息对接及工厂模式

因为各个系统发送过来的消息种类有很多, 比如下单,发货,拒签,而这些消息的topic都不同,触发的各种业务逻辑也不同,而又有一些消息它们一次可以触发多种业务,比如单据签收,会触发商品的库存,权属,调货三种业务,而这些业务又和其他的业务有重合,所以在这个位置使用了抽象工厂模式。
首先,将这些topic、需要触发的类名称、描述信息保存在一个枚举种,因为有些topic是可以触发多个业务操作的,所以在类名称位置使用的是数组来保存。
然后创建一个抽象类,各个具体实现的类都集成这个抽象类,并且将共有的操作提取到了抽象类中
在消费消息时,依据topic在枚举中获取到这个topic具体需要执行的类的名称数组,然后遍历这个数组执行消息,而因为自己new出来的对象是无法使用spring的依赖注入的,我们在将这些类交给spring管理后,写了一个工具类实现了spring的ApplicationContextAware接口,依据类的名称获取对象来消费消息。

middle

middle模块主要负责中台的业务逻辑处理以及上云操作,mall模块在订单签收,或者关单导入后如果它们的销售方主体不等于采购主体,并且不是服务类商品的话会集成至内部调货模块进行调货操作,而同一条单据传送内部调货节点可能有多个,并且清关传送进来的单据又需要最后签收的消息来触发签收操作,再加上并发原因,甚至可能出现同一秒传送过来两条甚至三条,这样就会造成数据重复。

幂等

而在数据传递到内部调货模块时,如果不是代转采直发那么会以条码级维度传过来,数量为一,如果是代转采直发那么没有条码,数量可能是多个。但是,订单号+商品id+条码 一定是一个唯一值,我就在数据库中做了唯一索引。所以在接收数据时先查询一次数据库看数据库中是否有值,如果有值,判断签收,如果,传送过来的数据时签收状态,而数据库中是未签收,那么触发签收逻辑。其他情况则不插入,如果数据库中没有值就插入,然后在插入的sql语句中使用insert ignore into做了幂等保证数据的唯一性。

拆分及匹配

当数据进入拆分表后会自动进行拆分,单据主要包括跨境的单据,非跨境跨主体的单据以及跨境非跨主体的单据,
非跨主体的单据是不集成内部调货的,而非跨境的单据进行拆分时它们就是直接进行调货从主体a到主体B,跨境的单据因为需要清关,一旦需要清关,那么金额信息就需要从关单来获取。所以我们把它拆分成了多段, 比如:采购主体是意大利销售主体是上海,那么就需要拆分为三段,意大利到香港,香港到库天下,库天下到上海。每一段都有对应的加价规则,而香港到库天下这一段则是需要等待从erp进行关单审核后传送过来关单信息后从关单中获取到价格。单据在进行拆分后如果不是冲销单据那么会进行自动匹配,匹配后会自动传送:内往,结算,库存,gl日记账。由于需要保证传送过去的单据在同一个期间内,而库存又有云端的验证,商品只有从意大利到达香港并且传送至库存后才可以继续下一段的香港到库天下,所以我们的批次匹配必须保证第一段匹配后才可以匹配第二段。所以在拆分后设定第一段的批次匹配标识为1可匹配,后面的分段设置为不可匹配,第一段匹配后才可以匹配下一段。这些程序逻辑在测试的时候是完全没有问题的,但是在实际上开始跑数据的时候发现一个很有意思的问题匹配后入账时金额经常计算错误,而程序逻辑有没有问题,而且错误的金额总是比正确的小。经过排查发现这个是因为我们使用了sharding-jdbc做了读写分离导致的。

读写分离并发读取错误

mysql中有一个binlog文件,每当执行一个修改或者保存的语句,都会向binlog中存放一份记录,而从库中有一个线程负责扫描这个日志,然后保存到从库的relaylog中,同时有一个线程会扫描这个log文件,解析并同步到从库中。
在高并发情况下,数据还没有从主库同步到从库,这时又进行了读取将会读取到旧的数据,最终导致这次的金额错误。经过查阅资料我们采用强制读取主库方法,但是在设置了强制读取主库后,发现金额值还是错误的。经过多次测试发现开启强制读取主库功能后需要强制读取主库的代码块必须放在同一个事物中,一旦跨越了这个事物将会失效。

锁机制

volatile和synchronized的区别

线程的通信有两种机制

  1. 共享内存 - 隐试通信
  2. 消息传递 - 显示通信

每一个线程在线程内都有一个线程内部的缓存,缓存内部的有一个变量a=1,而主内存中也有一个变量a=1,
如果这个变量是使用volatile修饰的,那么如果线程a中对变量a进行了修改,jvm会向处理器发送一条look前缀的指令,会把这个变量所在缓存行中的数据刷回到主内存,在多个处理器的情况下保证处理器一致性的特点。当其他处理器嗅探到主内存中的变量值修改了,就会从主内存中把变量的值刷到当前内存中。

synchronize:可重入性,互斥性,可见性
volatile:原子性,可见性,不能保证可重入性

lock和synchronized的区别

lock是在java5之后出现的:在java.util.concurrent.locks中

  1. synchronize的锁什么时候释放
    1. 获取锁的线程执行完了该代码块
    2. 线程执行出现了异常
    3. 线程阻塞后
  2. synchronize的权限
    1. lock可以主动去释放锁
    2. synchronize是被动

      Rocketmq原理

      mq的优势

  • 削峰填谷(主要解决瞬时写压力大于应用服务能力导致消息丢失、系统奔溃等问题)
  • 系统解耦(解决不同重要程度、不同能力级别系统之间依赖导致一死全死)
  • 提升性能(当存在一对多调用时,可以发一条消息给消息系统,让消息系统通知相关系统)
  • 蓄流压测(线上有些链路不好压测,可以通过堆积一定量消息再放开来压测)

    rocketmq的优势

  • 支持事务型消息(消息发送和DB操作保持两方的最终一致性,rabbitmq和kafka不支持)

  • 支持结合rocketmq的多个系统之间数据最终一致性(多方事务,二方事务是前提)
  • 支持18个级别的延迟消息(rabbitmq和kafka不支持)
  • 支持指定次数和时间间隔的失败消息重发(kafka不支持,rabbitmq需要手动确认)
  • 支持consumer端tag过滤,减少不必要的网络传输(rabbitmq和kafka不支持)
  • 支持重复消费(rabbitmq不支持,kafka支持)

    原理

    1) Name Server

    就是一个注册中心,存储当前集群所有Brokers信息、Topic跟Broker的对应关系。

  • Namesrv用于存储Topic、Broker关系信息,功能简单,稳定性高。多个Namesrv之间相互没有通信,单台Namesrv宕机不影响其他Namesrv与集群;即使整个Namesrv集群宕机,已经正常工作的Producer,Consumer,Broker仍然能正常工作,但新起的Producer, Consumer,Broker就无法工作。

  • Namesrv压力不会太大,平时主要开销是在维持心跳和提供Topic-Broker的关系数据。但有一点需要注意,Broker向Namesr发心跳时,会带上当前自己所负责的所有Topic信息,如果Topic个数太多(万级别),会导致一次心跳中,就Topic的数据就几十M,网络情况差的话,网络传输失败,心跳失败,导致Namesrv误认为Broker心跳失败。

    2) Broker

    Broker相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的Broker Name,不同的Broker Id来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。

    3) Producer

    Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
    Producer每隔30s(由ClientConfig的pollNameServerInterval)从Name server获取所有topic队列的最新情况,这意味着如果Broker不可用,Producer最多30s能够感知,在此期间内发往Broker的所有消息都会失败。
    Producer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s中扫描所有存活的连接,如果Broker在2分钟内没有收到心跳数据,则关闭与Producer的连接。

    4) Consumer

    Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
    Consumer每隔30s从Name server获取topic的最新队列情况,这意味着Broker不可用时,Consumer最多最需要30s才能感知。
    Consumer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s扫描所有存活的连接,若某个连接2分钟内没有发送心跳数据,则关闭连接;并向该Consumer Group的所有Consumer发出通知,Group内的Consumer重新分配队列,然后继续消费。
    当Consumer得到master宕机通知后,转向slave消费,slave不能保证master的消息100%都同步过来了,因此会有少量的消息丢失。但是一旦master恢复,未同步过去的消息会被最终消费掉。

    dubbo+zookeeper

  1. dubbo直连:在配置上加上url
  2. dubbo问题:dubbo传输数据过大报异常

    1. 设置dubbo传输大小
    2. 分页传送

      redis

      五种数据类型

  3. string

  4. list
  5. set
  6. zset
  7. hash

    持久化

    两种:RDP(redis data base)和 AOF(append-only file)