核心业务开发架构,一切为了保证高可用。
framework-all的为了解决framework过于庞大,导致程序引入过多无效JAR包问题而来的,早期用framework的代码都应该切换到framework-all里的具体jar包来使用
新项目请直接使用framework-all, 老项目根据Framework-all接入这篇文档来迁移
下文中所有使用到framework的地方,都应该使用framework-all版本中的具体子包。
framework-enum也不再使用,使用framework-all里的framework-enums子包
(以前的****-model不再使用,有歧义)
全新的springcloud项目认真参照这个规范,并且咨询翟永超用脚手架建新项目
业务层接口应该使用Service后缀,实现使用ServiceImpl后缀。持久化层接口使用Dao后缀,实现使用DaoImpl后缀例如: PayServiceImpl实现PayService接口,PayDaoImpl实现PayDao接口
业务模块中禁止直接编写没有接口的业务实现类,工具类等例外。我们代码中含有大量这种情况,下图所示的直接针对实现编程是被禁止的:
如果枚举值不局限于一个项目使用,需要注意这些问题:
【强制】不允许直接拿 HashMap 与 HashTable 作为查询结果集的输出。
反例:某同学为避免写一个<resultMap>,直接使用 HashTable 来接收数据库返回结果,结果
出现日常是把 bigint 转成 Long 值,而线上由于数据库版本不一样,解析成 BigInteger,导
致线上问题。
【推荐】不要写一个大而全的数据更新接口,传入为 POJO 类,不管是不是自己的目标更新字 段,都进行 update table set c1=value1,c2=value2,c3=value3; 这是不对的。执行 SQL 时,尽量不要更新无改动的字段,一是易出错;二是效率低;三是 binlog 增加存储。(目前不少都是通过整行全量更新,比较容易出类似问题)
【参考】@Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要 考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo暂未提供分布式事务支持。
服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸
不建议使用过于抽象的通用接口,如:Map query(Map),这样的接口没有明确语义,会给后期维护带来不便。
除了字段。api url也都统一和字段一样命名,全小写无分隔符。按功能 业务分组path。
之所以有两个注释是因为我们序列化用的fastjson 的JSONField 来输出。但是swagger 不支持这个注释,所以只能加jackson的jsonproperty来 确保swagger显示正确。所以两个注释,缺一不可
@JsonProperty("incompletebagcodes")
@JSONField(name = "incompletebagcodes")
public List<String> getIncompleteBagCodes() {
return incompleteBagCodes;
}
注意: 如果是spring cloud提供rest api,序列化都是jackson的,不用fastjson了,不需要加@JSONField
注意必须使用swagger来注释API
@AccessRequired
@ApiOperation(value = "生成二维码", httpMethod = "GET", produces = "application/json", notes = "购物车同步接口,将客户端购物车传入,支持增量或全量模式,同时支持微信商城和App 2.0版本", tags = {"码"}) @ApiImplicitParams({ @ApiImplicitParam(value = "w", name = "宽度", dataType = "int", defaultValue = "150", paramType = "query"), @ApiImplicitParam(value = "h", name = "高度", dataType = "int", defaultValue = "150", paramType = "query"), @ApiImplicitParam(value = "url", name = "url to encode", dataType = "string", required = true, paramType = "query"), }) @GetMapping(value = "/qrcode")
不允许只写
@RequestMapping(value = "/sync") 这样会支持所有的http action.
@ApiModelProperty(value = "pet status in the store", allowableValues = "available,pending,sold", required=true)
ValueDescription ValueDescription2 ValueDescription3
代码内部用 joda.time.DateTime 处理时间计算逻辑,不要使用jdk内置 Calendar 类,Calendar 是个设计很糟糕的非线程安全可变的类,很难用
原因如下:
Provider上尽量多配置Consumer端的属性,让Provider实现者一开始就思考Provider服务特点、服务质量的问题。
示例:
超时时间默认为5秒钟,有特殊要求的可以15秒,spring cloud超时也按照5秒设置
服务端默认全局强制必须手工将重试次数设为 0(否则不设DUBBO默认为2)。
<dubbo:provider retries="0" timeout="5000"/>
如果某些方法是只读的,或者支持可重入的更新操作,在方法级别设置重试 2次(除非百分之百肯定整个服务接口都是只读的,那就直接设置服务级别)
<dubbo:service interface="com.xx.XxxService">
<dubbo:method name="findXxx" retries="2" />
</dubbo:service>
调用端都不配置retry,超时属性
在Provider可以配置的Consumer端属性有:
<
dubbo:protocol
threads
=
"200"
/>
<
dubbo:service
interface
=
"com.alibaba.hello.api.HelloService"
version
=
"1.0.0"
ref
=
"helloService"
executes
=
"200"
>
<
dubbo:method
name
=
"findAllPerson"
executes
=
"50"
/>
</
dubbo:service
>
|
Provider上可以配置的Provider端属性有:
<dubbo:application name="makeup-api-server" owner="ding.lid,william.liangf"/> <!-- owner有问题时便于的找到服务的负责人,至少写两个人以便备份。dubbo:service、dubbo:reference没有配置负责人,则使用dubbo:application设置的负责人。 --> <dubbo:provider retries="0" timeout="5000"/> <!-- 使用zookeeper注册中心暴露服务地址 --> <dubbo:registry protocol="zookeeper" address="${dubbo.registry.address}" id="zookeeper"/> <!-- 用dubbo协议在20800端口暴露服务 --> <dubbo:protocol name="dubbo" port="30300" dispather="all" threadpool="cached" threads="5000"/> <dubbo:service interface="com.yonghui.makeup.service.api.DemoMakeupService" ref="demoMakeupService" version="1.0" registry="zookeeper" owner="shp" />
Dubbo间调用是有网络延迟的,因此应该尽可能提供批量接口,同时限制批量大小
接口方法应该考虑幂等性,在重复调用时,不会影响最终结果
目前dubbo服务的可重入性还没有统一框架保证,在framework里有个 UniqueOperationContext ,用来提供唯一id,和操作人的。如果没有更好办法前,建议所有数据更改接口操作都加上这个公共参数。(如果有人提供更好的想法和框架,请联系林凯)
dubbo服务不要设计为返回ServiceResponse<T> 这种。特别是不要将所有异常捕获,然后封装成一个带错误码的ServiceResponse,将未处理的异常封装处理是有害的,会大大增加查找bug的难度,使系统陷入非正常状态。虽然返回了错误码,但调用方在大部分情况下都不清楚如何处理,而且非常容易遗漏。
简单的做法就是只catch 100%知道如何处理的异常,其他的异常不处理不catch,丢给下层调用者。可以参考本文异常里的描述
命名原则是为了帮助他人从命名上看出方法/字段的功能,因此需要慎重取名,必要时添加注释
加入业务名称是XXX (例如trade)
git项目名称:
应该尽量使用名词来定义类名
枚举类
类名带上Enum
后缀
枚举成员名称需要全大写,单词间用下划线隔开
?个人不建议统一实现com.yonghui.common.util.ValueDescription3
, 增加耦合且毫无意义
统一放在各模块的enums
目录下
工具类
类名带上Util
后缀
统一放在各模块的utils
目录下
异常类
类名带上Exception
后缀
统一放在各模块的exceptions
目录下
数据对象
即映射到数据库的对象
数据对象类名=表名(驼峰写法)+DO(固定后缀)
以DO
为后缀,例如pageDO
数据传输对象
即Dubbo中调用时传递的java对象
数据传输对象=业务领域相关的名称+DTO(固定后缀)
以DTO
为后缀, 例如shopDTO
展示对象
例如Ajax调用时返回给前端JSP页面的对象
展示对象=网页名称+VO(固定后缀)
以VO
为后缀,例如goodsVO
返回给APP的响应为 WebResult<T>
【强制】抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命 名以它要测试的类的名称开始,以 Test 结尾。
【强制】POJO 类中的任何布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。
反例:定义为基本数据类型 boolean isSuccess;的属性,它的方法也是 isSuccess(),RPC框架在反向解析的时候,“以为”对应的属性名称是 success,导致属性获取不到,进而抛出
异常。
【强制】long 或者 Long 初始赋值时,必须使用大写的 L,不能是小写的 l,小写容易跟数字 1 混淆,造成误解。
说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2?
所有定义的包名开头都是com.yonghui
【强制】long 或者 Long 初始赋值时,必须使用大写的 L,不能是小写的 l,小写容易跟数字 1 混淆,造成误解。
说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2?
【推荐】不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护。如:缓存 相关的常量放在类:CacheConsts 下;系统配置相关的常量放在类:ConfigConsts 下。
说明:大而全的常量类,非得 ctrl+f 才定位到修改的常量,不利于理解,也不利于维护
应该尽量使用动词来定义方法名
场合 | 前缀 | 举例 |
---|---|---|
获取单个对象 | get | getShop() |
获取多个对象 | list | listShop() |
获取统计值 | count | countShop() |
插入 | save | saveShop() |
删除 | remove | removeShop() |
修改 | update | updateShop() |
【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。
说明:对于 Integer var=?在-128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
【强制】所有的覆写方法,必须加@Override 注解。 反例:getObject()与 get0bject()的问题。一个是字母的 O,一个是数字的 0,加@Override可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。
【强制】Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals。
正例: "test".equals(object);
反例: object.equals("test");
说明:推荐使用 java.util.Objects#equals (JDK7 引入的工具类)
【强制】关于基本数据类型与包装数据类型的使用标准如下: 1) 所有的 POJO 类属性必须使用包装数据类型。
2) RPC 方法的返回值和参数必须使用包装数据类型。
3) 所有的局部变量推荐使用基本数据类型。
说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE 问题,或者入库检查,都由使用者来保证。
正例:数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
反例:某业务的交易报表上显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的RPC 服务,调用不成功时,返回的是默认值,页面显示:0%,这是不合理的,应该显示成中划线-。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
.【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。 新增序列化类必须添加serialVersionUID属性。
说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。
【强制】POJO 类必须写 toString 方法。使用工具类 source> generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。
说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。
【推荐】使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内 容的检查,否则会有抛 IndexOutOfBoundsException 的风险。
说明:
String str = "a,b,c,,"; String[] ary = str.split(","); //预期大于 3,结果是 3 System.out.println(ary.length);
job-scheduler定时执行的任务,实际只是一个动作触发。实际代码执行依然在center中。而这个代码是要获取所有待处理的订单一条条进行处理。整个过程在早晨来单后,非常容易堆积。几百个订单根本无法在dubbo默认15秒的调用时间内处理完。导致job超时失败。剩下的订单无法处理。在job再次执行后,又因查询条件设置的问题,job浪费了很多时间处理不需要处理的订单、剩余的时间不够处理真正需要处理的单子。进一步引发订单延时。
此问题导致早上社区合伙人无法通过app接单,影响配送时效,门店反映强烈!
从中总结出的希望大家遵守的的一个设计原则。有不同意见的大家交流
job-scheduler 不只是调度器,我看到好多人还是把job-scheduler的代码做的很轻,中间只有简单的调用center的服务接口。其实这不是一种好的设计方式。job-scheduler 中的job其实和你的center是属于同一个业务系统。不需要所有代码都放入center执行。
job-scheduler 本身就是设计为分布式的可分片的处理需要定时执行的任务,job-scheduler的CPU就应该用于任何和定时任务本身有关的代码和业务逻辑,这时候center应该只是提供部分业务接口和数据接口,不应该参与定时批量运行的逻辑。
因为定时任务在center上运行更加消耗center资源,不利于center轻量级,隔离不够。
更大的问题在于:在job中调度center服务,本身设个dubbo同步调用。而大部分定时任务的执行时间都是很长的,很可能无法在指定时间内完成,有些速度快的任务也可能因为数据量变大而延长时间。
这时候job很容易因为rpc超时产生异常。今天管家的问题就是如此引发。
假如把调度做成异步的,虽然可以解决上述超时问题,但可能会导致job下一次执行的时候,上一次job的执行还在center中继续进行。如果程序设计不好,很可能重复执行产生问题。
除了上面几点。作为调度器之后,就无法利用job本身的分片功能,在job多台实例上均衡job负载。
以上这些问题,希望大家在设计job的时候推荐上述标准用法。如果有功能也业务确有需要只将job作为调度器的,要考虑清楚,应该有完善的设计方案应对执行时间长、负载不均衡、重复执行等问题。
【强制】Map/Set 的 key 为自定义对象时,必须重写 hashCode 和 equals。 正例:String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用
【强制】ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList ;
说明:subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是 ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。
【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全 一样的数组,大小就是 list.size()。
反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误。
正例:
List<String> list = new ArrayList<String>(2); list.add("guan"); list.add("bao"); String[] array = new String[list.size()]; array = list.toArray(array);
说明:使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组地址;如果数组元素大于实际所需,下标为[ list.size() ]的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。
【强制】使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { "a", "b" };
List list = Arrays.asList(str);
第一种情况:list.add("c"); 运行时异常。
第二种情况:str[0]= "gujin"; 那么 list.get(0)也会随之修改。
【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法。
说明:苹果装箱后返回一个<? extends Fruits>对象,此对象就不能往里加任何水果,包括苹果。
【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
反例:
List<String> a = new ArrayList<String>(); a.add("1"); a.add("2"); for (String temp : a) { if("1".equals(temp)){ a.remove(temp); } }
说明:这个例子的执行结果会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
正例:
Iterator<String> it = a.iterator(); while(it.hasNext()){ String temp = it.next(); if(删除元素的条件){ it.remove(); } }
【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key
所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更
高。如果是 JDK8,使用 Map.foreach 方法。
正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是
一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。
【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
反例:很多同学认为 ConcurrentHashMap 是可以置入 null 值。在批量翻译场景中,子线程分发时,出现置入 null 值的情况,但主线程没有捕获到此异常,导致排查困难。
【参考】合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
说明:稳定性指集合每次遍历的元素次序是一定的。有序性是指遍历的结果是按某种比较规则依次排列的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是order/sort。
NumberUtils、DateFormatUtils、DateUtils 等优先使用org.apache.commons.lang3 这个包下的,不要使用 org.apache.commons.lang 包下面的。原因是 commons.lang 这个包是从 JDK1.2 开始支持的所以很多 1.5/1.6 的特性是不支持的,例如:泛型。
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; 说明:如果是 JDK8 的应用,可以使用 instant 代替 Date,Localdatetime 代替 Calendar,
Datetimeformatter 代替 Simpledateformatter,官方给出的解释:simple beautiful strong
immutable thread-safe。
【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造 成死锁。
说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 各个方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。 如何正确地使用ThreadPoolExecutor,请必读ThreadPoolExecutor
【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法可以执行,避免主线程无法执行至countDown 方法,直到超时才返回结果。
说明:注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。
【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。
说明:Random 实例包括 java.util.Random 的实例或者 Math.random()实例。
正例:在 JDK7 之后,可以直接使用 API ThreadLocalRandom,在 JDK7 之前,可以做到每个线程一个实例。
【参考】volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果想取回 count++数据,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); count++操作如果是JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)
【强制】在一个 switch 块内,每个 case 要么通过 break/return 来终止,要么注释说明程序 将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、 获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。
JAVADOC 文档地址How to Write Doc Comments for the Javadoc ToolRequirements for Writing Java API Specifications
【强制】类、类属性、类方法的注释必须使用 javadoc 规范,使用/**内容*/格式,不得使用 //xxx 方式。
说明:在 IDE 编辑窗口中,javadoc 方式会提示相关注释,生成 javadoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
【强制】所有的抽象方法(包括接口中的方法)必须要用 javadoc 注释、除了返回值、参数、
异常说明外,还必须指出该方法做什么事情,实现什么功能。
说明:如有实现和调用注意事项,请一并说明。
说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。
反例:
// put elephant into fridge put(elephant, fridge); 方法名 put,加上两个有意义的变量名 elephant 和 fridge,已经说明了这是在干什么,语义清晰的代码不需要额外的注释。
1) 待办事宜(TODO):( 标记人,标记时间,[预计处理时间])
表示需要实现,但目前还未实现的功能。这实际上是一个 javadoc 的标签,目前的javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个 javadoc标签)。
2) 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])
在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。
可以实现 后端统一使用下列配置服务器对所有可变配置进行管理,此配置灵活度高,而且没有任何性能问题,配置变化会立即变更客户端,没有延时。配置服务器不可用时,依然不影响应用获取本地缓存的值。详情请看:配置服务器
请确保config没有配置的时候,不要影响程序启动,需要有默认值配置,或者处理没有配置的情况。
目前订单,管家,包裹等一些状态变化逻辑多的项目已经使用该框架,李琦提供了一个文档,例子可以查看bbc-center(权限已经全部开放)
可以到framework 的com.yonghui.common.statemachine包下查看具体代码,如果要使用,可以咨询(林凯,宁夏)该框架脱胎于订单处理中心(makeup-service), 后抽象成框架到framework后应用于管家,包裹,售后单,实现了以下功能
注意订阅者推荐实现可重入的服务,这样在订阅的时候选择可重入模式,在一定时间内会自动出错重试,真正实现自动化补偿机制。在部署的时候注意选择正确的按钮。请做成可重入,新增不可重入的,请和我说明下,否则我会assign bug给你
可以通过简单的maven命令做到更新数据库版本
在****-api-server的pom文件里,确认有
<build> <plugin> <groupId>org.flywaydb</groupId> <artifactId>flyway-maven-plugin</artifactId> </plugin> </build> <profile> <id>dev</id> <properties> <env>dev</env> <flyway.mybatis.jdbc.url>jdbc:mysql://127.0.0.1:3306/order_db?autoReconnect=true&useUnicode=true&zeroDateTimeBehavior=convertToNull</flyway.mybatis.jdbc.url> <flyway.mybatis.jdbc.username>yhhy</flyway.mybatis.jdbc.username> <flyway.mybatis.jdbc.password>yhhy</flyway.mybatis.jdbc.password> </properties> <activation> <activeByDefault>true</activeByDefault> </activation> </profile>
这样可以通过
点击flyway migrate进行升级。这些应该都在脚手架里,必须设置正确,这样docker本地开发测试平台可以正确升级db。
@Component public class SellerDisplayCategoryCacheManager extends GenericCacheManager<SellerDisplayCategory, Long> { @Autowired private SellerDisplayCategoryMapper sellerDisplayCategoryMapper; public SellerDisplayCategoryCacheManager() { this.cacheExpiry = 5; } @Override protected SellerDisplayCategory createObject(Long id) { return new SellerDisplayCategory(); } @Override protected SellerDisplayCategory getObject(Long id) { return sellerDisplayCategoryMapper.selectByPrimaryKey(id); } }
使用SXSSFWorkbook api (设置introwAccessWindowSize,即每次在内存中保持的行数,当达到这个值的时候,那么会把这些数据flush到磁盘上);在测试环境成功导出90万+数据,几乎达到了EXCEL的瓶颈。
说明:不要在方法体内定义:Pattern pattern = Pattern.compile(规则);
说明:Apache BeanUtils 性能较差,可以使用其他方案比如 Spring BeanUtils, Cglib BeanCopier。参考Bean复制的几种框架性能比较(Apache BeanUtils、PropertyUtils,Spring BeanUtils,Cglib BeanCopier)
【强制】注意 Math.random() 这个方法返回是 double 类型,注意取值范围 0≤x<1(能够取 到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。
【强制】获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime();
说明:如果想获取更加精确的纳秒级时间值,用 System.nanoTime。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。
System.currentTimeMillis() is obviously the most efficient since it does not even create an object, but new Date() is really just a thin wrapper about a long, so it is not far behind. Calendar, on the other hand, is relatively slow and very complex, since it has to deal with the considerably complexity and all the oddities that are inherent to dates and times (leap years, daylight savings, timezones, etc.).
It's generally a good idea to deal only with long timestamps or Date objects within your application, and only use Calendar when you actually need to perform date/time calculations, or to format dates for displaying them to the user. If you have to do a lot of this, using Joda Time is probably a good idea, for the cleaner interface and better performance.
【推荐】任何数据结构的使用都应限制大小。
说明:这点很难完全做到,但很多次的故障都是因为数据结构自增长,结果造成内存被吃光。
【推荐】对于“明确停止使用的代码和配置”,如方法、变量、类、配置文件、动态配置属性 等要坚决从程序中清理出去,避免造成过多垃圾。清理这类垃圾代码是技术气场,不要有这样的观念:“不做不错,多做多错”。
【强制】不要捕获 Java 类库中定义的继承自 RuntimeException 的运行时异常类,如:IndexOutOfBoundsException / NullPointerException,这类异常由程序员预检查来规避,保证程序健壮性。
正例:if(obj != null) {...}
反例:try { obj.method() } catch(NullPointerException e){…}
【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
说明:如果 JDK7,可以使用 try-with-resources 方法。
【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为包装数据类型,有可能是 null,返回 int 值时注意判空。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象,一律要求进行 NPE 判断。
5) 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
com.yonghui.common.constants.Constant中引入新的error code, error message.
然后抛出
throw new BaseKnownException(Constant.ERRORCODE_ORDER_MUST_PICKUP_BY_PARTNER, Constant.ERRORMESSAGE_ORDER_MUST_PICKUP_BY_PARTNER);
如果这个Exception包含其他辅助信息,可以通过自定义Exception来做到(这个Exception要定义在Framework,否则其他进程无法识别)
public class IdGenerationNotConfigException extends BaseUnknownException { public IdGenerationNotConfigException(String code) { super(Constant.ERRORCODE_INTERNAL_SERVER_ERROR, Constant.ERRORMESSAGE_INTERNAL_ERROR, null, null, Constant.HTTPCODE_INTERNAL_SERVER_ERROR, Constant.ERRORCODE_INTERNAL_ID_GENERATION_NOT_CONFIG, String.format(Constant.ERRORMESSAGE_INTERNAL_ID_GENERATION_NOT_CONFIG, code), null); } }
新系统推荐使用framework-all里的
【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger logger = LoggerFactory.getLogger(Abc.class);
如果是需要以后需要查看的异常,加入exception log,这个日志文件中的内容会在elk出现。如何引入exception log框架
应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType:日志类型,推荐分类有 stats/desc/monitor/visit等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
正例:mppserver 应用中单独监控时区转换异常,如: mppserver_monitor_timeZoneConvert.log
说明:推荐对日志进行分类,错误日志和业务日志尽量分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。注意日志输出的级别,error 级别只记录系统逻辑出错、异常、或者重要的错误信息。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使 用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。纪录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
【参考】如果日志用英文描述不清楚,推荐使用中文注释。对于中文 UTF-8 的日志,在 secureCRT中,set encoding=utf-8;如果中文字符还乱码,请设置:全局>默认的会话设置>外观>字体>选择字符集 gb2312;如果还不行,执行命令:set termencoding=gbk,并且直接使用中文来进行检索。
禁止打印无意义的日志,干扰正常的日志。
能够合并成一条日志的,尽量合并成一条、
日志建议添加前缀来描述功能模块
测试时应该同时查看tesin的日志,防止未发现的错误
不应在代码中直接使用常量值, 而应将值赋给有意义的变量
反例: if(sellerId == 1)
正例:
// 次日达商城 int SELLER_CRD = 1; ... if(sellerId == SELLER_CRD)
空指向异常
编写代码时,应该防范空指向异常,不能寄希望于"约定"上级方法不返回null
使用"test".equals(object)
而不是 object.equals("test")
如果条件判断的语句过多,应该抽成一个方法。例如:
// 反例 if(条件1 && 条件2 && 条件3 && 条件4) // 正例 if( isXXConditionOk() ){...} private boolean isXXConditionOk(){ return 条件1 && 条件2 && 条件3 && 条件4; }
禁止复制粘贴重复代码,对于大量重复的代码应该抽取出公共方法
不用使用config.properties,尽量使用config server。目前config.properties里面都属性都已经由运维单独配置,很容易漏掉。
需要控制一行的长度,防止一行中出现过长的代码影响阅读
方法体的长度不能过长,否则应该拆成若干个小方法。需遵循“开放闭合原则”
必需添加必要的中文注释,不要卖弄英文注释
新接口替换旧接口时应该遵循一下原则
让林凯在群内 @All, 告知所有人旧接口的替换时间,旧接口支持截止时间
旧接口不应该被立即删除,应该添加@Deprecated注解,对于影响较大的旧接口,需要保留至少一周,以便相关开发人员有充足的切换并充分测试。
新编写的代码中,不能使用@Deprecated注释的过时方法
DUBBO接口不能直接修改参数,否则会导致上线时候服务调用失败,应该增加新接口,等稳定确保老接口无人调用后,再删除。
【强制】表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint ( 1 表示是,0 表示否),此规则同样适用于 odps 建表。
说明:任何字段如果为非负数,必须是 unsigned。
正例:getter_admin,task_config,level3_name
反例:GetterAdmin,taskConfig,level_3_name
【强制】表名不使用复数名词。
说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。
【强制】小数类型为 decimal,禁止使用 float 和 double。
说明:float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
【强制】varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 TEXT,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL,
created_by VARCHAR(64),
updated_at DATETIME(6) NULL,
updated_by VARCHAR(64) COMMENT '最后更新人',
last_updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)
【强制】表必备三字段:id, last_updated_at。 说明:其中 id 必为主键,类型为 unsigned bigint、单表时自增、步长为 1;分表时改为从id service取值,确保分表之间的全局唯一。last_updated_at 的类型均为datetime(6)类型。(注意Mybatis自动生成的model不能带上这个last_updated_at的字段,否则代码会覆盖mysql的自动设置)
`last_updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '最后更新时间',
.【推荐】字段允许适当冗余,以提高性能,但是必须考虑数据同步的情况。冗余字段应遵循:
1)不是频繁修改的字段。
2)不是 varchar 超长字段,更不能是 text 字段。
正例:各业务线经常冗余存储商品名称,避免查询时需要调用 IC 服务获取。
【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
反例:某业务三年总数据量才 2 万行,却分成 1024 张表,问:你为什么这么设计?答:分 1024张表,不是标配吗?
【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索
速度。
正例:人的年龄用 unsigned tinyint(表示范围 0-255,人的寿命不会超过 255 岁);海龟就必须是 smallint,但如果是太阳的年龄,就必须是 int;如果是所有恒星的年龄都加起来,那么就必须使用 bigint。
凡有sql 改动和schema change的请在CR里CCreviewer list带上 DBA 李俊杰,否则将无法merge!!!
如何选择正确的数据源来调用sql
【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验和控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
【强制】超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时, 保证被关联的字段需要有索引。
说明:即使双表 join 也要注意表索引、SQL 性能。
【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。mysql 索引长度和区分度
--此处展示的语句用于创建一个索引,索引使用列名称的前10个字符。 CREATE INDEX part_of_name ON customer (name(10));
【推荐】利用覆盖索引来进行查询操作,来避免回表操作。 理解MySQL数据库覆盖索引
说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。
正例:IDB 能够建立索引的种类:主键索引、唯一索引、普通索引,而覆盖索引是一种查询的一种效果,用 explain 的结果,extra 列会出现:using index.
【推荐】利用延迟关联或者子查询优化超多分页场景。
说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
正例:先快速定位需要获取的 id 段,然后再关联: SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
【推荐】 SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好。
说明:
1)consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
2)ref 指的是使用普通的索引。(normal index)
3)range 对索引进范围检索。
反例:explain 表的结果,type=index,索引物理文件全扫描,速度非常慢,这个 index 级别比较 range 还低,与全表扫描是小巫见大巫。
【强制】不要使用 count(列名)或 count(常量)来替代 count(*),count(*)就是 SQL92 定义的 标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
【强制】当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题。
正例:可以使用如下方式来避免 sum 的 NPE 问题:SELECT IF(ISNULL(SUM(g)),0,SUM(g)) FROM table;
【强制】使用 ISNULL()来判断是否为 NULL 值。注意:NULL 与任何值的直接比较都为 NULL。
说明:
1) NULL<>NULL 的返回结果是 NULL,不是 false。
2) NULL=NULL 的返回结果是 NULL,不是 true。
3) NULL<>1 的返回结果是 NULL,而不是 true
【强制】不得使用外键与级联,一切外键概念必须在应用层解决。 说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。
如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,则为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
like '%XXX'
不会使用索引,like 'XXX%'
会使用索引。如果需要使用更强功能的模糊查询,应考虑ElasticSearch所有的select 里的column都要指定xxx.尤其有JOIN的,否则容易出现,新增了列,造成列名混乱。### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'remark' in field list is ambiguous ; SQL []; Column 'remark' in field list is ambiguous; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'remark' in field list is ambiguous
更新的sql schema一定要可以先跑。要兼容上层代码,比如新增列,一定要可以为空。这样老的sql插入不会错
【强制】POJO 类的 boolean 属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射。
说明:参见定义 POJO 类以及数据库字段定义规定,在 sql.xml 增加映射,是必须的
【强制】iBATIS 自带的 queryForList(String statementName,int start,int size)不推荐使 用。
说明:其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList取 start,size 的子集合,线上因为这个原因曾经出现过 OOM。
正例:在 sqlmap.xml 中引入 #start#, #size#
Map<String, Object> map = new HashMap<String, Object>(); map.put("start", start); map.put("size", size);
说明:操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服务器端会因为处于 time_wait 的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值。
正例:在 linux 服务器上请通过变更/etc/sysctl.conf 文件去修改该缺省值(秒): net.ipv4.tcp_fin_timeout = 30
说明:主流操作系统的设计是将 TCP/UDP 连接采用与文件一样的方式去管理,即一个连接对应于一个 fd。主流的 linux 服务器默认所支持最大 fd 数量为 1024,当并发连接数很大时很容易因为 fd 不足而出现“open too many files”错误,导致新的连接无法建立。 建议将 linux服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关)。
说明:OOM 的发生是有概率的,甚至有规律地相隔数月才出现一例,出现时的现场信息对查错非常有价值。
【强制】用户请求传入的任何参数必须做有效性验证。 说明:忽略参数校验可能导致:
page size 过大导致内存溢出
恶意 order by 导致数据库慢查询
正则输入源串拒绝服务 ReDOS
任意重定向
SQL 注入
Shell 注入
反序列化注入
【强制】在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制, 如数量限制、疲劳度控制、验证码校验,避免被滥刷、资损。
说明:如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。
自己负责的模块合并到develop分支后,应及时在testin环境中编译一遍,防止被人耗费大量时间在解决b2c-platform,api-rest编译报错的问题。
testin应该以develop分支为主,由于个人需要切分支的,应该在群里周知。用完后要及时改回develop
自己拉的分支建议增加姓名,例如feature/skusuggestion-luyunfei
PRD是给开发看的,开发人员应该对PRD进行把关,提高接收PRD的门槛。 来避免有问题甚至是未经思考的需求
产品PRD应该以图片为主,文字为辅。流程性的东西应该画流程图
对于连描述语句都不通顺的PRD,应该敦促产品进行修改
经常收到很多未经产品充分考虑的功能需求,甚至连产品自己都不太了解。对于"一周产品会议,仅用一小时编写的需求文档",应该慎重评估需求的可行性
产品内部的需求有时候是有冲突。
请认真阅读本章,这都是以前发生过的教训。 RCA-业务开发