Effective Java:
第1条:用静态工厂方法代替构造器
示例:将boolean基本类型转换成了一个Boolean对象引用:
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
这里的静态工厂不同于设计模式的工厂方法
静态工厂方法对比构造器的优缺点
优点:
1.有名称,可以确切的描述正被返回的对象
2.不必每次调用都创建一个新对象,可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。(为重复的调用返回相同的对象)
3.可以返回原返回类型的任何子类型的对象,这种灵活性的应用是API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简洁。这项技术适用于基于接口的框架,因为在这种框架中,接口为静态工厂提供了自然返回类型。
4.所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
5.方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。
这种灵活的静态工厂方法构成了服务提供者框架的基础,例如JDBC API。服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来。
对于JDBC来说,Connection就是其服务接口的一部分, DriverManager.registerDriver是提供者注册API,DriverManager.getConnection是服务访问API,Driver是服务提供者接口。
缺点:
1.类如果不含公有的或者受保护的构造器,就不能被子类化。要想将Collections Framework中的任何便利的实现类子类化,这是不可能的。但这样会因祸得福,因为它鼓励程序员使用复合,而不是继承,这正是不可变类型所需要的(不可变类:实例不能被修改的类,每个实例中包含的所有信息都必须在创建时就提供,并在整个对象的生命周期内固定不变,比如String、基本类型的包装类、BigInteger、BigDecimal)
2.程序员很难发现它们。在API文档中,它们没有像构造器那样在API文档中明确标识出来。
场景问题:
一、设计一个订单号生成服务
1.唯一性:UUID 雪花算法
2.数据量:设计时需要充分考虑后续数据量变大的兼容问题,需要提前预留足够的位数
3.可读性:订单号应该易于理解和记忆,可以根据业务需求自定义订单号格式和组成方式,比如时间戳、用户id
4.基因法:设计时将和分表有关的字段编码到订单号中,比如买家ID
5.可扩展性:订单号生成服务需要支持高并发、分布式部署、横向扩展等特性,可以采用分布式ID生成器、Redis来实现。
6.高性能:内存缓存、异步处理
7.高可用:多节点部署、负载均衡、健康检查
比如
扩展:
UUID:
简单总结一下,Version 1和Version2这两个版本的UUID,主要基于时间和MAC地址,所以比较适合应用于分布式计算环境下,具有高度唯一性。
Version 3和 Version5 这两种UUID都是基于名称空间的,所以在一定范围内是唯一的,而且如果有需要生成重复UUID的场最的话,这两种是可以实现的。
Version 4 这种是最简单的,只是基于随机数生成的,但是也是最不靠谱的。适合数据量不是特别大的场景下
雪花算法:
是由twitter研发的一种分布式ID生成算法,可以生成全局唯一且递增的ID。他的思想是将一个64位的ID划分成多个部分,包括时间戳、数据中心表示、机器标识和序列号等。
其中时间戳位于id的最高位,保证新生成的ID比旧的ID大,在不同的毫秒内,时间戳不一样。(如果出现一毫秒内生成多个id的情况,通过引入序列号来解决,每次生成id时序列号自增)
优点:
1.高性能高可用:生成时不依赖于数据库,完全在内存中生成。
2.高吞吐:每秒钟能生成数百万的自增ID
3.ID自增:在单个进程中,生成的ID是自增的,可以用作数据库主键做范围查询,但是在集群中是没办法保证一定顺序递增的
缺点:
- 每个节点的机器ID和数据中心ID都是硬编码在代码中的,并且全局唯一,当某个节点出现故障或需要扩容时,就需要更改其对应的机器ID或数据中心ID,但是这个过程比较麻烦,需要重新编译代码,重新部署系统。另外,当某个节点的机器ID或者数据中心ID被设置成了已经被分配的ID,那么就会出现重复的ID,这样会导致系统的错误和异常。
- 雪花算法中需要使用zookeeper来协调各个节点的ID生成,但是zookeeper的部署成本大
3.依赖系统时间的一致性,如果系统时间回拨,或者不一致,可能造成ID重复。(解决时间回拨问题,最简单的是发现时钟回调,直接抛异常。另外一种是发现时钟变小就拒绝ID生成请求,等到时钟恢复到上一次的ID生成时间后,再开始生成新的ID,美团Leaf引入了Zookeeper来解决时钟回拨问题,其大致思路为:每个Leaf运行时定时向zk上报时间戳。每次Leaf服务启动时,先校验本机时间与上次发ID的时间,再校验与zk上所有节点的平均时间戳。如果任何一个阶段有异常,那么就启动失败报警。)
分表字段的选择:
通常用买家ID分表,可以避免数据倾斜(性能瓶颈、资源利用不均、查询效率低下),在生成订单号时,一般会把分表结果编码到订单号中,比如买家ID的路由结果1023,直接作为一段固定的值放到订单号里就行了,这就是基因法。
如果没有买卖家ID怎么办:
低频查询或非核心功能查询,可以用ES等搜索引擎解决。
二、如何设计一个购物车功能
主要功能:在用户选购后下单前,把用户的意向商品、数量等信息保存下来、方便统一支付。
主要操作:加入购物车、查看购物车、通过购物车下单。
一般我们在存储时并不需要把所有商品信息都保存,只需要保存一个SKUID就可以,再加上数量、时间等字段。至于商品的库存、价格、介绍等信息,只需要在渲染购物车时实时反查和计算就行。
对于电商平台来说,用户一般分为未登录和登录两种状态。
未登录用户:
对于未登录用户,其实他的购物车的信息没必要存储在后端,只需要在客户端做临时缓存就行了。客户端存储可以选择Cookie 和LocalStorage等技术。在存储时,只需要设计一个JSON格式就可以了,因为用户没登录,所以也就不需要标识数据属于谁
已登录用户:
对于已经登录的用户的购物车,我们就不能存储在客户端了,因为客户端的数据可能会超时、一旦换了设备也就没有了。我们需要用持久化存储,那么就可以使用数据库和Redis缓存。
如果是使用数据库,那么就直接建表存储就行了,表中主要需要包含user id、sku id、count、time stamp等几个业务字段就可以了。这样每一个加过购物车的用户都有一条记录。
如果使用Redis来保存的话,其实也简单,只需要在上面的未登录用户的购物车的基础上增加一个user id作为key就行了
扩展:
使用Redis和数据库存储各有好处:
- Redis 性能要比 MySQL高,响应时间更短,可以支撑更多的并发请求
2.MySQL的数据可靠性是要好于 Redis 的 ,因为 Redis 是异步刷盘,如果出现服务器掉电等异常情况,Redis 是有可能会丢数据的。
3.MySQL的另一个优势是,它支持丰富的查询方式和事务机制
三、每天100w次登录请求,4C8G机器如何做JVM调优?
首先,我们需要问清楚,一天100W次的登录,在一天内有没有某个时段是高峰的?高峰期的QPS大概可以达到多少。
如果没有高峰期,虽然100万听上去挺多的,但是其实平均下来一秒钟的QPS也就10,这个量的话,其实根本不需要做什么特别的JVM优化。
一般业务场景中,都是有自己的业务高峰期的,比如电商业务基本上上午十点和下午两点是业务高峰期,基本上这时候的QPS是平时的20倍都不止。
我们假设登录业务存在高峰期,峰值时长大概持续1个小时,峰值的QPS可以达到200。那么需要做哪些优化?
作为一个登录服务,一般来说我们在接收到请求之后,只需要给用户进行鉴权并把结果返回给前端就行了。在这个过程中一般不太会去查询太多的数据,比如权限什么的也都是在后面访问页面再查询的。所以,峰值200左右的QPS,对于JVM的内存来说,最主要的就是会因为远程调用,而创建出很多请求参数和请求的响应。而这些对象基本都是朝生暮死的,接口调用结束之后就会被回收掉并且通常来说这些对象也不会很大,因为登录并不是注册,其实并不携带特别多的信息,那么也就是说,会产生大量的小对象,即新生代会不断的创建对象并被回收掉
JVM调优:
1.堆内存设置:
当我们机器只有4核8G 堆内存大小的设置不能太大,要给其他应用预留一部分。所以一般把JVM的堆内存设置成操作系统内存的一半,也就是4G。至于初始内存和最大内存,建议设置成一样的,可以避免JVM在运行过程中频繁进行内存扩容和收缩操作,提高应用程序的性能和稳定性。
-Xms4G
-Xmx4G
2.垃圾回收器的选择:
经过分析,这个业务中频繁在新生代创建并销毁对象,那么就意味着新生代的GC比较频繁,所以应该选择一种GC过程中STW时间短的,并且在年轻代的回收中也能发挥效果的。
在新生代的垃圾收集器中,主要以Serial、ParNew、ParallelScavenge以及支持整堆回收的G1了。
因为新生代采用的都是复制算法,所以不太需要考虑碎片的问题,我们主要考虑吞吐量和STW的时长就行了,
首先排除单线程的Serial,剩下ParNew是一个并发的收集器,ParallelScavenge更加关注吞吐量,而G1作为JDK9中默认垃圾收集器,他不仅同时具有低暂停时间和高吞吐量的优点,但是他对内存有要求,最小要4G,
从使用门槛上来说,G1是可以用的,因为一般来说,内存要大于等于4G的话,才适合使用G1进行GC。
所以,我们采用G1作为垃圾收集器
-XX: +UseG1GC
在使用G1之后,其实他自己是有一套自动的预测和调优机制的。我们只需要通过参数来设置最大停顿时间就行了,一般建议设置到100-200之间,一般不会被用户感知
-XX:MaxGCPauseMillis=200
其次我们还可以调节一些G1的配置,比如GC线程数,可以先配置4个,后续根据实际情况调整:
-XX:ParallelGCThreads=4 设置并行GC线程数为4
-XX:ConcGCThreads=2 设置并发GC线程数为2
并发是一个CPU处理器同时处理多个线程任务。(宏观上是同时处理多个任务,微观上其实是CPU在多个线程之间快速的交替执行。操作系统中有一个组件叫做任务调度器,它将CPU的时间片(windows下时间片最小约为15毫秒)配给各个线程使用,在一个时间段的线程运行时,其他线程处于挂起状态,这种就称之为并发。)
并行是多个CPU处理器同时处理多个线程任务。(当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这就被称之为并行。)
3.各区的大小设置:
-XX:G1NewSizePercent 设置年轻代的初始大小 默认5%
-XX:G1MaxNewSizePercent 设置年轻代的最大大小 默认60%
对于我们的业务场景 5%太小了 可以调整到30%
-Xx:G1HeapRegionsize=2m:将 G1 的区域大小设置为 2MB,以提高垃圾回收的效率和精度。
-xx:G1NewsizePercent=20:设置年轻代的初始大小为堆的20%。
-XX:G1MaxNewsizePercent=50:设需年轻代的最大大小为堆的50%。
-XX:G10ldcsetRegionThresholdPercent=10:设置老年代的大小为堆的10%。
-XX:G1HeapWastePercent=5:设置垃圾回收后留下的未使用区域的最大比例为5%。
4.添加必要的日志:
因为以上配置都是根据业务大致分析出来的初始配置,所以我们一定是需要不断地调优的,那么必要的日志相关参数就要添加。如:
-XX:MaxGCPauseMillis=100:最大 Gc 暂停时间为 100 毫秒,可以根据实际情况调整;
-XX:+HeapDumpon0utofMemoryError:当出现内存溢出时,自动生成堆内存快照文件;
-XX:HeapDumpPath=/path/to/heap/dump/file.hprof:堆内存快照文件的存储路径;
-XX:+PrintGc:输出 GC 信息;
-XX:+PrintGcDatestamps:输出GC发生时间;
-XX:+PrintGcTimestamps:输出GC发生时JVM的运行时间;
-XX:+PrintGcDetails:输出GC 的详细信息;
-Xlog:gc*:file=/path/to/gc.log:time,uptime:filecount=10,filesize=108M:将GC 日志输出到指定文件中
四、如果你的业务量突然提升100倍QPS你会怎么做?
正常情况:业务有起色或业务蹭到某个热点
异常情况:被DDOS攻击
如果蹭到某个热点,就通过临时方案解决,最简单的就是扩容,增加集群的服务器数量,提升机器的硬件资源配置,让整体的吞吐量提升。
如果是业务自然增长,就需要考虑设计一个高并发系统
扩展:
防范DDOS攻击:
1、如果可以识别出攻击源,如机器IP等,可以在防火墙服务器上放置一份 ACL(访问控制列表) 来阻断这些来自这些 IP 的访问。
2、对于带宽消耗型攻击,最有效的办法那就是增加带宽
3、提高服务器的服务能力,增加负载均衡,多地部署等。
4、优化资源使用提高 web server 的负载能力。例如,使用 apache 可以安装 apachebooster 插件,该插件与varnish 和 nginx 集成,可以应对突增的流量和内存占用。
5、使用高可扩展性的 DNS 设备来保护针对 DNS 的 DDOS 攻击。可以考虑购买 Cloudflare 的商业解决方案,它可以提供针对 DNS 或 TCP/IP3 到7层的 DDOS 攻击保护。
6、启用路由器或防火墙的反IP欺骗功能。
7、付费,使用第三方的服务来保护你的网站。
8、监控网络和 web 的流量。时刻观察流量变化
代码随想录:
- 242.有效的字母异位词
- 349. 两个数组的交集
- 202. 快乐数
- 1. 两数之和
-
454.四数相加II
-
383. 赎金信
-
15. 三数之和
-
18. 四数之和