数据层的多租户综述
多租户(Multi Tenancy/Tenant)是一种软件架构,其定义是:
在一台服务器上运行单个应用实例,它为多个租户提供服务。
在SaaS实施过程中,有一个显著的考量点,就是如何对应用数据进行设计,以支持多租户,而这种设计的思路,是要在数据的共享、安全隔离和性能间取得平衡。
传统的应用,仅仅服务于单个租户,数据库多部署在企业内部网络环境,对于数据拥有者来说,这些数据是自己“私有”的,它符合自己所定义的全部安全标准。而在云计算时代,随着应用本身被放到云端,导致数据层也经常被公开化,但租户对数据安全性的要求,并不因之下降。同时,多租户应用在租户数量增多的情况下,会比单租户应用面临更多的性能压力。本文即对这个主题进行探讨:多租户在数据层的框架如何在共享、安全与性能间进行取舍,同时了解一下市面上一些常见的数据厂商怎样实现这部分内容。
常见的三种模式
在 MSDN 的这篇文章 Multi-Tenant Data Architecture 中,系统的总结了数据层的三种多租户架构:
独立数据库
共享数据库、独立 Schema
共享数据库、共享 Schema、共享数据表
独立数据库是一个租户独享一个数据库实例,它提供了最强的分离度,租户的数据彼此物理不可见,备份与恢复都很灵活;共享数据库、独立 Schema 将每个租户关联到同一个数据库的不同 Schema,租户间数据彼此逻辑不可见,上层应用程序的实现和独立数据库一样简单,但备份恢复稍显复杂; 最后一种模式则是租户数据在数据表级别实现共享,它提供了最低的成本,但引入了额外的编程复杂性(程序的数据访问需要用 tenantId 来区分不同租户),备份与恢复也更复杂。这三种模式的特点可以用一张图来概括:
图 1. 三种部署模式的异同
上图所总结的是一般性的结论,而在常规场景下需要综合考虑才能决定那种方式是合适的。例如,在占用成本上,认为独立数据库会高,共享模式较低。但如果考虑到大租户潜在的数据扩展需求,有时也许会有相反的成本耗用结论。
而多租户采用的选择,主要是成本原因,对于多数场景而言,共享度越高,软硬件资源的利用效率更好,成本也更低。但同时也要解决好租户资源共享和隔离带来的安全与性能、扩展性等问题。毕竟,也有客户无法满意于将数据与其他租户放在共享资源中。
目前市面上各类数据厂商在多租户的支持上,大抵都是遵循上文所述的这几类模式,或者混合了几种策略,这部分内容将在下面介绍。
JPA Provider
JSR 338 定义了 JPA 规范 2.1,但如我们已经了解到的,Oracle 把多租户的多数特性推迟到了 Java EE 8 中。尽管这些曾经在 JavaOne 大会中有过演示,但无论是在 JPA 2.0(JSR 317)还是 2.1 规范中,都依然没有明文提及多租户。不过这并不妨碍一些 JPA provider 在这部分领域的实现,Hibernate 和 EclipseLink 已提供了全部或部分的多租户数据层的解决方案。
Hibernate 是当今最为流行的开源的对象关系映射(ORM)实现,并能很好地和 Spring 等框架集成,目前 Hibernate 支持多租户的独立数据库和独立 Schema 模式。EclipseLink 也是企业级数据持久层JPA标准的参考实现,对最新 JPA2.1 完整支持,在目前 JPA 标准尚未引入多租户概念之际,已对多租户支持完好,其前身是诞生已久、功能丰富的对象关系映射工具 Oracle TopLink。因此本文采用 Hibernate 和 EclipseLink 对多租户数据层进行分析。
Hibernate
Hibernate 是一个开放源代码的对象/关系映射框架和查询服务。它对 JDBC 进行了轻量级的对象封装,负责从 Java 类映射到数据库表,并从 Java 数据类型映射到 SQL 数据类型。在 4.0 版本 Hibenate 开始支持多租户架构——对不同租户使用独立数据库或独立 Sechma,并计划在 5.0 中支持共享数据表模式。
在 Hibernate 4.0 中的多租户模式有三种,通过 hibernate.multiTenancy 属性有下面几种配置:
NONE:非多租户,为默认值。
SCHEMA:一个租户一个 Schema。
DATABASE:一个租户一个 database。
DISCRIMINATOR:租户共享数据表。计划在 Hibernate5 中实现。
模式1:独立数据库
如果是独立数据库,每个租户的数据保存在物理上独立的数据库实例。JDBC 连接将指向具体的每个数据库,一个租户对应一个数据库实例。在 Hibernate 中,这种模式可以通过实现 MultiTenantConnectionProvider 接口或继承 AbstractMultiTenantConnectionProvider 类等方式来实现。三种模式中它的共享性最低,因此本文重点讨论以下两种模式。
模式 2:共享数据库,独立 Schema
对于共享数据库,独立 Schema。所有的租户共享一个数据库实例,但是他们拥有独立的 Schema 或 Catalog,本文将以多租户酒店管理系统为案例说明 Hibernate 对多租户的支持和用使用方法。
图 2. guest 表结构
这是酒店客户信息表,我们仅以此表对这种模式进行说明,使用相同的表结构在 MySQL 中创建 DATABASE hotel_1 和 hotel_2。基于 Schema 的多租户模式,需要在 Hibernate 配置文件 Hibernate.cfg.xml 中设置 hibernate.multiTenancy 等相关属性。
清单 1. 配置文件 Hibernate.cfg.xml
jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8
root
com.mysql.jdbc.Driver
org.hibernate.dialect.MySQLInnoDBDialect
false
false
false
SCHEMA
hotel.dao.hibernate.TenantIdResolver
hotel.dao.hibernate.SchemaBasedMultiTenantConnectionProvider
属性规定了一个合约,以使 Hibernate 能够解析出应用当前的 tenantId,该类必须实现 CurrentTenantIdentifierResolver 接口,通常我们可以从登录信息中获得 tenatId。
清单 2. 获取当前 tenantId
public class TenantIdResolver implements CurrentTenantIdentifierResolver {
public String resolveCurrentTenantIdentifier() {
return Login.getTenantId();
}
}
< hibernate.multi_tenant_connection_provider> 属性指定了 ConnectionProvider,即 Hibernate 需要知道如何以租户特有的方式获取数据连接,SchemaBasedMultiTenantConnectionProvider 类实现了MultiTenantConnectionProvider 接口,根据 tenantIdentifier 获得相应的连接。在实际应用中,可结合使用 JNDI DataSource 技术获取连接以提高性能。
清单 3. 以租户特有的方式获取数据库连接
public class SchemaBasedMultiTenantConnectionProvider
implements MultiTenantConnectionProvider, Stoppable,
Configurable, ServiceRegistryAwareService {
private final DriverManagerConnectionProviderImpl connectionProvider
= new DriverManagerConnectionProviderImpl();
@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
final Connection connection = connectionProvider.getConnection();
connection.createStatement().execute("USE " + tenantIdentifier);
return connection;
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection)
throws SQLException {
connection.createStatement().execute("USE test");
connectionProvider.closeConnection(connection);
}
……
}
与表 guest 对应的 POJO 类 Guest,其中主要是一些 getter 和 setter方法。
清单 4. POJO 类 Guest
@Table(name = "guest")
public class Guest {
private Integer id;
private String name;
private String telephone;
private String address;
private String email;
//getters and setters
……
}
我们使用 ServiceSchemaBasedMain.java 来进行测试,并假设了一些数据以方便演示,如当有不同租户的管理员登录后分别进行添加客户的操作。
清单 5. 测试类 ServiceSchemaBasedMain
public class ServiceSchemaBasedMain {
public static void main(String[] args) {
Session session = null;
Guest guest =null;
List list = null;
Transaction tx = null;
System.out.println("======== 租户 hotel_1 ========");
Login.setTenantId("hotel_1");
session = sessionFactory.openSession();
tx = session.beginTransaction();
guest = new Guest();
guest.setName("张三");
guest.setTelephone("56785678");
guest.setAddress("上海市张扬路88号");
guest.setEmail("[email protected] ");
session.saveOrUpdate(guest);
list = session.createCriteria(Guest.class).list();
for (Guest gue : list) {
System.out.println(gue.toString());
}
tx.commit();
session.close();
System.out.println("======== 租户 hotel_2 ========");
Login.setTenantId("hotel_2");
session = sessionFactory.openSession();
tx = session.beginTransaction();
guest = new Guest();
guest.setName("李四");
guest.setTelephone("23452345");
guest.setAddress("上海市南京路100号");
guest.setEmail("[email protected] ");
session.saveOrUpdate(guest);
list = session.createCriteria(Guest.class).list();
for (Guest gue : list) {
System.out.println(gue.toString());
}
tx.commit();
session.close();
}
}
清单 6. 运行程序 ServiceSchemaBasedMain 的输出
======== 租户 hotel_1 ========
Guest [id=1, name=Victor, telephone=56008888, address=上海科苑路399号, [email protected] ]
Guest [id=2, name=Jacky, telephone=66668822, address=上海金科路28号, [email protected] ]
Guest [id=3, name=张三, telephone=56785678, address=上海市张扬路88号, [email protected] ]
======== 租户 hotel_2 ========
Guest [id=1, name=Anton, telephone=33355566, address=上海南京路8号, [email protected] ]
Guest [id=2, name=Gus, telephone=33355566, address=北京大道3号, [email protected] ]
Guest [id=3, name=李四, telephone=23452345, address=上海市南京路100号, [email protected] ]
模式3:共享数据库、共享 Schema、共享数据表
在这种情况下,所有租户共享数据表存放数据,不同租户的数据通过 tenant_id 鉴别器来区分。但目前的 Hibernate 4 还不支持这个多租户鉴别器策略,要在 5.0 才支持。但我们是否有可选的替代方案呢?答案是使用 Hibernate Filter.
为了区分多个租户,我在 Schema 的每个数据表需要添加一个字段 tenant_id 以判定数据是属于哪个租户的。
图 3. 共享 Schema、共享数据表案例 E-R 图
根据上图在 MySQL 中创建 DATABASE hotel。
我们在 OR-Mapping 配置文件中使用了 Filter,以便在进行数据查询时,会根据 tenant_id 自动查询出该租户所拥有的数据。
清单 7. 对象关系映射文件 Room.hbm.xml
接下来我们在 HibernateUtil 类中通过 ThreadLocal 存放和获取 Hibernate Session,并将用户登录信息中的 tenantId 设置为 tenantFilterParam 的参数值。
清单 8. 获取 Hibernate Session 的工具类 HibernateUtil
不过 Filter 只是有助于我们读取数据时显示地忽略掉 tenantId,但在进行数据插入的时候,我们还是不得不显式设置相应 tenantId 才能进行持久化。这种状况只能在 Hibernate5 版本中得到根本改变。
清单 9. 运行程序 HotelServiceMain 输出
多租户下的 Hibernate 缓存
基于独立 Schema 模式的多租户实现,其数据表无需额外的 tenant_id。通过 ConnectionProvider 来取得所需的 JDBC 连接,对其来说一级缓存(Session 级别的缓存)是安全的可用的,一级缓存对事物级别的数据进行缓存,一旦事物结束,缓存也即失效。但是该模式下的二级缓存是不安全的,因为多个 Schema 的数据库的主键可能会是同一个值,这样就使得 Hibernate 无法正常使用二级缓存来存放对象。例如:在 hotel_1 的 guest 表中有个 id 为 1 的数据,同时在 hotel_2 的 guest 表中也有一个 id 为 1 的数据。通常我会根据 id 来覆盖类的 hashCode() 方法,这样如果使用二级缓存,就无法区别 hotel_1 的 guest 和 hote_2 的 guest。
在共享数据表的模式下的缓存, 可以同时使用 Hibernate的一级缓存和二级缓存, 因为在共享的数据表中,主键是唯一的,数据表中的每条记录属于对应的租户,在二级缓存中的对象也具有唯一性。Hibernate 分别为 EhCache、OSCache、SwarmCache 和 JBossCache 等缓存插件提供了内置的 CacheProvider 实现,读者可以根据需要选择合理的缓存,修改 Hibernate 配置文件设置并启用它,以提高多租户应用的性能。
EclipseLink
EclipseLink 是 Eclipse 基金会管理下的开源持久层服务项目,为 Java 开发人员与各种数据服务(比如:数据库、web services、对象XML映射(OXM)、企业信息系统(EIS)等)交互提供了一个可扩展框架,目前支持的持久层标准中包括:
Java Persistence API (JPA)
Java Architecture for XML Binding (JAXB)
Java Connector Architecture (JCA)
Service Data Objects (SDO)
EclipseLink 前身是 Oracle TopLink, 2007年 Oracle 将后者绝大部分捐献给了 Eclipse 基金会,次年 EclipseLink 被 Sun 挑选成为 JPA 2.0 的参考实现。
注: 目前 EclipseLink2.5 完全支持 2013 年发布的 JPA2.1(JSR 338) 。
在完整实现 JPA 标准之外,针对 SaaS 环境,在多租户的隔离方面 EclipseLink 提供了很好的支持以及灵活地解决方案。
应用程序隔离
隔离的容器/应用服务器
共享容器/应用服务器的应用程序隔离
同一应用程序内的共享缓存但隔离的 entity manager factory
共享的 entity manager factory 但每隔离的 entity manager
数据隔离
隔离的数据库
隔离的Schema/表空间
隔离的表
共享表但隔离的行
查询过滤
Oracle Virtual Private Database (VPD)
对于多租户数据源隔离主要有以下方案
Single-Table Multi-tenancy ,依靠租户区分列(tenant discriminator columns)来隔离表的行,实现多租户共享表。
Table-Per-Tenant Multi-tenancy ,依靠表的租户区分(table tenant discriminator)来隔离表,实现一租户一个表,大体类似于上文的共享数据库独立Schema模式。
Virtual Private Database(VPD ) Multi-tenancy ,依靠 Oracle VPD 自身的安全访问策略(基于动态SQL where子句特性),实现多租户共享表。
本节重点介绍多租户在 EclipseLink 中的共享数据表和一租户一个表的实现方法,并也以酒店多租户应用的例子展现共享数据表方案的具体实践。
EclipseLink Annotation @Multitenant
与 @Entity 或 @MappedSuperclass 一起使用,表明它们在一个应用程序中被多租户共享, 如清单 10。
清单10. @Multitenant
@Entity
@Table(name="room")
@Multitenant
...
publicclass Room {
}
表 1. Multitenant 包含两个属性
Annotation 属性
描述
缺省值
boolean includeCriteria
是否将租户限定应用到 select、update、delete 操作上
true
MultitenantType value
多租户策略,SINGLE_TABLE, TABLE_PER_TENANT, VPD.
SINGLE_TABLE
共享数据表(SINGLE_TABLE)
Metadata配置
依靠租户区分列修饰符 @TenantDiscriminatorColumn
实现。
清单11. @TenantDiscriminatorColumn
@Entity
@Table(name="hotel_guest")
@Multitenant(SINGLE_TABLE)
@TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id")
publicclass HotelGuest {
}
或者在EclipseLink描述文件orm.xml定义对象与表映射时进行限制,两者是等价的。
清单12. orm.xml
属性配置
租户区分列定义好后,在运行时环境需要配置具体属性值,以确定当前操作环境所属的租户。
三种方式的属性配置,按优先生效顺序排序如下
EntityManager(EM)
EntityManagerFactory(EMF)
Application context (when in a Java EE container)
例如 EntityManagerFactory 可以间接通过在 persistence.xml 中配置持久化单元(Persistence Unit)或直接传属性参数给初始化时 EntityManagerFactory。
清单 13. 配置 persistence.xml
或者
清单 14. 初始化 EntityManagerFactory
按共享粒度可以作如下区分,
用户需要通过 eclipselink.session-name 提供独立的会话名,确保每个租户占有独立的会话和缓存。
清单 15. 为 EntityManagerFactory 配置会话名
共享的 EntityManagerFactory 级别
EntityManagerFactory 的默认模式, 此级别缺省配置为独立二级缓存(L2 cache), 即每个 mutlitenant 实体缓存设置为 ISOLATED,用户也可设置 eclipselink.multitenant.tenants-share-cache 属性为真以共享,此时多租户 Entity 缓存设置为 PROTECTED。
这种级别下,一个活动的 EntityManager 不能更换 tenantId。
这种级别下,共享 session,共享 L2 cache, 用户需要自己设置缓存策略,以设置哪些租户信息是不能在二级缓存共享的。
清单 16. 设置缓存
同样,一个活动的EntityManager不能更换tenant ID。
几点说明:
每个表的区分列可以有任意多个,使用修饰符 TenantDiscriminatorColumns。
清单 17. 多个分区列
@TenantDiscriminatorColumns({
@TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id"),
@TenantDiscriminatorColumn(name = "guest_id", contextProperty="guest.id")
})
租户区分列的名字和对应的上下文属性名可以取任意值,由应用程序开发者设定。
生成的 Schema 可以也可以不包含租户区分列,如 tenant_id 或 guest_id。
租户区分列可以映射到实体对象也可以不。
注意:当映射的时候,实体对象相应的属性必须标记为只读(insertable=false, updatable=false),这种限制使得区分列不能作为实体表的 identifier。
TenantDiscriminatorColumn
被以下 EntityManager 的操作和查询支持:
persist,find,refresh,named queries,update all,delete all 。
一租户一表(TABLE_PER_TENANT )
这种多租户类型使每个租户的数据可以占据专属它自己的一个或多个表,多租户间的这些表可以共享相同 Schema 也可使用不同的,前者使用前缀(prefix)或后缀(suffix)命名模式的表的租户区分符,后者使用租户专属的 Schema 名来定义表的租户区分符。
Metadata配置
依靠数据表的租户区分修饰符 @TenantTableDiscriminator
实现
清单 18.
@Entity
@Table(name=“CAR”)
@Multitenant(TABLE_PER_TENANT)
@TenantTableDiscriminator(type=SCHEMA, contextProperty="eclipselink-tenant.id")
public class Car{
}
或
清单 19.
如前所述,TenantTableDiscriminatorType
有 3 种类型:SCHEMA、SUFFIX 和 PREFIX。
属性配置
与另外两种多租户类型一样,默认情况下,多租户共享EMF,如不想共享 EMF,可以通过配置 PersistenceUnitProperties.MULTITENANT_SHARED_EMF 以及 PersistenceUnitProperties.SESSION_NAME 实现。
清单 20.
或在 persistence.xml 配置属性。
酒店多租户应用实例(EclipseLink 共享(单)表)
数据库 Schema 和测试数据与上文 Hibernate 实现相同,关于对象关系映射(OR mapping)的配置均采用 JPA 和 EclipseLink 定义的 Java Annotation 描述。
关于几个基本操作
添加一个对象实例, 利用EntityManager.persist()
清单 21. 添加
public void save(T t) {
em.persist(t);
}
public void saveBulk(List bulk) {
for(T t:bulk){
em.persist(t);
}
}
更新一个对象实例, 利用EntityManager.merge()
清单 22. 更新
public void update(T t){
em.merge(t);
}
查询, 利用EntityManager的NamedQuery,
清单 23. 多条件多结果查询
若用 JPQL 实现则示例如下:
清单 24. JPQL NamedQuery 定义
部分测试数据如下(MySQL):
hotel_admin
hotel_guest
room
运行附件 MT_Test_Hotels.zip 中的测试代码(请参照 readme)来看看多租户的一些典型场景。
清单 25. 运行测试代码
能得到输出片段如下:
清单 26. 输出
通过共享表的测试数据以及运行结果可以看到,对于多个不同的租户(hotel_admin),在添加、查找、更新操作没有显示声明租户标识的情况下,EntityManager 可以根据自身的租户属性配置
实现租户分离。在本实例,EntityManager 初始化时利用到 hotel_admin 登录后的会话上下文进行租户判断,这里不再赘述。
注:上文中提及的全部源码都可以在附件中找到。
其它方面的考虑
数据备份
独立数据库和独立Sechma的模式,为每个租户备份数据比较容易,因为他们存放在不同的数据表中,只需对整个数据库或整个Schema进行备份。
在共享数据表的模式下,可以将所有租户的数据一起备份,但是若要为某一个租户或按租户分开进行数据备份,就会比较麻烦。通常需要另外写sql脚本根据tenant_id来取得对应的数据然后再备份,但是要按租户来导入的话依然比较麻烦,所以必要时还是需要备份所有并为以后导入方便。
性能
独立数据库:性能高,但价格也高,需要占用资源多,不能共享,性价比低。
共享数据库,独立 Schema:性能中等,但价格合适,部分共享,性价比中等。
共享数据库,共享 Schema,共享数据表:性能中等(可利用 Cache 可以提高性能),但价格便宜,完全共享,性价比高。如果在某些表中有大量的数据,可能会对所有租户产生性能影响。
对于共享数据库的情况下,如果因为太多的最终用户同时访问数据库而导致应用程序性能问题,可以考虑数据表分区等数据库端的优化方案。
经济考虑
为了支持多租户应用,共享模式的应用程序往往比使用独立数据库模式的应用程序相对复杂,因为开发一个共享的架构,导致在应用设计上得花较大的努力,因而初始成本会较高。然而,共享模式的应用在运营成本上往往要低一些,每个租户所花的费用也会比较低。
结束语
多租户数据层方案的选择是一个综合的考量过程,包括成本、数据隔离与保护、维护、容灾、性能等。但无论怎样选择,OR-Mapping 框架对多租户的支持将极大的解放开发人员的工作,从而可以更多专注于应用逻辑。最后我们以一个 Hibernate 和 EclipseLink 的比较来结束本文。
表 2. Hibernate 与 EclipeLink 对多租户支持的比较
Hibernate
EclipseLink
独立数据库
支持,通过实现 MultiTenantConnectionProvider 接口以连接独立的数据库
支持,为每个租户配置独立的 EntityManagerFactory
共享数据库,独立 Schema
支持,通过实现 MultiTenantConnectionProvider 接口以切换 Schema
支持,使用 TABLE_PER_TENANT MultitenantType 以及 SCHEMA TenantTableDiscriminatorType
共享数据库,共享 Schema,共享数据表
多租户 Discriminator 计划在 Hibernate 5.0 支持
支持, 使用 SINGLE_TABLE MultitenantType 以及 TenantDiscriminatorColumn
你可能感兴趣的:(云计算,java)
Spring Boot项目初始化加载自定义配置文件内容到静态属性字段
@Corgi
Java面试题 spring boot 后端 java
文章目录创建配置文件cXXX.properties配置类XXXConfig.java添加第三方JAR包创建配置文件cXXX.properties在resource目录下新建配置文件cXXX.properties,内容如下:#商户号mch_id=xxxxx#商户密码pwd=xxxx#接口请求地址req_url=https://xxx#异步回调通知地址(请替换为实际地址)notify_url=htt
Nginx与Tomcat:谁更适合你的服务器?
当归1024
java 中间件 nginx nginx tomcat 服务器
nginx和Tomcat是两种不同类型的服务器软件,它们各有不同的用途和特点:基本定义nginx轻量级的HTTP服务器和反向代理服务器主要用于静态文件服务、负载均衡、反向代理TomcatJavaWeb应用服务器专门用于运行JavaWeb应用(JSP、Servlet)主要区别1.功能定位nginx:静态文件服务器反向代理服务器负载均衡器HTTP缓存服务器Tomcat:Java应用容器JSP/Serv
Spring AI Alibaba 支持国产大模型的Spring ai框架
程序员老陈头
面试 学习路线 阿里巴巴 spring 人工智能 java
总计30万奖金,SpringAIAlibaba应用框架挑战赛开赛点此了解SpringAI:java做ai应用的最好选择过去,Java在AI应用开发方面缺乏一个高效且易于集成的框架,这限制了开发者快速构建和部署智能应用程序的能力。SpringAI正是为解决这一问题而生,它提供了一套统一的接口,使得AI功能能够以一种标准化的方式被集成到现有的Java项目中。此外,SpringAI与原有的Spring生
Node.js 全局对象
froginwe11
开发语言
Node.js全局对象引言Node.js作为一种流行的JavaScript运行环境,以其高性能、轻量级和跨平台的特点,被广泛应用于服务器端编程、网络应用开发等领域。在Node.js中,全局对象是一个重要的概念,它为开发者提供了一系列内置的全局变量和方法,使得编程变得更加便捷。本文将详细介绍Node.js的全局对象,帮助开发者更好地理解和运用它们。Node.js全局对象概述Node.js的全局对象指
企业级AI开发利器:Spring AI框架深度解析与实战_spring ai实战
AI大模型-海文
人工智能 spring python 算法 开发语言 java 机器学习
企业级AI开发利器:SpringAI框架深度解析与实战一、前言:Java生态的AI新纪元在人工智能技术爆发式发展的今天,Java开发者面临着一个新的挑战:如何将大语言模型(LLMs)和生成式AI(GenAI)无缝融入企业级应用。传统的Java生态缺乏统一的AI集成方案,开发者往往需要为不同AI供应商(如OpenAI、阿里云、HuggingFace)编写大量重复的接口适配代码,这不仅增加了开发成本,
009 【入门】单双链表及其反转-堆栈诠释
要天天开心啊
算法专栏 算法 链表
链表与堆栈系统详解|[数据结构]-[中级]-[通用]一、基础概念与内存模型1.按值传递vs按引用传递|[Java]-[基础]-[内存]//[典型错误示例]-Java中的引用传递陷阱voidmodify(Nodenode){node=node.next;//[警告]错误!仅修改局部引用的指向,不影响原始链表}//[正确做法]-通过引用修改对象内部状态voidrealModify(Nodenode){
深度解析JavaScript 闭包
coding随想
JavaScript javascript 开发语言 ecmascript
深度解析JavaScript闭包引言:为什么闭包让人又爱又怕?在JavaScript的学习过程中,闭包(Closure)是一个绕不开的“坎”。很多开发者第一次接触闭包时,会感到一头雾水:“为什么函数能记住外部作用域的变量?”、“为什么闭包会导致内存泄漏?”。但另一方面,闭包又是JavaScript最强大的特性之一,它支撑着模块化开发、数据封装、异步编程等核心场景。本文将通过通俗的语言和生动的案例,
JavaScript中的函数柯里化(Currying):从概念到实战
coding随想
JavaScript javascript ecmascript 开发语言 前端
JavaScript中的函数柯里化(Currying):从概念到实战在JavaScript开发中,函数式编程(FunctionalProgramming)逐渐成为一种主流思想。而函数柯里化(Currying),正是这一思想中的核心技巧之一。它不仅能提升代码的复用性和灵活性,还能帮助我们构建更优雅、更模块化的解决方案。本文将带你从零开始,深入理解柯里化的原理、实现方式及实际应用场景。一、什么是函数柯
webpack和vite区别
PromptOnce
webpack 前端 node.js
一、Webpack1.概述Webpack是一个模块打包工具,它会递归地构建依赖关系图,并将所有模块打包成一个或多个bundle(包)。2.特点配置灵活:Webpack提供了高度可定制的配置文件,可以根据项目需求进行各种优化。生态系统丰富:Webpack拥有庞大的插件和加载器生态系统,可以处理各种资源类型(JavaScript、CSS、图片等)。支持代码拆分:通过代码拆分和懒加载,Webpack可以
javascript 动态画心加文字
das白
# javascript javascript 动态 心型线 文字
测试//铺满屏幕varwidth=document.documentElement.clientWidth;varheight=document.documentElement.clientHeight;document.getElementById("gycanvas").setAttribute("width",width);document.getElementById("gycanvas"
javascript 动态画心
das白
# javascript javascript 动态 心型线
测试canvas{background:lawngreen;//画布背景色}//铺满屏幕varwidth=document.documentElement.clientWidth;varheight=document.documentElement.clientHeight;document.getElementById("gycanvas").setAttribute("width",width
javascript 画心型线
测试canvas{background:lawngreen;//画布背景色}//铺满屏幕varwidth=document.documentElement.clientWidth;varheight=document.documentElement.clientHeight;document.getElementById("gycanvas").setAttribute("width",width
掌握Web3开发:从入门到精通
夲奋亻Jay
Web3 web3
掌握Web3开发是一个涉及多个步骤和学习阶段的过程。以下是一些关键的步骤和开发案例,以及它们在搜索结果中的索引编号:了解区块链基础:学习区块链的基本概念,如去中心化、加密技术、共识机制等[1]。学习智能合约:学习智能合约的工作原理和它们在区块链上的应用,特别是以太坊平台上的智能合约[1]。掌握Web3.js或Ethers.js:学习如何使用这些JavaScript库与智能合约交互、发送交易和监听事
JavaScript性能优化
lyh1344
javascript 性能优化 开发语言
JavaScript性能优化方法减少重绘和回流频繁操作DOM会导致浏览器反复计算布局,引发性能问题。使用documentFragment进行批量DOM操作,或通过classList一次性修改多个样式属性。缓存DOM查询结果,避免重复访问。事件委托利用事件冒泡机制,将事件监听器绑定到父元素而非多个子元素。减少内存占用,提升动态内容的事件处理效率。节流与防抖高频事件(如滚动、输入)通过节流(Throt
将图片的base64编码直接嵌入到html文件的css中
Kuo-Teng
软件开发实战 html css javascript
将图片的base64编码直接嵌入到html文件的css中1.背景2.将图片进行base64编码3.将图片的base64编码写入到css1.背景如果你需要在html中引入一张外部图片,你可能会这样做:如果你将引用的图片保存到本地,你可能会这样做:但是,如果网络延迟较高,或者在jar包中运行Java项目时无法根据路径顺利找到图片呢?那么,将图片的base64编码直接写入html文件便是最好的选择!2.
什么是Node.js,有什么特点
前端与小赵
node.js
Node.js简介Node.js是一个基于ChromeV8引擎的JavaScript运行时环境,由RyanDahl于2009年创建。Node.js允许开发者使用JavaScript编写服务器端应用程序,打破了JavaScript仅限于浏览器端的限制。Node.js的设计目标是提供一种简单、高效的方式来构建可伸缩的网络应用。Node.js的特点非阻塞I/O特点:Node.js使用事件驱动的非阻塞I/
Node.js到底是什么
浪裡遊
杂文 node.js php 开发语言 前端 javascript vue.js
我想像是npm、vite这些名词大家都很熟悉,对它们的作用也有大致印象,但是可能都像我一样不明白Node.js到底是什么,这里给大家带来一个简单介绍。Node.js详解:历史发展、生态构建与底层原理一、Node.js的起源与历史发展诞生背景2009年5月:Node.js由RyanDahl开发并首次发布。其核心目标是解决JavaScript仅限于浏览器端运行的局限性,通过ChromeV8引擎(Jav
【Html实现“心形日出”(附效果+源代码)】| JavaScript面试题:解释一下异步编程中的回调函数、Promise和Async/Await的概念。它们有什么区别?
追光者♂
html5 css3 心形日出 前端特效 JS面试题 Promise Async/Await
风会带走你曾经存在过的证明。——虞姬作者主页:追光者♂个人简介:[1]计算机专业硕士研究生[2]2023年城市之星领跑者TOP1(哈尔滨)[3]2022年度博客之星人工智能领域TOP4[4]阿里云社区特邀专家博主[5]CSDN-人工智能领域优质创作者无限进步,一起追光!!!
java毕业设计房产中介系统mybatis+源码+调试部署+系统+数据库+lw
兮兮科技
java mybatis 开发语言
java毕业设计房产中介系统mybatis+源码+调试部署+系统+数据库+lwjava毕业设计房产中介系统mybatis+源码+调试部署+系统+数据库+lw本源码技术栈:项目架构:B/S架构开发语言:Java语言开发软件:ideaeclipse前端技术:Layui、HTML、CSS、JS、JQuery等技术后端技术:JAVA运行环境:Win10、JDK1.8数据库:MySQL5.7/8.0源码地址
JavaSE -- 时间类的详细介绍(Date,LocalDate)
@Touper
Java学习笔记 java 开发语言
Date类构造方法newDate():当前系统日期和时间。newDate(long):给定的日期时间常用方法after(Date):判断当前日期对象是否在给定日期之后before(Date):判断当前日期对象是否在给定日期之前equals(Object):判断两日期是否相等compareTo(Date):比较两日期前后顺序,如果当前日期对象大于给定日期对象返回1,小于返回-1,等于返回0。Date
支持java8的kafka版本
兮动人
kafka 分布式 支持java8的kafka版本
文章目录1.Kafka支持Java8的版本范围2.官方建议与兼容性3.版本迁移建议4.关键时间点5.注意事项6.总结1.Kafka支持Java8的版本范围Kafka2.x和3.x版本:Kafka2.x和3.x版本(如2.8.0、3.0.0等)理论上支持Java8,但官方已逐步弃用对Java8的支持。Kafka3.0:官方在3.0版本中弃用Java8(但仍允许使用),并强烈建议升级到Java11或更
Java基础:流程控制语句:条件、循环和跳转
越重天
Java 基础入门教程 Java 宝藏 java 开发语言 java流程控制语句 零到一学Java
前言Java中的流程控制语句其实和C、C++一样,在Java中,流程控制会涉及到包括if-else、while、do-while、for、return、break以及选择语句switch。下面以此进行分析。流程控制语句,分为三大类:条件语句,循环语句和跳转语句,如下图所示:1.条件语句条件语句可根据不同的条件执行不同的语句。包括if条件语句与switch多分支语句。1.1if语句if语句
HTML中引入JavaScript的三种方式
北冥郇翔
javascript html 前端
在HTML中引入JavaScript主要有以下三种方式,每种方式都有其适用场景和注意事项:1.内嵌方式(在标签内直接编写代码)直接在HTML文件的或中使用标签编写JavaScript代码。特点:代码与HTML混合,适用于简单逻辑或快速测试。alert()等函数会阻塞HTML页面渲染(如引用[1]所示)。示例:window.alert("页面加载被阻塞!");//阻塞后续内容渲染2.外部引入方式(推
Java/Kotlin 主线程IO操作全方位监控指南(实战代码+性能优化)
时小雨
Android实战与技巧 android kotlin
本文涵盖从基础监控到高级诊断的全套解决方案,包含10+个可直接落地的代码示例一、为什么需要监控主线程IO?主线程IO阻塞会导致界面卡顿、响应延迟等严重问题。典型场景:文件读写阻塞UI渲染网络请求未使用异步线程数据库查询未优化日志输出同步阻塞二、代码级监控方案(Kotlin实现)1.装饰器模式监控流操作classMonitoredInputStream(privatevalorigin:InputS
org.apache.rocketmq.client.consumer.DefaultMQPushConsumer.setNamespaceV2(java.lang.String) not exist
nextera-void
java-rocketmq apache rocketmq
***************************APPLICATIONFAILEDTOSTART***************************Description:Anattemptwasmadetocallamethodthatdoesnotexist.Theattemptwasmadefromthefollowinglocation:org.apache.rocketmq.sp
Java底层原理:深入理解JVM内存模型与线程安全
代码老y
java 开发语言 jvm
一、JVM内存模型(JMM)JVM内存模型(JMM)是Java语言规范中定义的内存模型,它描述了Java程序中的变量存储在内存中的方式以及线程如何访问这些变量。JMM是Java并发编程的基础,理解它可以帮助我们更好地理解和解决线程安全问题。(一)JMM的基本概念主内存(MainMemory)主内存是所有线程共享的内存区域,存储了Java程序中的所有变量。主内存中的变量可以被所有线程访问和修改。工作
python中使用grpc方法示例_Python中使用grpc与consul
weixin_39719077
gRPC客户端和服务端可以在多种环境中运行和交互,并且可以用任何gRPC支持的语言来编写。gRPC支持C++JavaPythonGoRubyC#Node.jsPHPDart等语言gRPC默认使用protocolbuffers,这是Google开源的一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或RPC数据交换格式。安装GoogleProtocolBuf
什么是 Session?如何应用?
魔道不误砍柴功
计算机网络基础 Java使用与案例分享 Session Cookie http https
文章目录一、什么是Session?举个例子:二、Session的工作原理Session和Cookie的关系三、Session的应用场景1.用户登录状态管理示例代码(Java):2.购物车功能示例代码(Java):3.防止表单重复提交示例代码(Java):四、Session的过期与销毁1.Session过期时间2.手动销毁Session五、Session的安全性六、总结推荐阅读文章在Web开发中,S
Spring AI入门教学:从零搭建智能应用(2025最新实践)
程序员子固
spring 人工智能 java ai
目录引言:为什么选择SpringAI?一、环境搭建(附避坑指南)1.开发环境要求2.依赖配置二、实战:智能客服接入(代码级详解)1.配置模型参数2.实现流式对话接口三、高级功能:多模态AI开发1.图像描述生成2.智能文档处理四、开发者工具箱1.调试技巧2.性能优化五、学习路径建议引言:为什么选择SpringAI?随着生成式AI技术的爆发式发展(如OpenAI的GPT-4.5新动态24),Java开
2.jdbc之工具类,SQL注入攻击和JDBC事务
hutc_Alan
sql java 数据库
4.JDBC工具类抽取工具类1)编写配置文件在src目录下创建config.properties配置文件driverClass=com.mysql.cj.jdbc.Driverurl=jdbc:mysql://192.168.1.224:3306/db14username=rootpassword=1234562)编写jdbc工具类utils文件下(JDBCUtils.java)packagejd
LeetCode[Math] - #66 Plus One
Cwind
java LeetCode 题解 Algorithm Math
原题链接:#66 Plus One
要求:
给定一个用数字数组表示的非负整数,如num1 = {1, 2, 3, 9}, num2 = {9, 9}等,给这个数加上1。
注意:
1. 数字的较高位存在数组的头上,即num1表示数字1239
2. 每一位(数组中的每个元素)的取值范围为0~9
难度:简单
分析:
题目比较简单,只须从数组
JQuery中$.ajax()方法参数详解
AILIKES
JavaScript jsonp jquery Ajax json
url: 要求为String类型的参数,(默认为当前页地址)发送请求的地址。
type: 要求为String类型的参数,请求方式(post或get)默认为get。注意其他http请求方法,例如put和 delete也可以使用,但仅部分浏览器支持。
timeout: 要求为Number类型的参数,设置请求超时时间(毫秒)。此设置将覆盖$.ajaxSetup()方法的全局
JConsole & JVisualVM远程监视Webphere服务器JVM
Kai_Ge
JVisualVM JConsole Webphere
JConsole是JDK里自带的一个工具,可以监测Java程序运行时所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。我们可以根据这些信息判断程序是否有内存泄漏问题。
使用JConsole工具来分析WAS的JVM问题,需要进行相关的配置。
首先我们看WAS服务器端的配置.
1、登录was控制台https://10.4.119.18
自定义annotation
120153216
annotation
Java annotation 自定义注释@interface的用法 一、什么是注释
说起注释,得先提一提什么是元数据(metadata)。所谓元数据就是数据的数据。也就是说,元数据是描述数据的。就象数据表中的字段一样,每个字段描述了这个字段下的数据的含义。而J2SE5.0中提供的注释就是java源代码的元数据,也就是说注释是描述java源
CentOS 5/6.X 使用 EPEL YUM源
2002wmj
centos
CentOS 6.X 安装使用EPEL YUM源1. 查看操作系统版本[root@node1 ~]# uname -a Linux node1.test.com 2.6.32-358.el6.x86_64 #1 SMP Fri Feb 22 00:31:26 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux [root@node1 ~]#
在SQLSERVER中查找缺失和无用的索引SQL
357029540
SQL Server
--缺失的索引
SELECT avg_total_user_cost * avg_user_impact * ( user_scans + user_seeks ) AS PossibleImprovement ,
last_user_seek ,
 
Spring3 MVC 笔记(二) —json+rest优化
7454103
Spring3 MVC
接上次的 spring mvc 注解的一些详细信息!
其实也是一些个人的学习笔记 呵呵!
替换“\”的时候报错Unexpected internal error near index 1 \ ^
adminjun
java “\替换”
发现还是有些东西没有刻子脑子里,,过段时间就没什么概念了,所以贴出来...以免再忘...
在拆分字符串时遇到通过 \ 来拆分,可是用所以想通过转义 \\ 来拆分的时候会报异常
public class Main {
/*
POJ 1035 Spell checker(哈希表)
aijuans
暴力求解--哈希表
/*
题意:输入字典,然后输入单词,判断字典中是否出现过该单词,或者是否进行删除、添加、替换操作,如果是,则输出对应的字典中的单词
要求按照输入时候的排名输出
题解:建立两个哈希表。一个存储字典和输入字典中单词的排名,一个进行最后输出的判重
*/
#include <iostream>
//#define
using namespace std;
const int HASH =
通过原型实现javascript Array的去重、最大值和最小值
ayaoxinchao
JavaScript array prototype
用原型函数(prototype)可以定义一些很方便的自定义函数,实现各种自定义功能。本次主要是实现了Array的去重、获取最大值和最小值。
实现代码如下:
<script type="text/javascript">
Array.prototype.unique = function() {
var a = {};
var le
UIWebView实现https双向认证请求
bewithme
UIWebView https Objective-C
什么是HTTPS双向认证我已在先前的博文 ASIHTTPRequest实现https双向认证请求
中有讲述,不理解的读者可以先复习一下。本文是用UIWebView来实现对需要客户端证书验证的服务请求,网上有些文章中有涉及到此内容,但都只言片语,没有讲完全,更没有完整的代码,让人困扰不已。但是此知
NoSQL数据库之Redis数据库管理(Redis高级应用之事务处理、持久化操作、pub_sub、虚拟内存)
bijian1013
redis 数据库 NoSQL
3.事务处理
Redis对事务的支持目前不比较简单。Redis只能保证一个client发起的事务中的命令可以连续的执行,而中间不会插入其他client的命令。当一个client在一个连接中发出multi命令时,这个连接会进入一个事务上下文,该连接后续的命令不会立即执行,而是先放到一个队列中,当执行exec命令时,redis会顺序的执行队列中
各数据库分页sql备忘
bingyingao
oracle sql 分页
ORACLE
下面这个效率很低
SELECT * FROM ( SELECT A.*, ROWNUM RN FROM (SELECT * FROM IPAY_RCD_FS_RETURN order by id desc) A ) WHERE RN <20;
下面这个效率很高
SELECT A.*, ROWNUM RN FROM (SELECT * FROM IPAY_RCD_
【Scala七】Scala核心一:函数
bit1129
scala
1. 如果函数体只有一行代码,则可以不用写{},比如
def print(x: Int) = println(x)
一行上的多条语句用分号隔开,则只有第一句属于方法体,例如
def printWithValue(x: Int) : String= println(x); "ABC"
上面的代码报错,因为,printWithValue的方法
了解GHC的factorial编译过程
bookjovi
haskell
GHC相对其他主流语言的编译器或解释器还是比较复杂的,一部分原因是haskell本身的设计就不易于实现compiler,如lazy特性,static typed,类型推导等。
关于GHC的内部实现有篇文章说的挺好,这里,文中在RTS一节中详细说了haskell的concurrent实现,里面提到了green thread,如果熟悉Go语言的话就会发现,ghc的concurrent实现和Go有点类
Java-Collections Framework学习与总结-LinkedHashMap
BrokenDreams
LinkedHashMap
前面总结了java.util.HashMap,了解了其内部由散列表实现,每个桶内是一个单向链表。那有没有双向链表的实现呢?双向链表的实现会具备什么特性呢?来看一下HashMap的一个子类——java.util.LinkedHashMap。
读《研磨设计模式》-代码笔记-抽象工厂模式-Abstract Factory
bylijinnan
abstract
声明: 本文只为方便我个人查阅和理解,详细的分析以及源代码请移步 原作者的博客http://chjavach.iteye.com/
package design.pattern;
/*
* Abstract Factory Pattern
* 抽象工厂模式的目的是:
* 通过在抽象工厂里面定义一组产品接口,方便地切换“产品簇”
* 这些接口是相关或者相依赖的
压暗面部高光
cherishLC
PS
方法一、压暗高光&重新着色
当皮肤很油又使用闪光灯时,很容易在面部形成高光区域。
下面讲一下我今天处理高光区域的心得:
皮肤可以分为纹理和色彩两个属性。其中纹理主要由亮度通道(Lab模式的L通道)决定,色彩则由a、b通道确定。
处理思路为在保持高光区域纹理的情况下,对高光区域着色。具体步骤为:降低高光区域的整体的亮度,再进行着色。
如果想简化步骤,可以只进行着色(参看下面的步骤1
Java VisualVM监控远程JVM
crabdave
visualvm
Java VisualVM监控远程JVM
JDK1.6开始自带的VisualVM就是不错的监控工具.
这个工具就在JAVA_HOME\bin\目录下的jvisualvm.exe, 双击这个文件就能看到界面
通过JMX连接远程机器, 需要经过下面的配置:
1. 修改远程机器JDK配置文件 (我这里远程机器是linux).
 
Saiku去掉登录模块
daizj
saiku 登录 olap BI
1、修改applicationContext-saiku-webapp.xml
<security:intercept-url pattern="/rest/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<security:intercept-url pattern=&qu
浅析 Flex中的Focus
dsjt
html Flex Flash
关键字:focus、 setFocus、 IFocusManager、KeyboardEvent
焦点、设置焦点、获得焦点、键盘事件
一、无焦点的困扰——组件监听不到键盘事件
原因:只有获得焦点的组件(确切说是InteractiveObject)才能监听到键盘事件的目标阶段;键盘事件(flash.events.KeyboardEvent)参与冒泡阶段,所以焦点组件的父项(以及它爸
Yii全局函数使用
dcj3sjt126com
yii
由于YII致力于完美的整合第三方库,它并没有定义任何全局函数。yii中的每一个应用都需要全类别和对象范围。例如,Yii::app()->user;Yii::app()->params['name'];等等。我们可以自行设定全局函数,使得代码看起来更加简洁易用。(原文地址)
我们可以保存在globals.php在protected目录下。然后,在入口脚本index.php的,我们包括在
设计模式之单例模式二(解决无序写入的问题)
come_for_dream
单例模式 volatile 乱序执行 双重检验锁
在上篇文章中我们使用了双重检验锁的方式避免懒汉式单例模式下由于多线程造成的实例被多次创建的问题,但是因为由于JVM为了使得处理器内部的运算单元能充分利用,处理器可能会对输入代码进行乱序执行(Out Of Order Execute)优化,处理器会在计算之后将乱序执行的结果进行重组,保证该
程序员从初级到高级的蜕变
gcq511120594
框架 工作 PHP android html5
软件开发是一个奇怪的行业,市场远远供不应求。这是一个已经存在多年的问题,而且随着时间的流逝,愈演愈烈。
我们严重缺乏能够满足需求的人才。这个行业相当年轻。大多数软件项目是失败的。几乎所有的项目都会超出预算。我们解决问题的最佳指导方针可以归结为——“用一些通用方法去解决问题,当然这些方法常常不管用,于是,唯一能做的就是不断地尝试,逐个看看是否奏效”。
现在我们把淫浸代码时间超过3年的开发人员称为
Reverse Linked List
hcx2013
list
Reverse a singly linked list.
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
p
Spring4.1新特性——数据库集成测试
jinnianshilongnian
spring 4.1
目录
Spring4.1新特性——综述
Spring4.1新特性——Spring核心部分及其他
Spring4.1新特性——Spring缓存框架增强
Spring4.1新特性——异步调用和事件机制的异常处理
Spring4.1新特性——数据库集成测试脚本初始化
Spring4.1新特性——Spring MVC增强
Spring4.1新特性——页面自动化测试框架Spring MVC T
C# Ajax上传图片同时生成微缩图(附Demo)
liyonghui160com
1.Ajax无刷新上传图片,详情请阅我的这篇文章。(jquery + c# ashx)
2.C#位图处理 System.Drawing。
3.最新demo支持IE7,IE8,Fir
Java list三种遍历方法性能比较
pda158
java
从c/c++语言转向java开发,学习java语言list遍历的三种方法,顺便测试各种遍历方法的性能,测试方法为在ArrayList中插入1千万条记录,然后遍历ArrayList,发现了一个奇怪的现象,测试代码例如以下:
package com.hisense.tiger.list;
import java.util.ArrayList;
import java.util.Iterator;
300个涵盖IT各方面的免费资源(上)——商业与市场篇
shoothao
seo 商业与市场 IT资源 免费资源
A.网站模板+logo+服务器主机+发票生成
HTML5 UP:响应式的HTML5和CSS3网站模板。
Bootswatch:免费的Bootstrap主题。
Templated:收集了845个免费的CSS和HTML5网站模板。
Wordpress.org|Wordpress.com:可免费创建你的新网站。
Strikingly:关注领域中免费无限的移动优
localStorage、sessionStorage
uule
localStorage
W3School 例子
HTML5 提供了两种在客户端存储数据的新方法:
localStorage - 没有时间限制的数据存储
sessionStorage - 针对一个 session 的数据存储
之前,这些都是由 cookie 完成的。但是 cookie 不适合大量数据的存储,因为它们由每个对服务器的请求来传递,这使得 cookie 速度很慢而且效率也不