一、对缓存中间件的诉求

1.1 我们为什么需要缓存中间件
我们一直使用关系型数据库作为我们几乎是唯一的数据存储方案。关系型数据库在对复杂结构的数据的组织上、持久性和一致性控制上有巨大的优势。但磁盘数据库无论如何进行查询优化,速度上终究无法和内存读写相提并论。而随着客户数据量越来越大、并发量越来越高、客户场景越来越复杂,我们对数据访问效率的要求也在提高。此时,引入缓存中间件成了我们一定要考虑的事情。

在2022年,一听到缓存中间件,我们首先想到的依然是redis。但我们团队长期以来并没有充分地利用起redis提高系统性能,依然大量依赖于关系型数据库处理数据的存储和读写。为提高系统整体性能,尝试引入新的缓存中间件解决我们的问题。

1.2 缓存的分类
我将缓存解决方案划分为两大类别:弱势缓存和强势缓存。

1.1.1 弱势缓存
第一类,是以Redis为首的弱势缓存。这类缓存强调的是极高的读性能和写性能,一般作为高并发场景下,高速的应用服务和较低速的磁盘数据库之间的缓存。
弱势缓存为提高读写效率,舍弃了强一致性,追求最终一致性,数据结构简单,因此,基于弱势缓存设计的应用系统,通常以磁盘关系型数据库的数据为准,缓存中更倾向于存储一些相对静态稳定的基础数据,用于辅助关系型数据库。对于数据的更新模式,也更偏向于追加,而不是大并发下的频繁更新,应用系统不会完全信任缓存中的数据。这种效率优先,对数据准确性要求不高的方向与如今的互联网行业(尤其是toC领域)的需求十分契合。

1.1.2 强势缓存
而对于业务数据模型较复杂,对数据实时性和准确性要求较高的金融行业、企服行业,更需要的是一个强一致性的数据存储,在引入缓存之前,关系型数据库承担了这一角色。
业务特性导致我们几乎无法接受为了读写效率牺牲数据准确性。复杂业务场景下,我们需要一个强一致性和高实时性的、对数据结构有更强的表达能力的,能描述部分逻辑表达的内存数据库。能满足这些要求的产品,我称其为强势缓存。因为这些特性可以让内存数据库作为我们数据的基准,而让关系型数据库作为一个持久化的备份,进一步降低磁盘的访问率,提高内存的存在感,让一个完整业务流程中的数据流转可以在内存中可靠地完成。

二、什么是Apache Geode
Apache Geode正是满足我给出的强势缓存定义的一款内存数据库产品。它是商业内存数据网格Geofirm的开源版本,已经在金融支付领域和12306等大型订购网站中经受住了考验。

2.1 Apache Geode的架构

2.1.1 通信拓扑

2.1.1.1 点对点

点对点(Peer To Peer)的部署模式没有服务器的概念,所有参与缓存的节点一视同仁,这种部署模式主要用于把缓存嵌入到集群中每个应用节点上。

2.1.1.2 客户端/服务器

客户端/服务器部署下,Apache Geode作为独立的集群服务存在,这样部署的好处是,客户端只选择性地保留一小部分本地缓存,将大部分缓存数据委托给服务集群,节点和节点之间不需要频繁地进行数据分发,也更便于进行扩展。

2.1.2 服务发现
Apache Geode提供了定位器(Locator)进程,为参与缓存的所有成员(Client、Server、其它的Locator)提供其他成员的发现和负载均衡。Locator既可以和其他Geode进程部署在一起,也可以独立部署,独立部署可以更好地保证定位器的可靠性和可用性(因为一旦一起的Geode进程挂了,这个Locator也很难幸免于难)。可以部署多个Locator共同起作用,client连接时可以选择连接到哪个Locator上。

Client可以配置连接到哪个Server服务器,但更合理的配置方式是连接到某个Locator上,由这个Locator为Client分配一个负载较低的Server,Client启动后只会和Locator沟通一次,在获知被分配到的Server的IP和端口之后,每次读写都会直接连接到Server上。

2.1.3 数据存储形式和区域
市面上大部分的内存数据存储,都将数据按键值对的格式进行存放,Geode也是如此。但与Redis等简单的KV不同,Geode将KV数据们按数据区域(Region)进行组织。对于不同的区域可以单独配置(如是否分区或需要副本)。

数据区域可以类比于关系型数据库中的表的概念,是一系列结构相同的数据结构的集合。实际上,在实现上数据区域就是一个ConcurrentMap<K, V>,其键就是一条数据的唯一性标识(类型任意,只要重写了equals和hashcode以便于Region确认键的唯一),其值是一个表达完整数据概念的对象,这样其实也让一条数据中按类的成员又划分出了列的概念。基于这种类似关系型数据库的存储模式,Geode提供了一种类似于SQL的查询语言,称为OQL,并支持多区域查询(类似于连表查询)。
下面是一个OQL查询的小例子:

class DictPlatform implements DataSerializable {

short platformId;
String name;
String status;

}

class TestServiceImpl {

public void query() {
    String queryString = "SELECT dp.platformId, dp.name FROM /dict_platform dp WHERE dp.status >= 0;"
    QueryService queryService = cache.getQueryService();
    Query query = queryService.newQuery(queryString);
    SelectResults results = (SelectResults)query.execute();

    DictPlatform p = (DictPlatform)results.iterator().next();
}

}
2.1.3.1 区域的分布式存储和复制
数据区域可配置的类型主要为 Partitioned , Replicated , Distributed non-replicated , Non-distributed 这四种。下面重点介绍前两种类型。

Partitioned 分区区域
如果某个区域数据量很大,一个成员放不下,可以将这个区域划分为多个bucket,分别存储在不同的server上,为了保证高可用,可以让不同的bucket的副本分布在多个server上,以某个server上的bucket作为master,很类似于Apache Kafka的设计。当存储不够,可以增加新的server,增加新server后的需要发起重平衡,重平衡不需要停机,但可能会导致正在执行的事务失败。
可以从任何一个副本中读取到数据,如果和Client联系的那个Server没有想访问的分区的副本,需要经过server间的一跳,将请求转给目标server。因此,分区的读性能稍差。
Replicated 复制区域
如果某个区域数据量不大,为了提高读性能,复制区域可以将区域中所有数据完整地复制给其它副本,这样所有server中保存着完全相同的数据。
2.1.4 数据量的控制和热点数据
Geode有两种模式控制内存中的数据规模,持久化和失效。但无论如何,热点数据都通过最近最少使用(LRU)算法来判断。

2.1.4.1 持久化
持久化(Persisted) 和 溢出(Overflowed) 是配合使用的两个概念,配置共同使用持久化和溢出之后,所有数据都会被复制到磁盘,在内存中只保留热点数据的值,但所有数据的键都会被保留,以便于确认数据在磁盘上是否存在。内存中数据量达到阈值后(指定条数或内存负载),排在LRU队尾的数据会被溢出到磁盘中(也即删除),当这个键对应的数据又一次被访问到,这个键会被恢复到内存中。
磁盘中的值和内存中的值在应用程序看来没有任何区别。

2.1.4.2 失效
驱逐(Eviction) 和 到期(Expiration) 是可配置的两种数据失效的方式,总的来说,失效就是值当数据满足某些条件时,就从内存中删除掉。

驱逐
驱逐是指当内存中的数据量到达一定阈值时,将LRU队尾的数据销毁。这个阈值可以是条数或占用的内存大小。驱逐和溢出并不是互斥的,溢出实际上是一种不会丢失数据的驱逐。
到期
到期是指按时间销毁掉冷数据,到期有两种计算方式,一种是从数据创建或更新开始计算,被称为TTL(Time to live),另一种是上次被访问开始计算,被称为Idle timeout。第二种更适用于热点数据的存储,第一种更适合于业务要求的定时失效的数据。
三、Apache Geode是否能满足我们的需要

3.1 性能

3.2.1 吞吐量和延迟
读写吞吐量由并发主存储器数据结构和高度优化的分发基础结构提供。 应用程序可以通过同步或异步复制在内存中动态复制数据,以实现高读取吞吐量,或者跨多个系统成员对数据进行分区,以实现高读写吞吐量。 如果数据访问在整个数据集中相当平衡,则数据分区会使聚合吞吐量翻倍。 吞吐量的线性增加仅受骨干网容量的限制。

优化的缓存层最大限度地减少了线程和进程之间的上下文切换。 它管理高度并发结构中的数据,以最大限度地减少争用点。 如果接收器可以跟上,则与对等成员的通信是同步的,这使得数据分发的延迟保持最小。 服务器以序列化形式管理对象图,以减少垃圾收集器的压力。

客户端可以将单个数据请求直接发送到持有数据key的服务器,从而避免多跳以定位已分区的数据。 客户端中的元数据标识正确的服务器。

3.2.3 索引
Geode的查询支持索引以提高查询效率。实际上,Geode维护了一个键和索引值之间关系的数据结构(一般是一颗B树),并支持范围查询。
但和所有的关系型数据库一样,索引能带来收益的前提是良好的索引设计,而且必然会带来写效率的降低。

3.2 CAP
我们知道分布式系统存在CAP不可能三角,即一个分布式系统最多只能同时满足 一致性(Consistency) 、 可用性(Availability) 和 分区容错性(Partition tolerance) 这三项中的两项。下面从一致性、可用性和分区容错性上分析Apache Geode的性能优劣。

一致性
Apache Geode的定位是一款强一致性的内存数据库。一致性的破坏有两种渠道: 副本更新的延迟或失败 和 并发更新 。

对于分区区域,即使该分区配置了冗余的副本,也只允许在主副本上按顺序写入,写入过程中会进行锁定,防止并发更新。对主副本的写入,必须在同步地执行对冗余副本的写入后才算成功。
对于复制区域的写操作,Geode会保证所有的副本都成功执行写入后,才返回成功。Geode并发地执行对多个server的数据分发,但仍会降低写入的效率。
对于复制区域的并发更新可能击中任何一个副本,这样就出现了并发问题。Geode在更新前提供了一致性检查,检测并一致地解决并发和无序更新。这个一致性检查实际上是使用版本号和时间戳来保证,多个更新到来时,只保留最高版本的更新请求。如果有多个相同版本的更新请求,每个成员都有一个资格ID,最高资格ID的成员的更新生效。
可用性
对于复制区域,每个server上都保存着相同的数据,因此一个server宕机完全不影响数据的正常读写。
对于分区区域,为了保证高可用性,可以对分区设置冗余副本,在某个server宕机后,Geode会自动地将副本标记为master,当可用的副本数量到达一个阈值后,Geode会启动一个线程,启用一个新的server作为新的副本的据点,将所有数据复制到这个新的server上并标记为可用副本。这个过程是否立即执行是可配置的,也可以等一会看宕机的server是否能恢复。
上面提到的持久化能力也提高了区域的灾备能力,Geode允许以分区为维度进行落盘持久化,并在一个新的server启动后,将磁盘中的内容恢复到内存中。

分区容错性
Apache Geode作为分布式内存数据库,天然地满足了分区容错性。

3.3 复杂业务场景的需要

3.2.1 事务支持
Geode提供了ACID事务的能力,但基于乐观锁,重新定义了ACID。

原子性
乐观事务通过使用预约系统提供原子性并实现速度,而不是使用传统的两阶段锁行关系数据库技术。这种保留阻止了其他交叉事务的完成,允许提交检查冲突,并在对数据进行更改之前以全有或全无的方式保留资源。在本地和远程完成所有更改之后,将释放预订。在预订系统中,交叉事务将被简单地丢弃。避免了获取锁的序列化。
一致性
一致性要求在事务中编写的数据必须遵守为受影响区域建立的键和值约束。请注意,事务的有效性是应用程序的责任。
隔离
隔离是事务状态对系统组件可见的级别。Geode事务具有可重复的读隔离。一旦为给定的键读取提交的值,它总是返回相同的值。如果事务中的写操作删除了已读取的键的值,则后续的读操作将返回事务引用。
默认配置在流程线程级别隔离事务。当一个事务正在进行时,它的更改只在运行该事务的线程中可见。同一进程中的其他线程和其他进程中的线程在提交操作开始之前不能看到更改。在开始提交之后,更改在缓存中是可见的,但是访问更改数据的其他线程可能会看到事务的部分结果,从而导致脏读。但可以通过修改配置避免脏读。
持久性
关系数据库通过使用磁盘存储进行恢复和事务日志记录来提供持久性。Geode针对性能进行了优化,不支持事务的磁盘持久性。
3.2.2 Functions
可以在Geode服务中注册一些函数,应用程序只需要发送函数的名称就可以执行函数内容。

3.2.3 连续查询
这是一个类似于消息队列的机制,Client向Server发布一个OQL查询,当Server执行了会导致这个OQL的查询结果发生变更的事件后,会将这个事件和新的结果通知给Client,Client可以基于此做一些特殊的操作。比如我们在缓存中管理库存量,发布一个连续查询 "SELECT * FROM /stock s WHERE s.stock_num + +s.purchase_num < 100",当这个查询收到值时,就向采购领域发布采购预警事件。

3.2.4 异构
Geode提供了多种序列化方式,当想要在异构的系统中使用Geode时,可以不使用Java的序列化,而是其它更通用的序列化方式,如PDX或DataSerializer,PDX可以不用反序列化整个数据对象,就读到其中的字段的值,还能兼容多版本的对象;DataSerializable则提供了更快速的序列化。

日志驱动
日志可以通过驱动支持不同的方式写入,默认日志会记录到文件中,系统已经内置的写入驱动包括 File、Socket,如果要临时关闭日志写入,可以设置日志类型为Test即可,例如:

'log' => [

// 可以临时关闭日志写入
'type'  => 'test',

],
File驱动
日志的记录方式默认是File方式,可以通过驱动的方式来扩展支持更多的记录方式。

记录方式由log.type参数配置,例如:

'log' => [

// 日志记录方式,支持 file socket
'type' => 'File',
//日志保存目录
'path' => LOG_PATH,
//单个日志文件的大小限制,超过后会自动记录到第二个文件
'file_size'     =>2097152,
//日志的时间格式,默认是` c `
'time_format'   =>'c'

],
为了避免同一个目录下面的日志文件过多的性能问题,file方式记录的日志文件会自动生成日期子目录。

Scoket驱动
Socket驱动配置,具体参考后面的远程调试部分。

其他驱动
thinkphp5.0支持SAE驱动的扩展,具体参考“SAE”章节

每个日志记录方式需要对应一个日志驱动文件,例如File方式记录,对应的驱动文件是library/think/log/driver/File.php。

关闭日志
系统并未提供关闭日志的方法,但有两种方式可以关闭日志的写入,第一种方式是设置日志类型为test,即可不写入任何日志。第二种方式是后面会提到的设置日志记录级别。

日志写入
版本 新增功能
5.0.4 命令行模式下日志自动定时写入
5.0.4 debug日志类型仅用于调试模式记录
手动记录
一般情况下,系统的日志记录是自动的,无需手动记录,但是某些时候也需要手动记录日志信息,Log类提供了3个方法用于记录日志。

方法 描述
Log::record() 记录日志信息到内存
Log::save() 把保存在内存中的日志信息(用指定的记录方式)写入
Log::write() 实时写入一条日志信息
由于系统在请求结束后会自动调用Log::save方法,所以通常,你只需要调用Log::record记录日志信息即可。

record方法用法如下:

Log::record('测试日志信息');
默认的话记录的日志级别是INFO,也可以指定日志级别:

Log::record('测试日志信息,这是警告级别','notice');
采用record方法记录的日志信息不是实时保存的,如果需要实时记录的话,可以采用write方法,例如:

Log::write('测试日志信息,这是警告级别,并且实时写入','notice');
V5.0.4+版本开始,为避免内容溢出,在命令行下面执行的话 日志信息会定时自动写入。

日志级别
ThinkPHP对系统的日志按照级别来分类,并且这个日志级别完全可以自己定义,系统内部使用的级别包括:

log 常规日志,用于记录日志
error 错误,一般会导致程序的终止
notice 警告,程序可以运行但是还不够完美的错误
info 信息,程序输出信息
debug 调试,用于调试信息
sql SQL语句,用于SQL记录,只在数据库的调试模式开启时有效
系统提供了不同日志级别的快速记录方法,例如:

Log::error('错误信息');
Log::info('日志信息');
// 和下面的用法等效
Log::record('错误信息','error');
Log::record('日志信息','info');
还封装了一个助手函数用于日志记录,例如:

trace('错误信息','error');
trace('日志信息','info');
也支持指定级别日志的输入,需要配置信息:

'log' => [

'type'  => 'File',
// 日志记录级别,使用数组表示
'level' => ['error'],

],

消息队列解决方案:
RabbitMQ:RabbitMQ 是一个开源的 AMQP 消息中间件,提供了高可用、高并发、高吞吐量的消息队列服务。使用 PHP 语言可以通过 AMQP 扩展连接到 RabbitMQ 服务,实现消息的生产、消费和处理。
Redis 队列:Redis 是一个开源的内存数据库,提供了高速、高性能的数据结构服务,如字符串、列表、集合、哈希表等。通过使用 Redis 的 List 类型实现队列,可以实现消息的生产、消费和处理。
Kafka:Kafka 是一个开源的分布式消息系统,具有高吞吐量、低延迟、高可靠性的特点。使用 PHP 语言可以通过 Kafka 客户端扩展连接到 Kafka 服务,实现消息的生产、消费和处理。
Beanstalkd:Beanstalkd 是一个轻量级的消息队列服务,具有高可用、高可靠性、高性能的特点。使用 PHP 语言可以通过 Pheanstalk 客户端扩展连接到 Beanstalkd 服务,实现消息的生产、消费和处理。
ZeroMQ:ZeroMQ 是一个开源的高性能消息传递库,提供了多种消息传递模式,如 publish/subscribe、request/reply、pipeline 等。使用 PHP 语言可以通过 ZeroMQ 扩展连接到 ZeroMQ 服务,实现消息的生产、消费和处理。
2,协程解决方案 + 异步 IO解决方案:
Swoole:Swoole 是一个基于 PHP 语言的高性能异步、协程网络通信框架,提供了协程、异步 IO、网络通信、定时器等功能,支持 HTTP、WebSocket、TCP 等多种协议。使用 Swoole 可以实现 PHP 的协程特性,提高 PHP 应用的性能和并发能力。
Coroutine:Coroutine 是一个轻量级的协程库,提供了协程、异步 IO、网络通信等功能,支持 HTTP、WebSocket、TCP 等多种协议。使用 Coroutine 可以方便地实现 PHP 的协程特性,以提高 PHP 应用的性能和并发能力。
PHP-FPM 协程:PHP-FPM 是 PHP 的 FastCGI 进程管理器,支持使用协程池实现协程,提高 PHP 应用的性能和并发能力。使用 PHP-FPM 协程需要在 PHP-FPM 配置文件中开启协程池,并通过特定的函数调用实现协程的切换和调度。
Amp:Amp 是一个基于 PHP 语言的异步编程框架,提供了协程、异步 IO、Promise、网络通信等功能,支持 HTTP、WebSocket、TCP 等多种协议。使用 Amp 可以方便地实现 PHP 的协程特性,以提高 PHP 应用的性能和并发能力。
ReactPHP:ReactPHP 是一个基于 PHP 语言的异步编程框架,提供了异步 IO、网络通信等功能,支持 HTTP、WebSocket、TCP 等多种协议。使用 ReactPHP 可以方便地实现 PHP 的* 异步 IO 特性,以提高 PHP 应用的性能和并发能力。
Workerman:Workerman是一个高性能的PHP异步网络框架,支持TCP/UDP/Unix Socket协议、异步IO、事件驱动等,可以用于开发高并发、实时通信的应用程序。
PHP-PM:PHP-PM是一个基于PHP的进程管理器,使用了Swoole扩展提供异步I/O和协程支持,可以快速、稳定地运行PHP应用程序。
3,缓存解决方案:
Memcached:Memcached 是一款开源的高性能分布式内存缓存系统,支持多种语言,包括 PHP,广泛应用于 Web 应用程序中,被认为是 PHP 缓存的首选方案之一。Memcached 的主要优点是速度快、稳定可靠,可扩展性好,能够有效地缓解 Web 应用程序中的数据库压力。
Redis:Redis 是一款开源的高性能键值对存储系统,支持多种数据结构,包括字符串、列表、哈希表、集合等,可以作为缓存、消息队列、分布式锁等多种用途。Redis 的主要优点是速度快、性能稳定、可靠性高、支持多种数据结构和高级特性,被广泛应用于 Web 应用程序中,也是 PHP 缓存的常用方案之一。
APC(Alternative PHP Cache):APC 是 PHP 官方推荐的一款缓存扩展,能够缓存 PHP 脚本文件,提高 PHP 应用程序的性能和响应速度。APC 的主要优点是易于安装和使用,对于小型的 Web 应用程序而言,可以提供一定的性能提升。
Xcache:Xcache 是一款开源的 PHP 缓存扩展,支持 PHP 5.x 和 PHP 7.x,具有性能高、稳定性好、支持多种缓存策略等优点,被广泛应用于 PHP 应用程序中。
OpCache:OpCache 是 PHP 官方推荐的一款缓存扩展,用于缓存 PHP 脚本的字节码,提高 PHP 应用程序的性能和响应速度。OpCache 的主要优点是易于安装和使用,对于小型的 Web 应用程序而言,可以提供一定的性能提升。
4,分布式解决方案:
Apache Thrift:Apache Thrift是一种可扩展、跨语言的分布式服务框架,支持多种编程语言,包括PHP、Java、Python等。它提供了高效的远程服务调用和跨语言的数据传输。
gRPC:gRPC是Google开发的高性能、开源的RPC框架,支持多种编程语言,包括PHP、Java、Python等。它基于HTTP/2协议,支持多种数据序列化格式,如Protobuf和JSON。
ZooKeeper:ZooKeeper是一种高可用性、高性能的分布式协调服务,主要用于构建分布式系统。它提供了高效的协调服务,支持多种编程语言,包括PHP、Java、Python等。
Redis:Redis是一种高性能、内存型的键值存储系统,支持多种数据结构,如字符串、哈希表、列表等。它提供了分布式集群和主从复制等功能,可以用于构建分布式应用。
Apache Kafka:Apache Kafka是一种分布式的流处理平台,主要用于构建实时数据流应用。它支持高吞吐量的消息传输,提供了高效的数据传输和持久化存储。
Eureka:Eureka是Netflix开发的一种分布式服务发现框架,主要用于构建微服务架构。它提供了高可用性、高性能的服务注册和发现功能,可以用于构建分布式系统。
Consul:Consul是一种分布式服务发现和配置管理系统,支持多种数据中心和跨数据中心的服务发现和故障转移。它提供了高可用性、高性能的服务注册和发现功能,可以用于构建分布式系统。
5,服务器集群解决方案:
LVS:Linux Virtual Server(LVS)是一个高可用性、高性能的服务器集群解决方案,可以提供负载均衡和高可用性服务。
Nginx:Nginx是一款高性能的Web服务器和反向代理服务器,可以通过反向代理和负载均衡实现服务器集群。
HAProxy:HAProxy是一款高性能的TCP/HTTP负载均衡器,可以实现服务器集群、高可用性、高并发等功能。
Apache:Apache是一款流行的Web服务器,可以通过模块扩展实现负载均衡和服务器集群功能。
MySQL Cluster:MySQL Cluster是MySQL提供的高可用性、高性能、可扩展的数据库解决方案,可以实现数据库集群。
6,分表分库解决方案:
分表:将单个数据表中的数据按照某种规则拆分到多个数据表中,可以提高单个数据表的查询效率和数据存储的性能。
分库:将整个数据库按照某种规则划分成多个子库,每个子库可以放置不同的数据表,可以提高数据库整体的扩展性和性能。

Sharding中间件:Sharding中间件是一种常见的分库分表方案,可以将数据分散到多个数据库中,同时也可以支持分表,将单个表的数据分散到多个数据表中。
Proxy层:通过在应用程序和数据库之间增加一层代理层,将请求分散到多个数据库中,可以实现分库分表的效果。
7,数据库索引解决方案:
B-tree索引:B-tree索引是一种常用的索引类型,可以加速等值查询、范围查询和排序等操作,常用于MySQL、PostgreSQL等数据库。
Hash索引:Hash索引是一种基于哈希表的索引方式,可以快速的进行等值查询,但不支持范围查询和排序等操作,常用于Memcached、Redis等内存型数据库。
Full-Text索引:Full-Text索引是一种用于全文搜索的索引方式,可以加速全文搜索和相关性排序等操作,常用于MySQL、PostgreSQL等数据库。
空间索引:空间索引是一种用于空间数据的索引方式,可以加速空间数据的查询和分析,常用于GIS系统等。
8,PHPCDN技术解决方案:
静态资源CDN加速:将PHP应用程序中的静态资源(如CSS、JS、图片等)上传到CDN服务商,并使用CDN加速访问,可以减轻PHP应用程序的服务器负载,提升访问速度和用户体验。
动态内容缓存:使用CDN服务商提供的动态内容缓存功能,可以将PHP应用程序中的动态内容缓存到CDN节点中,减轻PHP应用程序的服务器负载,提升访问速度和用户体验。
智能路由选择:通过CDN服务商提供的智能路由选择功能,可以根据用户的地理位置、网络环境等因素,自动选择最优的CDN节点进行访问,提升访问速度和稳定性。
防DDoS攻击:通过CDN服务商提供的DDoS防护功能,可以有效防御DDoS攻击,提升PHP应用程序的可用性和安全性。

Nginx:Nginx是一种高性能的Web服务器和反向代理服务器,可以集成CDN模块来实现CDN功能。
Varnish Cache:Varnish Cache是一种开源的Web加速器,可以将常见的静态资源缓存到本地服务器上,从而提高网站的响应速度。
Cloudflare:Cloudflare是一种云端CDN服务提供商,可以通过将网站的DNS解析到Cloudflare来实现CDN功能。
Akamai:Akamai是一种全球性CDN服务提供商,可以提供全球性的CDN服务,从而提高网站的访问速度。
Amazon CloudFront:Amazon CloudFront是一种AWS的CDN服务,可以通过将网站的内容存储到亚马逊的云端服务器上来实现CDN功能。
9,高可用架构设计
主从复制:主从复制是将数据从主数据库实时复制到一个或多个从数据库的过程。PHP 框架 Laravel 中就实现了主从复制机制。

第一步: 首先,需要在 config/database.php 文件中配置主库和从库的连接信息,如下所示:
点击查看代码

第二步: 然后,在使用数据库连接的地方,可以指定使用主库或从库连接,如下所示:
点击查看代码

补充: 可以使用 Laravel 自带的 DB 类或者第三方库来实现自动化的主从切换和负载均衡,如 laravel-master-slave 库。
双主架构:双主架构是指一个集群中有两个主节点,每个节点都能够进行写操作。当一个节点发生故障时,另一个节点可以接替它的工作。PHP 框架 Yii2 中支持双主架构。

第一步:在 Yii2 的配置文件 config/db.php 中配置主数据库和备份数据库的连接信息:
点击查看代码

第二步:在代码中进行双主架构的读写分离:
点击查看代码

补充:配置负载均衡,通过负载均衡将流量分配到多个服务器上,从而实现高可用和负载均衡。可以使用第三方软件如 Nginx 或 LVS 来实现。
读写分离:读写分离是指将读和写操作分离到不同的数据库中,可以提高数据库的性能和稳定性。PHP 框架 ThinkPHP 中支持读写分离。

第一步:在数据库配置文件中定义主库和从库的连接参数;
点击查看代码

第二步:定义一个数据库操作类,用于连接主库或从库
点击查看代码

第三步:在需要读写分离的地方,通过实例化Db类来连接数据库
点击查看代码

补充:主从库之间的数据同步需要通过第三方工具实现。在写入数据后,需要在主库和从库之间进行同步,否则在从库上可能无法查询到最新写入的数据。部分需要严格控制数据一致性的场景,例如金融、医疗等领域,读写分离可能不是最佳的解决方案,需要使用主从复制或者双主架构来实现数据同步和高可用。
多数据中心:多数据中心是指将数据分布在多个地理位置的数据中心中,可以提高数据的可用性和可靠性。PHP 框架 Symfony 中支持多数据中心架构。
方案一:使用Doctrine ORM框架以及其提供的分布式事务支持。通过使用分布式事务,可以在多个数据中心之间实现数据的同步和一致性。此外,可以使用Symfony的事件和消息系统,实现跨数据中心的异步通信。
方案二:使用Symfony Messenger组件,该组件提供了一个灵活的消息传递系统。可以使用该组件来实现数据的异步同步和处理。同样地,可以通过事件和消息传递来实现跨数据中心的通信。
10,安全架构设计
1),身份认证:使用 PHP 语言和框架(如 Laravel 或 Yii2)实现基于角色的访问控制(RBAC)系统,包括用户登录、注册、找回密码等功能,使用加盐哈希加密用户密码,保护用户隐私。
基于 session 的身份认证:通过保存用户的认证状态信息在服务器端的 session 中,判断用户是否已经登录。通常会在登录时设置 session,注销时删除 session。
基于 cookie 的身份认证:通过将用户的认证状态信息保存在客户端的 cookie 中,判断用户是否已经登录。与 session 方式相比,可以减轻服务器的压力,但需要注意 cookie 安全问题。
基于 token 的身份认证:通过给用户分配一个唯一的 token,用于标识用户身份和验证用户身份。通常会在用户登录时生成 token,并在后续的请求中携带 token 进行身份验证。
基于 OAuth2.0 的身份认证:OAuth2.0 是一种授权协议,可以用于实现第三方授权登录。用户可以通过第三方应用程序进行登录认证,同时保护用户的隐私和安全。
基于 OpenID Connect 的身份认证:OpenID Connect 是一种基于 OAuth2.0 的身份认证协议,可以用于实现单点登录和用户身份认证。用户可以通过 OpenID Connect 进行跨应用程序的身份认证。
基于 SAML 的身份认证:SAML 是一种基于 XML 的身份认证协议,可以用于实现企业间的单点登录和用户身份认证。用户可以通过 SAML 进行跨组织的身份认证。

内置规则
系统内置的验证规则如下:

格式验证类
require
验证某个字段必须,例如:

'name'=>'require'
number 或者 integer
验证某个字段的值是否为数字(采用filter_var验证),例如:

'num'=>'number'
float
验证某个字段的值是否为浮点数字(采用filter_var验证),例如:

'num'=>'float'
boolean
验证某个字段的值是否为布尔值(采用filter_var验证),例如:

'num'=>'boolean'
email
验证某个字段的值是否为email地址(采用filter_var验证),例如:

'email'=>'email'
array
验证某个字段的值是否为数组,例如:

'info'=>'array'
accepted
验证某个字段是否为为 yes, on, 或是 1。这在确认"服务条款"是否同意时很有用,例如:

'accept'=>'accepted'
date
验证值是否为有效的日期,例如:

'date'=>'date'
会对日期值进行strtotime后进行判断。

alpha
验证某个字段的值是否为字母,例如:

'name'=>'alpha'
alphaNum
验证某个字段的值是否为字母和数字,例如:

'name'=>'alphaNum'
alphaDash
验证某个字段的值是否为字母和数字,下划线_及破折号-,例如:

'name'=>'alphaDash'
chs
验证某个字段的值只能是汉字,例如:

'name'=>'chs'
chsAlpha
验证某个字段的值只能是汉字、字母,例如:

'name'=>'chsAlpha'
chsAlphaNum
验证某个字段的值只能是汉字、字母和数字,例如:

'name'=>'chsAlphaNum'
chsDash
验证某个字段的值只能是汉字、字母、数字和下划线_及破折号-,例如:

'name'=>'chsDash'
activeUrl
验证某个字段的值是否为有效的域名或者IP,例如:

'host'=>'activeUrl'
url
验证某个字段的值是否为有效的URL地址(采用filter_var验证),例如:

'url'=>'url'
ip
验证某个字段的值是否为有效的IP地址(采用filter_var验证),例如:

'ip'=>'ip'
支持验证ipv4和ipv6格式的IP地址。

dateFormat:format
验证某个字段的值是否为指定格式的日期,例如:

'create_time'=>'dateFormat:y-m-d'
长度和区间验证类
in
验证某个字段的值是否在某个范围,例如:

'num'=>'in:1,2,3'
notIn
验证某个字段的值不在某个范围,例如:

'num'=>'notIn:1,2,3'
between
验证某个字段的值是否在某个区间,例如:

'num'=>'between:1,10'
notBetween
验证某个字段的值不在某个范围,例如:

'num'=>'notBetween:1,10'
length:num1,num2
验证某个字段的值的长度是否在某个范围,例如:

'name'=>'length:4,25'
或者指定长度

'name'=>'length:4'
如果验证的数据是数组,则判断数组的长度。
如果验证的数据是File对象,则判断文件的大小。

max:number
验证某个字段的值的最大长度,例如:

'name'=>'max:25'
如果验证的数据是数组,则判断数组的长度。
如果验证的数据是File对象,则判断文件的大小。

min:number
验证某个字段的值的最小长度,例如:

'name'=>'min:5'
如果验证的数据是数组,则判断数组的长度。
如果验证的数据是File对象,则判断文件的大小。

after:日期
验证某个字段的值是否在某个日期之后,例如:

'begin_time' => 'after:2016-3-18',
before:日期
验证某个字段的值是否在某个日期之前,例如:

'end_time' => 'before:2016-10-01',
expire:开始时间,结束时间
验证当前操作(注意不是某个值)是否在某个有效日期之内,例如:

'expire_time' => 'expire:2016-2-1,2016-10-01',
allowIp:allow1,allow2,...
验证当前请求的IP是否在某个范围,例如:

'name' => 'allowIp:114.45.4.55',
该规则可以用于某个后台的访问权限

denyIp:allow1,allow2,...
验证当前请求的IP是否禁止访问,例如:

'name' => 'denyIp:114.45.4.55',
字段比较类
confirm
验证某个字段是否和另外一个字段的值一致,例如:

'repassword'=>'require|confirm:password'
5.0.4+版本开始,增加了字段自动匹配验证规则,如password和password_confirm是自动相互验证的,只需要使用

'password'=>'require|confirm'
会自动验证和password_confirm进行字段比较是否一致,反之亦然。

different
验证某个字段是否和另外一个字段的值不一致,例如:

'name'=>'require|different:account'
egt 或者 >=
验证是否大于等于某个值,例如:

'score'=>'egt:60'
'num'=>'>=:100'
gt 或者 >
验证是否大于某个值,例如:

'score'=>'gt:60'
'num'=>'>:100'
elt 或者 <=
验证是否小于等于某个值,例如:

'score'=>'elt:100'
'num'=>'<=:100'
lt 或者 <
验证是否小于某个值,例如:

'score'=>'lt:100'
'num'=>'<:100'
eq 或者 = 或者 same
验证是否等于某个值,例如:

'score'=>'eq:100'
'num'=>'=:100'
'num'=>'same:100'
filter验证
支持使用filter_var进行验证,例如:

'ip'=>'filter:validate_ip'
正则验证
支持直接使用正则验证,例如:

'zip'=>'d{6}',
// 或者
'zip'=>'regex:d{6}',
如果你的正则表达式中包含有|符号的话,必须使用数组方式定义。

'accepted'=>['regex'=>'/^(yes|on|1)$/i'],
也可以实现预定义正则表达式后直接调用,例如:

上传验证
file
验证是否是一个上传文件

image:width,height,type
验证是否是一个图像文件,width height和type都是可选,width和height必须同时定义。

fileExt:允许的文件后缀
验证上传文件后缀

fileMime:允许的文件类型
验证上传文件类型

fileSize:允许的文件字节大小
验证上传文件大小

行为验证
使用行为验证数据,例如:

'data'=>'behavior:appindexbehaviorCheck'
其它验证
unique:table,field,except,pk
版本 调整功能
5.0.5 支持指定完整模型类 并且默认会优先检测模型类是否存在 不存在则检测数据表
验证当前请求的字段值是否为唯一的,例如:

// 表示验证name字段的值是否在user表(不包含前缀)中唯一
'name' => 'unique:user',
// 验证其他字段
'name' => 'unique:user,account',
// 排除某个主键值
'name' => 'unique:user,account,10',
// 指定某个主键值排除
'name' => 'unique:user,account,10,user_id',
如果需要对复杂的条件验证唯一,可以使用下面的方式:

// 多个字段验证唯一验证条件
'name' => 'unique:user,status^account',
// 复杂验证条件
'name' => 'unique:user,status=1&account='.$data['account'],
requireIf:field,value
验证某个字段的值等于某个值的时候必须,例如:

// 当account的值等于1的时候 password必须
'password'=>'requireIf:account,1'
requireWith:field
验证某个字段有值的时候必须,例如:

// 当account有值的时候password字段必须
'password'=>'requireWith:account'