说说 Redis 的特性
Redis是一种开源的、基于内存的数据库系统,它可以用作数据库、缓存和消息中间件。
Redis是一种高性能的开源key-value内存数据库,它支持各种复杂数据结构,例如字符串、哈希、列表、集合等。Redis的特点是速度快,可以处理非常大的数据集,而且具有很强的可扩展性和数据持久化功能。
Redis的源码是用C语言编写的,它采用单线程模型,通过异步IO和事件驱动机制实现高并发。其架构包括网络层、客户端请求处理、命令执行引擎、键空间与过期管理、持久化、复制、Sentinel(哨兵)以及集群等模块。
Redis具有以下特点
-
内存数据库:Redis将数据存储在内存中,速度非常快,可以快速读写数据。
-
数据持久化:Redis支持将数据持久化到磁盘,以便在重启时恢复数据。
-
支持数据复制:Redis支持将数据复制到多个从节点,从而实现数据备份和故障恢复。
-
支持多种数据结构:Redis支持各种数据结构,包括字符串、列表、哈希表、集合和有序集合等。
-
支持事务:Redis支持事务,可以对多个操作进行原子性的集成,避免出现数据不一致的问题。
Redis应用场景包括
-
缓存:Redis可以作为缓存系统,用于存储热点数据,提高应用程序的响应速度。
-
数据库:Redis可以用作数据库,用于存储数据,支持多种数据结构和事务。
-
消息队列:Redis可以作为消息队列,用于异步消息的传递和处理。
-
分布式系统:Redis支持分布式架构,可以用于实现分布式数据存储和分布式计算。
-
监控和日志:Redis可以用于存储监控数据和日志,支持数据的快速查询和分析。
Redis的重功能及作用
-
网络层:负责接收来自客户端的socket连接请求,并将请求转发给执行引擎。
-
客户端请求处理: 将请求解析为命令,并在命令执行引擎中进行处理。
-
命令执行引擎: 负责解析和执行各种redis命令,例如set、get、hgetall等。
-
键空间与过期管理:管理redis数据库中的键值对,并按照过期时间删除过期的键值对。
-
持久化:提供RDB和AOF两种持久化方式,用于将redis数据库中的数据保存到磁盘上。
-
复制:实现主从复制功能,用于实现数据的备份和读写分离。
-
哨兵:用于监控redis主从服务器状态,并在主服务器宕机时自动将从服务器切换成主服务器。
-
集群:实现redis集群功能,支持水平扩展。
Redis支持多种数据结构,如字符串、散列表、列表、集合和有序集合等。Redis支持多种操作,如读取、写入、删除、事务处理、消息传输等。Redis还支持多种复杂的数据操作,如映射、排序、过滤等。
Redis源码,宏观层面主要分成两大部分
-
一个redis服务器,redis服务器是C语言编写的底层实现,包括协议栈、内存管理、数据结构、事务处理等模块;
-
另一个redis客户端,可以是不同的语言实现,比如Java编写的API层,redis客户端包括命令、过程、脚本、消息传输等模块。
redis客户端与redis服务器之间的通信是通过TCP协议进行的。
Redis服务器源码包括部分
-
协议栈:负责客户端与redis服务器之间的通信,包括协议解析、数据传输等。
-
内存管理:负责管理redis内存,包括内存池、内存分配和释放等。
-
数据结构:负责redis的数据存储和访问,包括字符串、散列表、列表、集合和有序集合等。
-
事务处理:负责支持多种事务,包括原子性、一致性和持久性等。
-
消息传输:负责支持多种消息传输协议,如PING、PONG、EVICT等。
-
过程和脚本:负责支持各种复杂的数据操作,包括映射、排序、过滤等。
Redis服务器源码非常庞大和复杂,如果要完整地研究和理解Redis的源码,需要有较强的编程能力和C/C++语言基础。
Redis集群的功能和实现方式
Redis集群是由多个Redis实例构成的分布式系统,它能够扩展到非常大的数据集,并提供高可用性和负载均衡功能。
Redis集群的功能作用
-
支持大规模数据存储:Redis集群可以支持非常大的数据集,可以方便地对数据进行扩容。
-
分布式计算:Redis集群可以将计算任务分配到不同的节点上进行计算,从而提高计算性能。
-
高可用性:当某个或某些节点发生故障时,Redis集群可以自动将工作节点转移到其他节点上,从而保证系统的高可用性。
-
负载均衡:Redis集群可以根据哈希槽位映射功能,将数据分布到不同的节点上,从而实现负载均衡。
Redis集群适合以下使用场景
-
对于需要存储大量数据并且需要进行读写操作的应用程序
-
高并发的Web应用程序,如电子商务、社交网络等。
-
大规模计算任务,如图像处理、视频处理等。
-
分散式数据存储。
Redis集群的两种主要的实现方式
1. Redis Sentinel实现
Redis Sentinel是一个特殊的Redis实例,它可以监控其他Redis实例的健康状态,并在发生故障时自动进行故障转移。Redis Sentinel通过主从复制实现故障转移,当主服务器出现故障时,Sentinel会将一个从服务器升级为新的主服务器,从而保证系统的高可用性。
Redis Sentinel实现的优点是实现简单,只需要开启Sentinel服务即可;缺点是不支持数据的水平扩展,通常适用于小规模集群环境。
2. Redis Cluster实现
Redis Cluster是Redis官方提供的集群实现方式,它采用分区(sharding)方式来保证系统的可扩展性和高可用性,每个数据片段被存储在不同的节点上,从而实现水平扩展。
Redis Cluster采用哈希槽位映射算法来对数据进行分片,每个节点负责一部分哈希槽位,当需要访问某个键值对时,客户端会先计算该键的哈希值,然后根据哈希槽位映射算法找到负责该哈希槽位的节点,从而实现负载均衡。
Redis Cluster实现的优点是支持数据的水平扩展和自动故障转移等功能;缺点是实现相对复杂,需要在多个节点上运行Redis实例,并进行相关配置。但是,在大规模集群环境下,Redis Cluster是更为合适的选择。
综上所述,Redis Sentinel实现适合小规模集群环境,而Redis Cluster实现适合大规模集群环境。
如何保证 MySQL 数据不丢
保证 MySQL 数据不丢有多种方法,主要包括以下几点:
3.1. 数据库备份
定期备份数据库,以防止数据丢失。可以使用 MySQL 自带的备份工具或者第三方备份工具来进行备份。
3.2. 数据库复制
使用 MySQL 的主从复制或者多主复制来进行数据备份和灾备。在主从复制中,主库写入数据后,从库会自动同步数据。在多主复制中,多个主库之间相互同步数据。
3.3. 数据库事务
使用数据库事务来保证数据的一致性和完整性。在事务中,如果某个操作失败,整个事务会回滚到之前的状态,从而保证数据不丢失。
3.4. 数据库高可用
使用数据库集群或者主备架构来保证数据库的高可用性。在集群或主备架构中,当一个节点出现故障时,可以自动切换到备用节点,从而保证服务的连续性和数据的不丢失。
3.5. 数据库监控
使用数据库监控工具来监控数据库的运行状态,及时发现并解决潜在的问题,从而保证数据的安全和可靠性。
读写分离,怎么从数据库读到最新数据
数据库读写分离,主要解决高并发时,提高系统的吞吐量。
读写分离数据库模型如下
-
写请求是直接写主库,然后同步数据到从库
-
读请求一般直接读从库,除非强制读主库
怎么保证从数据库读到最新的数据
方案一:强制走主库
● 写请求是直接写主库,然后同步数据到从库
● 读请求一般直接读从库,除非强制读主库
在高并发场景或者网络不佳的场景,如果存在较大的主从同步数据延迟,这时候读请求去读从库,就会读到旧数据。这时候最简单暴力的方法,就是强制读主库。
方案二:缓存标记法
在高并发场景或者网络不佳的场景,如果存在较大的主从同步数据延迟,这时候读请求去读从库,就会读到旧数据。这时候最简单暴力的方法,就是强制读主库。但是这样就违背了读写分离的初衷。
优化方案就是使用缓存标记法:
更新主库数据,并在缓存中设置一个标记,表示数据已更新。发起读请求,先判断数据已更新的标识,在缓存中有更新标记。则走主库;如果没有,请求走从库。
缓存标记法的执行流程如下
-
A发起写请求,更新主库数据,并在缓存中设置一个标记,表示数据已更新,标记格式为:userId+业务Id。
-
设置此标记,设置过期时间(估值为主库和从库同步延迟的时间)
-
B发起读请求,先判断此请求,在缓存中有没有更新标记。
-
如果存在标记,走主库;如果没有,请求走从库。
这个方案解决了数据不一致问题,但是每次请求都要先跟缓存打交道,会影响系统吞吐。
如何防止大流量请求把缓存击垮,可以引入多级缓存的架构。
高并发下如何设计秒杀系统?
秒杀系统首先是一个分布式后台系统,首先来看看,如何设计一个分布式后台系统?
一个分布式后台系统的设计要点
何设计一个分布式后台系统主要从需要考虑以下几个方面:
1. 架构设计
秒杀系统需要采用分布式架构,将请求分散到多个服务器上,以提高系统的并发能力和稳定性。可以使用负载均衡器来分发请求,使用缓存技术来减轻数据库的压力。
2. 数据库设计
秒杀系统需要采用高性能数据库,例如Redis等,来存储商品信息和用户订单信息。可以使用缓存技术来减轻数据库的压力,同时使用数据库事务来保证数据的一致性和完整性。
3. 接口设计
秒杀系统需要设计高性能的接口,以应对高并发的请求。可以采用异步处理的方式,将请求放入消息队列中,异步地处理请求,从而提高系统的并发能力。
4. 安全设计
秒杀系统需要采用安全措施,防止恶意攻击和刷单等行为。可以采用验证码、IP限制、用户限制等方式来保证系统的安全性和公平性。
5. 系统测试
秒杀系统需要进行充分的系统测试,包括压力测试、性能测试、安全测试等,以保证系统的可靠性和稳定性。
6. 业务设计
秒杀系统需要设计合理的业务规则,例如限制每个用户的购买数量、限制每个商品的秒杀数量等,以保证系统的公平性和可持续性。
综上所述,设计一个分布式后台系统,需要考虑多个方面,需要综合考虑系统的性能、安全、可靠性和公平性等因素。
一个高并发后台系统的设计要点
秒杀系统首先是一个高并发后台系统,再来看看,在高并发的情况下,设计秒杀系统需要考虑那些问题。
秒杀具有持续时间短和并发量大的特点,秒杀持续时间只有几分钟,而一般公司都为了制造轰动效应,会以极低的价格来吸引用户,因此参与抢购的用户会非常的多,短时间内会有大量请求涌进来。
一般在秒杀时间点(比如:双十一12点)前几分钟,用户并发量才真正突增,达到秒杀时间点时,并发量会达到顶峰。活动是大量用户抢少量商品的场景,其实绝大部分用户秒杀会失败,只有极少部分用户能够成功。
峰值持续的时间其实是非常短的,很容易出现瞬时高并发的情况,下面用一张图直观的感受一下流量的变化:
像这种瞬时高并发的场景,传统的系统很难应对。所以,高并发秒杀,需要额外的从高并发、超高并发的维度,进行架构设计和架构优化:
1. 限流
为了防止流量过大,造成系统崩溃或者无法正常使用,需要对流量进行限制。使用限流措施来控制请求流量,可以保证系统的稳定性和可用性。
例如,使用令牌桶算法(Token Bucket Algorithm)或漏桶算法(Leaky Bucket Algorithm)来限制请求速率。
为什么要限流?
很多刷子用户,并不会像我们一样老老实实,通过秒杀页面点击秒杀按钮,抢购商品。他们可能在自己的服务器上,模拟正常用户登录系统,跳过秒杀页面,直接调用秒杀接口。
如果是我们手动操作,一般情况下,一秒钟只能点击一次秒杀按钮。
但是如果是服务器,一秒钟可以请求成上千接口。这种差距实在太明显了,如果不做任何限制,绝大部分商品可能是被机器抢到,而非正常的用户,有点不太公平。
所以,我们有必要识别这些非法请求做一些限制。目前有两种常用的限流方式:
-
基于nginx限流
-
基于redis限流
秒杀最终的本质是数据库的更新,但是有很多大量无效的请求,我们最终要做的就是如何把这些无效的请求过滤掉,防止渗透到数据库。
2. 降级
为了避免高并发导致系统崩溃,可以采用降级策略,即当系统压力过大时,降低系统的并发量,保证系统的稳定性。
3. 数据库分库分表
为了提高系统的性能,可以采用数据库分库分表的方式,将数据拆分成多个表,以提高查询效率。
4. 缓存
为了减轻数据库的负载,可以采用缓存的方式,将一些热门数据缓存在内存中,加快查询速度。使用缓存技术来减轻数据库的负担,可以提高系统的性能和可用性。例如,使用Redis或Memcached等内存缓存来缓存热点数据,减少数据库的访问次数。
5. 异步
为了避免阻塞主线程,可以采用异步的方式,将耗时的操作放到子线程中,避免阻塞主线程。使用异步处理来处理大量请求,可以提高系统的吞吐量和响应速度。例如,使用mq异步处理来处理请求。
在真实的秒杀场景中,有三个核心流程:
而这三个核心流程中,真正并发量大的是秒杀功能,下单和支付功能实际并发量很小。所以,在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来,特别是下单功能要做成mq异步处理的。而支付功能,比如支付宝支付,是业务场景本身保证的异步。
于是,秒杀后下单的流程变成如下:
为了提升下单的效率,并且防止下单服务的失败。
需要将下单这一操作进行异步处理。最常采用的办法是使用消息队列,消息队列最显著的三个优点:异步、削峰、解耦。这里可以采用RocketMQ,在后台经过了限流、库存校验之后,流入到这一步骤的就是有效请求。然后发送到队列里,队列接受消息,异步下单。下完单,入库没有问题可以用短信通知用户秒杀成功。假如失败的话,可以采用补偿机制,重试。
6. CDN加速的页面静态化
秒杀详情页面是用户流量的第一入口,所以是并发量最大的地方。如果这些流量都能直接访问服务端,恐怕服务端会因为承受不住这么大的压力,而直接挂掉。秒杀详情页面绝大多数内容是固定的,比如:商品名称、商品描述、图片等。为了减少不必要的服务端请求,通常情况下,会对秒杀详情页面做静态化处理。
用户浏览商品等常规操作,并不会请求到秒杀服务端。只有到了秒杀时间点,并且用户主动点了秒杀按钮才允许访问秒杀服务端。这样能过滤大部分无效页面请求。
但只做页面静态化还不够,因为用户分布在全国各地,有些人在北京,有些人在成都,有些人在深圳,地域相差很远,网速各不相同。为了性能考虑,一般会将css、js和图片等静态资源文件提前缓存到CDN上,它的全称是Content Delivery Network,即内容分发网络。CDN 让用户能够就近访问秒杀页面,降低网络拥塞,提高用户访问响应速度和命中率。
7. 安全措施
采取安全措施来保护系统的安全性,包括防止SQL注入、XSS攻击等。例如,对用户输入的数据进行过滤和验证,使用SSL/TLS加密通信等。
8. 监控和报警
为了及时发现系统的异常情况,可以采用监控和报警的方式,及时发现和解决问题。
秒杀业务的架构难题
从业务视角,再来看看,秒杀系统有哪些特殊的业务架构难题:
-
库存超卖问题
-
秒杀url的接口防刷
-
库存高并发扣减问题
1.库存超卖问题
分析秒杀的业务场景,最重要的有一点就是超卖问题,在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。
问题:当商品A一共有库存15件,用户甲先下单10件,用户乙下单8件,这时候库存只能满足一个人下单成功,如果两个人同时提交,就出现了超卖的问题。
常见的解决的三种方案
-
悲观锁 通过悲观锁解决超卖
-
乐观锁 通过乐观锁解决超卖
-
异步分段锁方案 通过分段执行的排队方案解决超卖
悲观锁 和乐观锁的性能都比较低。重点介绍 高性能版本的 异步分段锁方案 。
分阶段排队下单方案
将提交操作变成两段式:
-
第一阶段申请,申请预减减库,申请成功之后,进入消息队列;
-
第二阶段确认,从消息队列消费申请令牌,然后完成下单操作。查库存 -> 创建订单 -> 扣减库存。通过分段锁锁保障解决多个provider实例并发下单产生的超卖问题。
申请阶段:
将存库从MySQL前移到Redis中,所有的预减库存的操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。
确认阶段:
然后通过队列等异步手段,将变化的数据异步写入到DB中。引入队列,然后数据通过队列排序,按照次序更新到DB中,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
异步分段锁架构图
基于异步分段锁的性能提升
一个高性能秒杀的场景:
假设一个商品1分钟6000订单,每秒的 600个下单操作。
在排队阶段,每秒的 600个预减库存的操作,对于 Redis 来说,没有任何压力。甚至每秒的 6000个预减库存的操作,对于 Redis 来说,也是压力不大。
但是在下单阶段,就不一样了。假设加锁之后,释放锁之前,查库存 -> 创建订单 -> 扣减库存,经过优化,每个IO操作100ms,大概200毫秒,一秒钟5个订单。600个订单需要120s,2分钟才能彻底消化。
如何提升下单阶段的性能呢?
可以使用Redis 分段锁。
为了达到每秒600个订单,可以将锁分成 600 /5 =120 个段,每个段负责5个订单,600个订单,在第二个阶段1秒钟下单完成。
有关Redis分段锁的详细知识:https://www.cnblogs.com/crazymakercircle/p/14731826.html
基于异步分段锁优点:
解决超卖问题,库存读写都在内存中,故同时解决性能问题。
基于异步分段锁缺点:
-
数据不一致的问题:
由于异步写入DB,可能存在数据不一致,存在某一时刻DB和Redis中数据不一致的风险。
-
可能存在少买
可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。
2.秒杀url的接口防刷问题
对于普通用户来讲,看到的只是一个比较简单的秒杀页面,在未达到规定时间,秒杀按钮是灰色的,一旦到达规定时间,灰色按钮变成可点击状态。这部分是针对小白用户的,如果是稍微有点电脑功底的用户,会通过F12看浏览器的network看到秒杀的url,通过特定软件去请求也可以实现秒杀。或者提前知道秒杀url的人,一请求就直接实现秒杀了。
这个问题我们需要考虑解决。
现在的秒杀大多都会出来针对秒杀对应的软件,这类软件会模拟不断向后台服务器发起请求,一秒几百次都是很常见的,如何防止这类软件不断发起的的重复无效请求进行限流,是我们需要认真考虑的。
接口防刷的话,需要入手的方面很多:
(1)前端限流
首先第一步就是通过前端限流,用户在秒杀按钮点击以后发起请求,那么在接下来的5秒是无法点击(通过设置按钮为disable)。这一小举措开发起来成本很小,但是很有效。
(2)同一个用户xx秒内重复请求直接拒绝
具体多少秒需要根据实际业务和秒杀的人数而定,一般限定为10秒。
具体的做法就是通过Redis的键过期策略,首先对每个请求都从String value = redis.get(userId);
如果获取到这个value为空或者为null,表示它是有效的请求,然后放行这个请求。如果不为空表示它是重复性请求,直接丢掉这个请求。如果有效,采用redis.setexpire(userId,value,10).value可以是任意值,一般放业务属性比较好,这个是设置以userId为key,10秒的过期时间(10秒后,key对应的值自动为null)。
(3)对同一ip限流
有时候只对某个用户限流是不够的,有些高手可以模拟多个用户请求,这时需要用Nginx加同一ip限流功能。
(4) 加验证码
通常情况下,用户在请求之前,需要先输入验证码。用户发起请求之后,服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作,否则直接返回,并且提示验证码错误。
此外,验证码一般是一次性的,同一个验证码只允许使用一次,不允许重复使用。
(5)业务层检查
业务服务层进行订单的数量检查,一个用户超过设置的订单数量, 下单失败。
3.库存高并发扣减问题
很多请求进来,都需要后台查询库存,这是一个频繁读的场景。
可以使用Redis来预减库存,在秒杀开始前可以在Redis设值,比如redis.set(goodsId,100),这里预放的库存为100可以设值为常量,每次下单成功之后,Integer stock = (Integer)redis.get(goosId);
然后判断sock的值,如果小于常量值就减去1;
不过注意当取消的时候,需要增加库存,增加库存的时候也得注意不能大于之间设定的总库存数(查询库存和扣减库存需要原子操作,此时可以借助lua脚本)下次下单再获取库存的时候,直接从Redis里面查就可以了。
说说 Object 类的方法
Object类是所有Java类的基类,它里面定义了一些通用的方法,如下:
-
equals()方法:判断两个对象是否相等。该方法默认使用"=="运算符进行比较,但可以通过重写该方法实现自定义的对象比较。
-
hashCode()方法:返回一个对象的哈希码值。哈希码值是根据对象的内部状态计算出来的一个整数,具有唯一性和稳定性。
-
toString()方法:返回一个字符串表示对象的信息。默认情况下,该方法返回对象所在的类名和内存地址,但可以通过重写该方法实现自定义的对象信息输出
-
getClass()方法:返回一个对象所属的类。该方法可以用于获取对象的运行时类型信息。
-
wait()、notify()、notifyAll()方法:这三个方法用于线程间的协作,其中wait()方法使当前线程进入阻塞状态,直到其他线程调用该对象的notify()或notifyAll()方法唤醒它。
-
finalize()方法:该方法由垃圾回收器调用,用于清理不再使用的对象。一般情况下,该方法不需要手动调用。
-
clone():创建并返回一个与原始对象相同类型和状态的新对象。
除此之外,Object类还提供了许多其他的静态方法和构造方法,例如:
-
getDeclaredFields():返回指定类的所有字段,但不包括父类的字段。
-
getDeclaredMethods():返回指定类的所有方法,但不包括父类的方法。
-
getConstructors():返回指定类的所有构造方法。
-
newInstance():使用给定的参数创建一个新的实例。
分布式事务的场景和其他方案
在分布式环境下开发时,经常会遇到分布式事务问题。分布式事务是一种解决分布式系统中的事务一致性问题的方案。在分布式系统中,由于各个节点之间的数据存储和处理是分离的,因此容易出现数据不一致的问题。
而分布式事务可以确保在分布式系统中的多个节点之间进行的多个操作的原子性、一致性和持久性,从而保证分布式系统的可靠性和可用性。
简单点来说,分布式事务是指涉及到多个数据库或应用服务器的事务,需要保证所有操作都要么全部成功,要么全部失败。
通常情况下,业务项目上,基本上的分布式事务可以使用以下两种方式实现:
1. 两阶段提交(2PC)
2PC是一种经典的分布式事务协议,它将分布式事务划分为两个阶段:准备阶段和提交阶段。在准备阶段,各参与节点进行数据校验,并向一个协调器发送是否同意提交事务的消息;在提交阶段,协调器向各个参与节点发送提交请求,参与节点执行事务并向协调器发送完成消息。
2PC方案的优点是简单易懂,适用于较小规模的分布式事务场景,但它存在单点故障风险和性能瓶颈等问题。
2. TCC事务
TCC事务是一种比较新颖的分布式事务方案,它将事务分为Try、Confirm、Cancel三个阶段,分别对应资源预留、执行确认、异常撤销等操作。TCC事务通过代码层面的控制,实现了无锁化和高性能的分布式事务处理。
TCC方案的优点是性能较好,且没有2PC的单点故障问题,但需要开发人员自己实现事务控制逻辑,实现较为复杂。
总之,在分布式环境下需要使用分布式事务方案来保证多个节点间数据的一致性。2PC和TCC都是比较常用的方案,需要根据具体业务场景选择合适方案。
什么场景用分布式事务,有其他方案嘛?
一般在保证数据的一致性和可靠性的高并发、分布式场景,使用分布式事务,比如秒杀系统中,通常会使用分布式事务来保证数据的一致性和可靠性。
使用分布式事务的主要原因是,在高并发场景下,如果每个请求都直接访问数据库,可能会出现数据不一致的情况。例如,当一个用户同时下单和支付时,如果这两个操作分别被不同的请求处理,就可能出现订单信息不完整或者支付失败的情况。而使用分布式事务可以将多个操作打包成一个事务,确保这些操作要么全部提交成功,要么全部回滚失败。
除了分布式事务,还有一些其他的方案可以用于实现高并发场景下的秒杀系统。
其中一些方案包括:
-
分布式锁:使用分布式锁来控制对某些资源的操作顺序,以避免多个请求同时访问同一资源。例如,使用Redis等内存数据库来实现分布式锁。
-
异步处理 + 定时任务补偿:使用异步处理来处理大量请求,可以提高系统的吞吐量和响应速度。例如,使用消息队列(如Kafka)或异步处理框架(如Redis Cluster)来处理请求。然后使用定时任务来进行数据一致性的扫描,一旦发现数据不一致,就行补偿,甚至预警。
总之,如何使用分布式事务,是否使用分布式事务,决于具体的业务需求和系统架构设计。
在实际应用中,通常需要综合考虑多个因素来选择最合适的方案。
分布式事务有几种解决方案?
分布式事务是指涉及到多个数据库或应用服务器的事务,需要保证所有操作要么全部成功,要么全部失败。为了解决这种问题,目前主要有以下几种分布式事务解决方案:
1. 两阶段提交(2PC)
2. TCC事务
3. Saga事务
Saga事务是一种将分布式事务拆解为多个本地事务的方案,它通过在各个本地事务之间传递消息,最终完成整个分布式事务。Saga事务与2PC和TCC不同,它可以实现更加灵活的事务处理,但需要开发人员自行设计和实现事务处理逻辑。
4. 消息队列
消息队列是一种异步通信方式,可以通过消息队列来保证数据的最终一致性。在分布式事务中,可以使用消息队列将事务处理过程异步化,从而提高系统的并发能力和可靠性。
综上所述,分布式事务有2PC、TCC、Saga事务和消息队列等多种解决方案。需要根据具体业务场景选择合适的方案。
JDK6、7、8 分别提供了哪些新特性
JDK6、7、8提供了许多不同的新特性,以下是其中一些的详细说明:
JDK6新特性
-
JSR 223:将脚本语言集成到Java应用程序中。
-
JAX-WS 2.0:更好的Web服务支持。
-
JAXB 2.0:更好的XML数据绑定和解析支持。
-
并发API增强:包括java.util.concurrent包的新增类和接口,如ConcurrentHashMap,并发集合等。
-
插入式注解处理API(APT):可用于在构建过程中生成源代码。
JDK7新特性
-
新的二进制字面量:可以使用0b或0B前缀表示二进制数字,并且可以使用下划线分隔数字,例如0b1011_0010。
-
switch语句支持字符串:可以使用字符串作为switch语句的参数。
-
try-with-resources语句:可以自动关闭实现java.lang.AutoCloseable接口的资源。
-
数值字面量下划线:可以使用下划线分隔数字,例如1_000_000。
-
简化泛型类型声明:可以通过“<>”来省略泛型类型声明。
-
动态语言支持:支持动态语言,如Groovy、Ruby等。
JDK8新特性
-
Lambda表达式:可以使用Lambda表达式实现函数式编程风格。
-
方法引用:可以使用方法引用来引用现有的方法。
-
接口默认方法:可以在接口中定义默认方法。
-
Stream API:可以使用Stream API实现数据流式处理。
-
新的日期/时间API:引入了新的日期和时间API,比原有的Date和Calendar类更加易于使用。
-
对Nashorn JavaScript引擎的支持:可以使用Nashorn引擎来运行JavaScript代码。
总之,JDK6、7、8提供了许多不同的新特性,包括并发API增强、Lambda表达式、Stream API、新的日期/时间API等。这些新特性可以帮助Java开发人员更加高效和方便地开发应用程序。
说说https原理和工作流程
HTTPS(Hyper Text Transfer Protocol Secure)是一种通过加密和认证来保护网络通信安全的协议。它是HTTP的安全版,可以有效地防止黑客窃听、中间人攻击等网络安全问题,广泛应用于电子商务、在线支付、社交媒体等领域。
HTTPS的原理可以简单概括为:使用公开密钥加密算法对数据进行加密,确保数据在传输过程中不被窃取或篡改;使用数字证书验证服务端身份,确保用户连接的是正规的服务器。
HTTPS的工作流程如下
-
建立连接:客户端向服务器发送一个请求,请求中包含了要访问的资源的URL。
-
SSL握手:当服务器接收到请求后,会返回一个数字证书,证书中包含了服务器公钥。客户端收到证书后,会对证书进行验证,如果验证通过,就会生成一个随机数并用服务器公钥加密,将加密后的随机数和自己的私钥一起发送给服务器。服务器收到后,使用自己的私钥解密该随机数,得到客户端发送的随机数。之后,双方会交换证书和密钥,用于后续的数据加密和解密。
-
发送HTTPS请求:在SSL握手完成后,客户端会发送一个HTTPS请求到服务器。此时,HTTP和HTTPS是分开的两个协议,需要进行切换。切换的过程是通过TLS协议实现的。TLS协议会在客户端和服务器之间建立一条安全通道,用于加密数据传输。
-
发送HTTPS请求:在TLS连接建立成功后,客户端会再次发送一个HTTPS请求到服务器。此时,客户端和服务器之间的通讯都是加密的。
-
接收HTTPS响应:服务器接收到请求后,处理完业务逻辑后会返回一个HTTPS响应给客户端。此时,客户端和服务器之间的通讯也是加密的。
-
接收HTTPS响应:客户端接收到HTTPS响应后,会对响应进行解密,得到实际的响应内容。
总之,HTTPS通过SSL/TLS协议对数据传输进行了加密和解密,保证了通信的安全性。
Redis的持久化方式和原理
Redis的持久化是指将Redis中的数据写入磁盘,以便在Redis重启后能够恢复数据。Redis提供了两种持久化方式:RDB和AOF。
1. RDB持久化
RDB持久化是将Redis在内存中的数据定期dump到磁盘中,生成一个RDB文件。RDB文件是一个二进制文件,包含了Redis在某个时间点上的数据快照。RDB文件的生成可以手动触发,也可以通过配置文件设置定期自动触发。
RDB持久化的原理是Redis会fork出一个子进程,将数据写入到一个临时文件中,然后将临时文件替换为旧的RDB文件。在这个过程中,Redis会将所有新的写操作缓存在内存中,直到持久化完成后再将缓存的写操作应用到新的RDB文件中。
2. AOF持久化
AOF持久化是将Redis的写操作以追加的方式写入到一个日志文件中,即AOF文件。AOF文件是一个文本文件,包含了Redis的所有写操作,以及执行这些操作所需的参数。AOF文件的生成可以手动触发,也可以通过配置文件设置定期自动触发。
AOF持久化的原理是Redis会将所有新的写操作追加到AOF文件中,当AOF文件过大时,Redis会自动执行一次重写操作,将AOF文件中的冗余操作删除,从而减小AOF文件的大小。
总的来说,RDB持久化和AOF持久化各有优劣。RDB持久化的优点是生成的文件较小,恢复数据的速度较快;而AOF持久化的优点是数据的完整性更好,可以做到每秒钟一次的持久化,从而减少数据的丢失。在实际使用中,可以根据实际情况选择适合自己的持久化方式。
谈谈 volatile 关键字作用?
volatile是Java中的一种关键字,用于确保多线程环境下变量的可见性和有序性。在多线程编程中,使用volatile可以解决一些内存可见性和指令重排序问题,从而避免了一些难以调试和排查的问题。
作用
-
可见性:当一个变量被定义为volatile时,它会保证所有线程都能够看到该变量的最新值。
-
禁止指令重排序:当一个变量被定义为volatile时,JVM会禁止对它进行指令重排序操作,从而保证程序的正确性。
原理
在Java内存模型中,每个线程都有自己的工作内存和主内存。当一个线程要读取共享变量的值时,它会先将该变量的值从主内存中复制到自己的工作内存中,然后再执行操作;当一个线程要写入共享变量的值时,它会先将该变量的值写入自己的工作内存中,然后再将其刷新到主内存中。
volatile关键字通过以下两种方式保证多线程环境下的可见性和有序性:
-
写入volatile变量时,JVM会立即将其更新到主内存中,这样就保证了其他线程可以及时看到最新的值。
-
读取volatile变量时,JVM会从主内存中读取最新的值,而不是使用工作内存的值。这样就保证了线程之间共享变量时的可见性和有序性。
需要注意的是,volatile只能保证单个变量的原子性操作,对于复合操作或者涉及多个变量的操作,还需要使用synchronized等同步机制来保证线程安全。
总之,volatile关键字可以保证多线程环境下变量的可见性和有序性,通过将变量的值立即更新到主内存中来实现。在多线程编程中,使用volatile可以提高程序的并发性和可靠性。
讲讲 java jmm volatile 的实现原理?
Java中的JMM(Java内存模型)是一种规范,用于定义多线程程序中的内存访问行为。它确保了多线程程序的正确性和可见性,从而避免了常见的线程安全问题。
在JMM中,volatile是一种关键字,用于确保变量的可见性和顺序性。当一个变量被声明为volatile时,它的值将始终从主存中读取,而不是从线程的本地缓存中读取。当一个线程修改了一个volatile变量的值时,这个值将立即被写回主存中,而不是在一段时间后才被写回。
volatile的实现原理是通过内存屏障(memory barrier)来实现的。内存屏障是一种硬件或软件机制,用于确保内存操作的顺序性和可见性。在Java中,volatile变量的读写操作会被编译器和CPU优化,为了确保操作的顺序性和可见性,Java会在编译时和运行时插入内存屏障。
具体来说,当一个线程访问一个volatile变量时,它会执行一个load指令,这个指令会强制从主存中读取变量的值,并将其存储在线程的本地缓存中。当一个线程修改一个volatile变量的值时,它会执行一个store指令,这个指令会强制将变量的值写回主存中。在这些指令周围,Java会插入内存屏障,确保指令的顺序性和可见性。
总之,volatile通过内存屏障来实现变量的可见性和顺序性,从而确保了多线程程序的正确性和可靠性。
在秒杀场景中,常用的限流算法有哪些?
秒杀场景是指在短时间内出现高并发请求的情况,为了保护系统的稳定性,需要对请求进行限流。常用的限流算法有以下几种:
1. 令牌桶算法
令牌桶算法是一种固定窗口限流算法,它通过令牌桶来控制请求的频率。令牌桶中会不断产生令牌,并将其放入桶中,每当一个请求到达时,就从桶中获取一个令牌,如果桶中没有令牌,则请求被拒绝。
2. 漏桶算法
漏桶算法也是一种固定窗口限流算法,它通过一个带有固定速率的漏桶,来控制请求的频率。当请求到达时,先进入漏桶中,然后以固定速率流出,如果漏桶已满,则请求被拒绝。
3. 计数器算法
计数器算法是一种简单的限流算法,它通过记录请求次数和时间戳,来控制请求的频率。当请求到达时,判断当前时间与最近一次请求的时间差是否小于设定阈值,如果小于,则请求被拒绝。
总之,在秒杀场景中,需要对请求进行限流来保护系统的稳定性。常用的限流算法有令牌桶算法、漏桶算法、计数器算法等,需要根据实际情况选择合适的算法。
TCP 为什么要三次握手
七层网络模型是一种分层的网络模型,它将网络分为七个层次,每一层都有自己的功能和特点。其中,最底层的是物理层,它主要负责将比特流转换为电信号,并进行物理传输。
TCP/IP 协议是一种常用于网络通信的协议,它由四个层次组成,分别是应用层、传输层、网络层和数据链路层。其中,传输层是一个非常重要的层次,它负责在不同的网络之间传递数据,并保证数据的可靠传输。
在传输层中,TCP 协议使用三次握手来确保数据的可靠传输。这三次握手分别是:
-
第一次握手:客户端向服务器发送一个 SYN 数据包,表示客户端想要建立一个 TCP 连接。服务器收到 SYN 数据包后,会向客户端发送一个 SYN+ACK 数据包,确认客户端的请求,同时也表示服务器愿意建立这个连接。
-
第二次握手:客户端收到服务器的 SYN+ACK 数据包后,会向服务器发送一个 ACK 数据包,确认服务器的请求。
-
第三次握手:服务器收到客户端的 ACK 数据包后,会向客户端发送一个 FIN 数据包,表示服务器已经接受到客户端发送的数据,并且会等待客户端发送最后一个 ACK 数据包。
这三次握手的过程中,客户端和服务器会交换很多数据包,但是每一次握手都是独立的,即每一次握手只传输一个数据包。只有当三次握手都成功完成后,客户端和服务器才会建立起一个 TCP 连接,并开始传输数据。
通过三次握手,TCP 协议可以确保数据的可靠传输,即使在数据传输过程中出现了一些问题,TCP 协议也可以通过重新发送数据包来恢复数据的传输。这也是 TCP 协议被广泛使用的重要原因之一。
说说线程池工作原理
线程池是一种常见的线程管理机制,它可以提高线程的利用率和性能,并且可以避免线程创建和销毁的开销。线程池中包含一组可重用的线程,这些线程可以被多个任务共享,从而减少了线程创建和销毁的开销。当有新的任务到来时,线程池会从池中取出一个空闲的线程来执行任务,当任务执行完成后,线程会返回到池中,等待下一个任务的到来。
线程池的工作原理如下
-
当线程池被创建时,会初始化一定数量的线程,并将它们放入线程池中。
-
当有新的任务到来时,线程池会从池中取出一个空闲的线程来执行任务。
-
如果池中没有空闲线程,线程池会根据预设的策略来创建新的线程,或者等待有线程空闲为止。
-
当任务执行完成后,线程会返回到池中,等待下一个任务的到来。
线程池的主要优点
-
提高程序性能:线程池可以减少线程创建和销毁的开销,从而提高程序的性能。
-
提高线程利用率:线程池可以避免线程因等待任务而处于空闲状态,从而提高线程的利用率。
-
提高程序可靠性:线程池可以避免因线程创建和销毁的错误而导致程序出错。
-
提高程序可扩展性:线程池可以根据需要动态调整线程的数量,从而提高程序的可扩展性。
总之,线程池是一种非常有用的线程管理机制,它可以提高程序的性能、可靠性和可扩展性,是多线程编程中不可缺少的一部分。
说说:JVM内存模型
首先说下概念误区,JVM内存模型很容易被误解为 JVM内存结构。
JVM内存结构是指Java程序在运行时所使用的内存结构和组织方式。Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。
Java 虚拟机所管理的内存被划分为如下几个区域:
-
程序计数器(Program Counter Register):用于记录当前线程执行到哪一条指令,是线程私有的内存区域。
-
Java虚拟机栈(Java Virtual Machine Stacks):每个线程都有一个Java虚拟机栈,用于存储方法调用的局部变量表、操作数栈、动态链接、方法返回值等信息。Java虚拟机栈也是线程私有的内存区域。
-
堆(Heap):用于存储对象实例和数组等数据结构,是所有线程共享的内存区域。堆可以分为新生代和老年代两个部分,根据对象的生命周期进行垃圾回收。
-
方法区(Method Area):用于存储已被加载的类的信息、常量、静态变量等数据,也是所有线程共享的内存区域。方法区也称为永久代(Permanent Generation),已经被标记为过时,现在被元空间(Metaspace)取代。
-
本地方法栈(Native Method Stack):用于存储本地方法的调用信息,与Java虚拟机栈类似,但是它是为虚拟机内部使用而设计的。
和 JVM 内存结构不同,JVM(Java Virtual Machine)内存模型定义了Java 程序中各种变量的访问规则,包括变量的读取、写入和同步等操作,以确保多线程程序的正确性和可靠性。
JVM内存模型主要分为两个部分:线程工作内存和主内存。
线程工作内存是每个线程独有的内存区域,它包含了线程执行时所需要的所有变量和对象,以及线程执行的指令集。每个线程都有自己的工作内存,线程之间的变量不共享,因此线程之间的通信需要通过主内存来进行。
主内存是所有线程共享的内存区域,它包含了所有的变量和对象。当一个线程需要访问某个变量时,它首先需要将变量从主内存中读取到自己的工作内存中,然后对变量进行操作。操作完成后,线程需要将变量的值写回主内存,以便其他线程可以看到这个变量的最新值。
JVM内存模型通过锁和内存屏障等机制来确保多线程程序的正确性和可靠性。锁用于同步多个线程对共享变量的访问,内存屏障用于保证变量的可见性和顺序性。在Java中,volatile关键字可以用来保证变量的可见性和顺序性,synchronized关键字可以用来保证线程的互斥和同步,而Lock接口和Atomic类可以用来实现更灵活的同步机制。
说说JVM 垃圾回收机制
垃圾回收机制是Java程序中非常重要的一部分,它可以自动地处理程序中不再使用的对象,从而避免内存泄漏的问题。
Java中有两种垃圾回收机制
-
标记-清除垃圾回收机制(Mark-Sweep GC):这是Java早期的垃圾回收机制,使用标记-清除的方式来回收内存。标记过程中,Java虚拟机会给所有可达对象分配一个标记,标记的对象会被回收。清除过程中,Java虚拟机会扫描整个内存区域,找出所有未被标记的对象,然后将它们清除。
-
复制垃圾回收机制(Copy-on-Write GC):这是Java最新的垃圾回收机制,使用复制-分发的方式来回收内存。在这种机制中,Java虚拟机会将内存划分为两个部分,一部分是新生代(Young Generation),一部分是老年代(Old Generation)。新生代中的对象会被复制到老年代中,当老年代满了之后,Java虚拟机会将老年代中的对象复制到新生代中。这样就可以保证新生代中始终有一部分空间可以使用,而老年代中的对象可以被回收。
Java中的内存分代模型
1. 内存分代模型
分代模型并不是一种垃圾回收算法,而是一种内存管理模型。将java中的内存分为不同区域,在GC时不同区域采用不同的算法,提高回收效率。内存分代模型将java堆内存中的区域分成两部分新生代(new)和老年代(old),两块区域的默认比例为1:2。
新生代:新生代中的对象被使用过,并且被引用过。
老年代:一旦新对象生成,就会被放入老年代中。老年代中的对象没有被使用过,也没有被引用过。
新生代又分为一个伊甸园区(eden)和两个存活区(survivor),s0和s1,默认比例为8:1:1
新生代的GC被称为YGC/MinorGC,老年代的GC被称为Full GC/MajorGC。
2. 分代垃圾回收算法
-
新对象出生在eden中,对象过大在eden装不下则直接进入老年代
-
第一次YGC:新生代对象大多数会被回收(80%-90%),剩余活着的对象会被复制到s0区,清空eden
-
第二次YGC:把活着的对象(eden+s0区)到复制到s1区,清空eden和s0
-
再次YGC:把活着的对象(eden+s1区)到复制到s0区,清空eden和s1
-
每经过一次GC,没有被回收的对象年龄+1,当存活对象到达一定年龄后,新生代对象进入老年代(一般是15,CMS回收器默认是6,其他垃圾回收器是15)
-
YGC复制时,如果对象过大,s区装不下也会直接将对象拷贝到老年代。
垃圾回收的过程一般是由Java虚拟机自动进行的,开发人员不需要手动干预。
但是,开发人员可以通过设置垃圾回收的参数来控制垃圾回收的频率和方式。垃圾回收机制对于Java程序的性能和稳定性都非常重要,需要开发人员认真对待。
类加载机制和双亲委派模型
类加载机制是指Java虚拟机(JVM)如何加载和链接Java程序中的类。在Java中,类的加载和链接是由JVM负责完成的,JVM会在运行时动态地将类加载到内存中,并进行链接、初始化等操作。
类加载机制指的是:虚拟机将描述类的数据从class文件加载到内存中,对加载的数据进行验证,解析,初始化,最后得到虚拟机认可后转化为直接可以使用的java类型的过程。
类加载机制一共有七个阶段:加载,验证,准备,解析,初始化,使用,卸载。其中的验证,准备,解析合称为连接阶段。
加载,验证,准备,初始化,卸载的顺序是确定是,另两个由动态绑定等情况可能会在初始化后面。画个草图:
双亲委派模型
双亲委派模型是Java类加载机制的核心概念之一。它是一种基于层次结构的类加载模型,用于描述Java类加载器之间的父子关系。简单来说,双亲委派模型就是“委托”父类加载器去加载子类的类,而不是自己直接去加载。
类加载器有是三个:启动类加载器、扩展类加载器、应用程序加载器(系统加载器)
具体来说,双亲委派模型包含以下几个阶段:
-
父类加载器加载本地模块或扩展包中的类。
-
如果父类加载器无法找到该类,则会委托给其父类加载器继续加载。
-
如果父类加载器也无法找到该类,则会委托给顶层的启动类加载器(Bootstrap ClassLoader)去加载。
-
如果启动类加载器也无法找到该类,则会抛出NoClassDefFoundError异常。
总之,双亲委派模型通过将类加载委托给父类加载器来实现对类的管理和控制,从而保证了Java类的安全性、可靠性和稳定性。
工作过程是:如果一个类加载器收到了一个类加载的请求,它首先不会去加载类,而是去把这个请求委派给父加载器去加载,直到顶层启动类加载器,如果父类加载不了(不在父类加载的搜索范围内),才会自己去加载。
-
启动类加载器:加载的是lib目录中的类加载出来,包名是java.xxx(如:java.lang.Object)
-
扩展类加载器:加载的是lib/ext目录下的类,包名是javax.xxx(如:javax.swing.xxx)
-
应用程序扩展器:这个加载器就是ClassLoader的getSystemClassLoader的返回值,这个也是默认的类加载器。
双亲委派模型的意义在于不同的类之间分别负责所搜索范围内的类的加载工作,这样能保证同一个类在使用中才不会出现不相等的类,举例:如果出现了两个不同的Object,明明是该相等的业务逻辑就会不相等,应用程序也会变得混乱。
进程间通信的方式有哪些
进程间通信(IPC,InterProcess Communication)的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。下面简述一下这六种方式:
-
管道(Pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。
-
命名管道(FIFO):命名管道也是半双工的通信方式,但它允许无亲缘关系进程间的通信。
-
消息队列(Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
-
共享内存(Shared Memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
-
信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
-
信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。
综上所述,不同的进程间通信方式各有优缺点,应根据具体的应用场景选择合适的方式。通常情况下,管道适用于亲缘关系进程之间的通信;消息队列适用于无连接的进程间通信;共享内存适用于需要高效数据共享的场景;套接字适用于需要跨平台、跨语言的网络通信。
String、StringBuffer、StringBuilder 的区别与场景
String、StringBuffer 和 StringBuilder 都是 Java 中用于处理字符串的类,它们的区别如下:
-
String 是一个不可变对象,一旦被创建就无法被改变。对 String 进行修改操作时,会创建一个新的 String 对象。因此在频繁修改字符串时,创建大量的 String 对象会导致内存浪费。
-
StringBuffer 是可变对象,可以进行字符串修改操作而不是创建新的对象。因此,使用 StringBuffer 可以提高程序的性能。但是,StringBuffer 需要手动控制方法的调用和字符串的长度,使用不当可能导致程序出现异常。
-
StringBuilder 是 StringBuffer 的子类,提供了一种更加简单易用的方式来处理字符串。StringBuilder 对一些常用的字符串操作(如 append、insert、delete)进行了重载,可以直接使用这些方法来进行字符串修改操作,而无需手动控制方法的调用和字符串的长度。此外,StringBuilder 还提供了一些其他的方法,如 reverse()、toString() 等。
在使用上,一般而言:
-
如果只需要对字符串进行简单的操作(如拼接),可以使用 String 或 StringBuffer;
-
如果需要频繁地对字符串进行修改操作,可以使用 StringBuffer;
-
如果需要方便地进行字符串修改操作,可以使用 StringBuilder。
使用策略
-
基本原则:如果要操作少量的数据,用String ;单线程操作大量数据,用StringBuilder ;多线程操作大量数据,用StringBuffer。
-
不要使用String类的"+"来进行频繁的拼接,因为那样的性能极差的,应该使用StringBuffer或StringBuilder类,这在Java的优化上是一条比较重要的原则。
-
为了获得更好的性能,在构造 StringBuffer 或 StringBuilder 时应尽可能指定它们的容量。当然,如果你操作的字符串长度(length)不超过 16 个字符就不用了,当不指定容量(capacity)时默认构造一个容量为16的对象。不指定容量会显著降低性能。
-
StringBuilder 一般使用在方法内部来完成类似 + 功能,因为是线程不安全的,所以用完以后可以丢弃。StringBuffer 主要用在全局变量中。
-
相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。而在现实的模块化编程中,负责某一模块的程序员不一定能清晰地判断该模块是否会放入多线程的环境中运行,因此:除非确定系统的瓶颈是在 StringBuffer 上,并且确定你的模块不会运行在多线程模式下,才可以采用 StringBuilder;否则还是用 StringBuffer。
总结:优先考虑StringBuffer
用过哪些第三方库?
第三方库是指由其他开发者创建和维护的,可以用于解决特定问题或实现特定功能的开源代码库。
Java 作为一门广泛应用于企业级开发的语言,拥有许多优秀的第三方库,以下是其中一些常用的库及其作用和应用场景:
-
Spring Framework:一个轻量级的、开源的应用程序框架,主要用于企业级Java应用程序的开发。它提供了一个全面的编程和配置模型,用于构建现代化的基于Java的企业应用程序。
-
Apache Struts:一个基于MVC的Web应用程序框架,它可以帮助开发人员构建可扩展的Web应用程序。
-
Hibernate:一个开源的对象关系映射框架,它可以将Java对象映射到关系型数据库中,从而简化了数据访问层的开发。
-
Apache Log4j:一个灵活的日志框架,它可以帮助开发人员记录应用程序的日志,并支持多种日志级别和输出格式。
-
Apache Tomcat:一个开源的Web应用程序服务器,它支持Java Servlet和JavaServer Pages(JSP)技术,并提供了一个可扩展的架构,可以用于构建高性能的Web应用程序。
-
Apache POI:一个开源的Java库,用于操作Microsoft Office格式的文档,包括Excel、Word和PowerPoint等。
-
Gson:一个开源的Java库,用于将Java对象转换为JSON格式的字符串,或者将JSON格式的字符串转换为Java对象。
-
Jackson:一个开源的Java库,用于将Java对象转换为JSON格式的字符串,或者将JSON格式的字符串转换为Java对象。它支持多种JSON格式,包括JSON、XML和YAML等。
-
JUnit:一个开源的Java测试框架,它可以帮助开发人员编写单元测试,并提供了丰富的断言和测试工具。
-
Apache Commons:一个开源的Java库,它包含了许多常用的工具类和函数,可以帮助开发人员简化Java编程。
以上是一些常用的Java第三方库,它们可以帮助开发人员简化Java编程,提高开发效率和代码质量。
工厂模式的三种实现方法
工厂模式: 是Java中最常用的设计模式之一, 它提供了一种创建对象的最佳方式。
在工厂模式中, 我们在使用工厂类创建对象时 不会对客户端暴露 创建逻辑, 并且是通过使用一个共同的接口来指向新创建的对象。三种实现方法
-
简单工厂模式
-
工厂方法模式
-
抽象工厂模式
简单工厂模式
简单工厂模式: 定义一个创建对象的工厂类, 由工厂类决定创建出哪一种对象的实例, 工厂类内部已封装创建出哪一种对象的实例的逻辑代码。
Phone 手机接口
interface Phone {
// 运行手机的抽象方法
void run();
}
IPhone 苹果手机实现类
class IPhone implements Phone{
@Override
public void run() {
System.out.println("运行苹果手机");
}
}
HuaweiPhone 华为手机实现类
class HuaweiPhone implements Phone {
@Override
public void run() {
System.out.println("运行华为手机");
}
}
PhoneFactory 定义一个创建Phone对象实例的手机工厂类, 内部已封装创建出哪一种品牌手机实例的逻辑代码
class PhoneFactory {
public Phone createPhone(String phoneType) {
Phone phone = null;
if ("IPhone".equals(phoneType)) {
phone = new IPhone();
} else if ("HuaweiPhone".equals(phoneType)) {
phone = new HuaweiPhone();
}
return phone;
}
}
测试
public class SimpleFactory {
public static void main(String[] args) {
// 创建手机工厂类
PhoneFactory phoneFactory = new PhoneFactory();
// 通过手机工厂类创建 IPhone手机实例对象
Phone IPhone = phoneFactory.createPhone("IPhone");
IPhone.run(); // 运行苹果手机
// 通过手机工厂类创建 HuaweiPhone手机实例对象
Phone HuaweiPhone = phoneFactory.createPhone("HuaweiPhone");
HuaweiPhone.run(); // 运行华为手机
}
}
简单工厂模式: 是使用工厂类来创建不同实例的对象, 可以将创建对象的方法静态化, 代码更简洁明了
public class PhoneFactory {
public static Phone createPhone(String phoneType) {
Phone phone = null;
if ("IPhone".equals(phoneType)) {
phone = new IPhone();
} else if ("HuaweiPhone".equals(phoneType)) {
phone = new HuaweiPhone();
}
return phone;
}
}
// 可以直接通过 PhoneFactory.createPhone("phoneType") 创建手机对象
简单工厂模式存在问题
-
工厂类集中了所有实例(品牌手机)的创建逻辑,一旦这个工厂类不能正常工作,整个系统都会受到影响
-
违背了开闭原则(对扩展是开放的, 对修改时关闭的), 一旦添加新对象实例(新品牌手机)就不得不修改工厂类的逻辑
工厂方法模式
工厂方法模式: 先定义一个工厂父类 (负责定义创建对象的抽象接口). 再定义一个工厂子类 (负责生成具体的对象), 工厂方法模式将对象的实例化推迟到工厂子类
Phone 手机抽象类、IPhone 苹果手机实现类 和 HuaweiPhone 华为手机实现类
/**
* Phone 手机抽象类: 提供运行手机的抽象方法
*/
abstract class Phone {
abstract void run();
}
/**
* IPhone 运行苹果手机
*/
class IPhone extends Phone{
@Override
public void run() {
System.out.println("运行苹果手机");
}
}
/**
* HuaweiPhone 运行华为手机
*/
class HuaweiPhone extends Phone {
@Override
public void run() {
System.out.println("运行华为手机");
}
}
先定义一个工厂抽象父类: 提供创建手机对象的抽象方法
abstract class PhoneFactory {
abstract Phone createPhone();
}
再定义一个苹果工厂子类: 负责创建具体的对象(苹果手机对象) 再定义一个华为工厂子类: 负责创建具体的对象(华为手机对象)
class IPhoneFactory extends PhoneFactory {
@Override
public IPhone createPhone() {
return new IPhone();
}
}
class HuaweiPhoneFactory extends PhoneFactory {
@Override
public HuaweiPhone createPhone() {
return new HuaweiPhone();
}
}
测试:
public class FactoryMethod {
public static void main(String[] args) {
// 苹果子类工厂 创建 苹果手机对象
IPhoneFactory iPhoneFactory = new IPhoneFactory();
IPhone iPhone = iPhoneFactory.createPhone();
iPhone.run(); // 运行苹果手机
// 华为子类工厂 创建 华为手机对象
HuaweiPhoneFactory huaweiPhoneFactory = new HuaweiPhoneFactory();
HuaweiPhone huaweiPhone = huaweiPhoneFactory.createPhone();
huaweiPhone.run(); // 运行华为手机
}
}
工厂方法模式存在问题: 对象父类 与 对象工厂父类 一一对应, 对象子类 与 对象工厂子类 一一对应. 即: 一个具体工厂子类只能创建一类产品
抽象工厂模式
抽象工厂模式: Abstarct Factory Pattern, 是围绕一个超级工厂创建其他工厂, 该超级工厂又称为其他工厂的工厂。
在抽象工厂模式中, 超级工厂中提供多个接口, 每个接口负责创建一个相关对象的其他工厂, 工厂创建的对象 不需要显式指定它们的类, 指向抽象类。
在抽象工厂模式中, 超级工厂子类负责创建具体的对象: 一个具体超级工厂子类创建多类产品。
/**
* 电子产品超级工厂: 超级工厂创建其他工厂
* 创建手机工厂的抽象方法
* 创建电脑工厂的抽象方法
*/
abstract class ElectronicProductsFactory {
/**
* 手机工厂 创建Phone手机抽象对象
* @return
*/
abstract Phone createPhoneFactory();
/**
* 电脑工厂 创建Computer电脑抽象对象
* @return
*/
abstract Computer createComputerFactory();
}
在抽象工厂模式中, 超级工厂子类负责创建具体的对象: 一个具体超级工厂子类创建多类产品
/**
* 苹果超级工厂子类: 创建苹果手机和苹果电脑对象
*/
class AppleFactory extends ElectronicProductsFactory {
@Override
IPhone createPhoneFactory() {
return new IPhone();
}
@Override
AppleComputer createComputerFactory() {
return new AppleComputer();
}
}
/**
* 华为超级工厂子类: 创建华为手机和华为电脑对象
*/
class HuaweiFactory extends ElectronicProductsFactory {
@Override
HuaweiPhone createPhoneFactory() {
return new HuaweiPhone();
}
@Override
HuaweiComputer createComputerFactory() {
return new HuaweiComputer();
}
}
定义 产品抽象类 和 产品实体类
/**
* 电子产品抽象族类
*/
abstract class ElectronicProducts {
abstract void run();
}
/**
* 手机电子产品抽象类
*/
abstract class Phone extends ElectronicProducts {
@Override
abstract void run();
}
/**
* 电脑电子产品抽象类
*/
abstract class Computer extends ElectronicProducts {
@Override
abstract void run();
}
/**
* IPhone 苹果手机实体类
*/
class IPhone extends Phone{
@Override
public void run() {
System.out.println("运行苹果手机");
}
}
/**
* HuaweiPhone 华为手机实体类
*/
class HuaweiPhone extends Phone {
@Override
public void run() {
System.out.println("运行华为手机");
}
}
/**
* AppleComputer 苹果电脑实体类
*/
class AppleComputer extends Computer {
@Override
public void run() {
System.out.println("运行苹果电脑");
}
}
/**
* HuaweiComputer 华为电脑实体类
*/
class HuaweiComputer extends Computer {
@Override
public void run() {
System.out.println("运行华为电脑");
}
}
抽象工厂模式的优缺点
-
将具体产品对象的实例化 推迟到 超级工厂子类实现
-
超级工厂子类可以创建多个具体产品对象
-
新增具体产品类时, 只需要增加 具体产品类 和 具体超级工厂子类. 对于新的具体产品类符合开-闭原则
-
抽象工厂模式 对于新增 抽象产品类, 是需要修改源代码. 因此 抽象工厂模式 对于新的抽象产品类不符合开-闭原则
工厂模式的意义
将实例化对象的代码提取处理, 放到一个类中统一管理和维护, 达到和主项目的依赖关系的解耦, 从而提提高项目的扩展和维护性.
工厂模式的依赖抽象原则
-
创建对象实例时, 不要直接new对象, 而是把这个new类的动作放在一个工厂的方法中, 并返回. 甚至是, 变量不要直接持有具体类的引用
-
不要让类继承具体的类, 而是继承抽象类 或者是 接口
-
不要覆盖类中已经实现的方法