精华内容
下载资源
问答
  • java框架--持久化层

    2021-01-13 21:08:43
    一、持久化层 1. 将内存中的数据保存到关系型数据库的过程称为持久化。 2. 持久化层的构成 事务(ACID) A:atomicity原子性。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。 C: ...

    一、持久化层

    1. 将内存中的数据保存到关系型数据库的过程称为持久化。
    2. 持久化层的构成

    1. 事务(ACID)
      A:atomicity原子性。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
      C: consistency一致性。事务必须是使数据库从一个一致状态变到另一个一致性状态。
      I: isolation隔离性。一个失误的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
      D: durability持久性。持久性也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。
    2. 数据的安全性(遵守约束规则)
    3. 完整性
      数据完整性指数据的精准性和可靠性。它是防止数据库中存在不符合语义规定的数据和防止因错误信息的输入输出造成无效操作或错误信息而提出的。数据完整性分为四类:实体完整性、域完整性、参照完整性、用户自定义完整性。

    3. 对象关系映射(O/R Mapping)的规则

    1. 实体类映射表,实体类的属性映射表的字段。
    2. 实体类对象映射表的记录。
    3. 实体对象的关联关系映射表的关联关系。

    4. 三大范式

    • 1NF:属性不能分割
    • 2NF:在1NF的基础上,所有属性都依赖于主键属性(主键约束)
    • 3NF:在2NF的基础上,将不依赖于主键的属性分离并设置关键约束(外键约束)

    二、持久化框架的技术实现框架

    1. JDBC
    2. EJB–CMP(容器管理的持久化,适用于分布式应用–金融证券)IBM
    3. JDO(oracle的持久化标准)
    4. OJB(开源组织的持久化框架标准)
    5. Mybatis(google持久化开源框架)
    6. Hibernate(开源组织的产品)
    展开全文
  • 对于需要被持久化的Java对象,在它的生命周期中,可处于以下三个状态之一:(1) 临时状态(transient):刚刚用new语句创建,还没有被持久化,不处于Session的缓存中。处于临时状态的Java对象被称为临时对象。(2) 持久....

    当应用程序通过new语句创建了一个对象,这个对象的生命周期就开始了,当不再有任何引用变量引用它,这个对象就结束生命周期,它占用的内存就可以被JVM的垃圾回收器回收。对于需要被持久化的Java对象,在它的生命周期中,可处于以下三个状态之一:

    (1) 临时状态(transient):刚刚用new语句创建,还没有被持久化,不处于Session的缓存中。处于临时状态的Java对象被称为临时对象。

    (2) 持久化状态(persistent):已经被持久化,加入到Session的缓存中。处于持久化状态的Java对象被称为持久化对象。

    (3) 游离状态(detached):已经被持久化,但不再处于Session的缓存中。处于游离状态的Java对象被称为游离对象。

    hibernate_essay_5_1.gif

    图1为Java对象的完整状态转换图,Session的特定方法触发Java对象由一个状态转换到另一个状态。从图1看出,当Java对象处于临时状态或游离状态,只要不被任何变量引用,就会结束生命周期,它占用的内存就可以被JVM的垃圾回收器回收;当处于持久化状态,由于Session的缓存会引用它,因此它始终处于生命周期中。

    临时对象的特征

    临时对象具有以下特征:

    (1) 不处于Session的缓存中,也可以说,不被任何一个Session实例关联。

    (2) 在数据库中没有对应的记录。

    在以下情况下,Java对象进入临时状态:

    (1) 当通过new语句刚创建了一个Java对象,它处于临时状态,此时不和数据库中的任何记录对应。

    (2) Session的delete()方法能使一个持久化对象或游离对象转变为临时对象。对于游离对象,delete()方法从数据库中删除与它对应的记录;对于持久化对象,delete()方法从数据库中删除与它对应的记录,并且把它从Session的缓存中删除。

    持久化对象的特征

    持久化对象具有以下特征:

    (1) 位于一个Session实例的缓存中,也可以说,持久化对象总是被一个Session实例关联。

    (2) 持久化对象和数据库中的相关记录对应。

    (3) Session在清理缓存时,会根据持久化对象的属性变化,来同步更新数据库。

    Session的许多方法都能够触发Java对象进入持久化状态:

    (1) Session的save()方法把临时对象转变为持久化对象。

    (2) Session的load()或get()方法返回的对象总是处于持久化状态。

    (3) Session的find()方法返回的List集合中存放的都是持久化对象。

    (4) Session的update()、saveOrUpdate()和lock()方法使游离对象转变为持久化对象。(nate注:根据hibernate reference的说法当试图用update更新一个持久化对象时会抛异常)

    (5)当一个持久化对象关联一个临时对象,在允许级联保存的情况下,Session在清理缓存时会把这个临时对象也转变为持久化对象。

    Hibernate保证在同一个Session实例的缓存中,数据库表中的每条记录只对应惟一的持久化对象。例如对于以下代码,共创建了两个Session实例:session1和session2。session1和session2拥有各自的缓存。在session1的缓存中,只会有惟一的OID为1的Customer持久化对象,在session2的缓存中,也只会有惟一的OID为1的Customer持久化对象。因此在内存中共有两个Customer持久化对象,一个属于session1的缓存,一个属于session2的缓存。引用变量a和b都引用session1缓存中的Customer持久化对象,而引用变量c引用session2缓存中的Customer持久化对象:

    Session session1=sessionFactory.openSession();

    Session session2=sessionFactory.openSession();

    Transaction tx1 = session1.beginTransaction();

    Transaction tx2 = session2.beginTransaction();

    Customer a=(Customer)session1.load(Customer.class,new Long(1));

    Customer b=(Customer)session1.load(Customer.class,new Long(1));

    Customer c=(Customer)session2.load(Customer.class,new Long(1));

    System.out.println(a= =b); //true

    System.out.println(a= =c); //false

    tx1.commit();

    tx2.commit();

    session1.close();

    session2.close();

    Java对象的持久化状态是相对于某个具体的Session实例的,以下代码试图使一个Java对象同时被两个Session实例关联:

    Session session1=sessionFactory.openSession();

    Session session2=sessionFactory.openSession();

    Transaction tx1 = session1.beginTransaction();

    Transaction tx2 = session2.beginTransaction();

    Customer c=(Customer)session1.load(Customer.class,new Long(1)); //Customer对象被session1关联

    session2.update(c); //Customer对象被session2关联

    c.setName("Jack"); //修改Customer对象的属性

    tx1.commit(); //执行update语句

    tx2.commit(); //执行update语句

    session1.close();

    session2.close();

    当执行session1的load()方法时,OID为1的Customer对象被加入到session1的缓存中,因此它是session1的持久化对象,此时它还没有被session2关联,因此相对于session2,它处于游离状态。当执行session2的update()方法时,Customer对象被加入到session2的缓存中,因此也成为session2的持久化对象。接下来修改Customer对象的name属性,会导致两个Session实例在清理各自的缓存时,都执行相同的update语句:

    update CUSTOMERS set NAME='Jack' …… where ID=1;

    在实际应用程序中,应该避免一个Java对象同时被多个Session实例关联,因为这会导致重复执行SQL语句,并且极容易出现一些并发问题。

    游离对象的特征

    游离对象具有以下特征:

    (1) 不再位于Session的缓存中,也可以说,游离对象不被Session关联。

    (2) 游离对象是由持久化对象转变过来的,因此在数据库中可能还存在与它对应的记录(前提条件是没有其他程序删除了这条记录)。

    游离对象与临时对象的相同之处在于,两者都不被Session关联,因此Hibernate不会保证它们的属性变化与数据库保持同步。游离对象与临时对象的区别在于:前者是由持久化对象转变过来的,因此可能在数据库中还存在对应的记录,而后者在数据库中没有对应的记录。

    Session的以下方法使持久化对象转变为游离对象:

    (1) 当调用Session的close()方法时,Session的缓存被清空,缓存中的所有持久化对象都变为游离对象。如果在应用程序中没有引用变量引用这些游离对象,它们就会结束生命周期。

    (2)Session的evict()方法能够从缓存中删除一个持久化对象,使它变为游离状态。当Session的缓存中保存了大量的持久化对象,会消耗许多内存空间,为了提高性能,可以考虑调用evict()方法,从缓存中删除一些持久化对象。但是多数情况下不推荐使用evict()方法,而应该通过查询语言,或者显式的导航来控制对象图的深度。

    posted on 2007-02-07 08:43 ☜♥☞MengChuChen 阅读(125) 评论(0)  编辑  收藏

    展开全文
  • 持久化层技术之间对比 JDBC Hibernate 和 JPA MyBatis 1、JDBC SQL夹杂在java代码中,耦合度搞,编码复杂。 实际开发中SQL发生变化,会频繁的修改,不易于维护。 代码冗余,开发效率低。 2、Hibernate 和 ...

    持久化层技术之间对比

    • JDBC
    • Hibernate 和 JPA
    • MyBatis

    1、JDBC

    • SQL夹杂在java代码中,耦合度搞,编码复杂。
    • 实际开发中SQL发生变化,会频繁的修改,不易于维护。
    • 代码冗余,开发效率低。

    2、Hibernate 和 JPA

    JPA是java类到数据库映射的标准、Hibernate是标准的具体实现。

    • 操作简便,开发效率高。
    • 对于复杂的SQL语句需要绕过框架。
    • 内部自动编写的SQL语句不易进行特殊化优化。
    • 反射操作太多,导致数据库性能下降。
    • 是基于映射的全自动框架,大量字段的POJO进行补发映射比较困难。

    什么是POJO

    POJO是Plain OrdinaryJava Object的缩写,但是它通指没有使用Entity Beans的普通java对象,可以把POJO作为支持业务逻辑的协助类。

    POJO实质上可以理解为简单的实体类,顾名思义POJO类的作用是方便程序员使用数据库中的数据表,对于广大的程序员,可以很方便的将POJO类当做对象来进行使用,当然也是可以方便的调用其get,set方法。POJO类也给我们在struts框架中的配置带来了很大的方便。

    3、MyBatis

    • 轻量级,性能出色。
    • SQL和java编码分开,java代码专注业务、SQL语句专注数据。
    • 开发效率不及Hibernate、但是功能完全够用。

    展开全文
  • (1)数据持久化层场景 (2)缓冲层场景 (3)基于常用组件的微服务场景及微服务踩坑 (4)开发运维场景 数据持久化层场景 场景1:冷热分离 订单功能,里面的主表有几千万的数据量,加上关联表,数据量达到上亿。...

    本部分内容包括:
    (1)数据持久化层场景
    (2)缓冲层场景
    (3)基于常用组件的微服务场景及微服务踩坑
    (4)开发运维场景

    数据持久化层场景

    场景1:冷热分离

    订单功能,里面的主表有几千万的数据量,加上关联表,数据量达到上亿。因为业务员查询的基本是近期常用的数据,常用的数据量大大减少了,可以使用"冷热分离"。

    什么情况下使用冷热分离

    1.用户能接受新旧数据分开查询,比如有些电商网站默认只让查询 3 个月内的订单。
    2.数据走到终态后,只有读没有写的需求,比如订单完结状态。

    如何判断一个数据到底是冷数据还是热数据

    在判断一个数据到底是冷数据还是热数据时,我们主要采用主表里的 1 个或多个字段组合的方式作为区分标识。比如“下单时间”这个字段,“订单状态”字段。还可以采用组合字段的方式来区分,比如我们把下单时间 > 3 个月且状态为“已完结”的订单标识为冷数据,其他的当作热数据。
    总之,最终究竟使用哪种字段来判断,还是需要根据你的实际业务来定。

    关于判断冷热数据的逻辑,这里还有 2 个注意要点必须说明:

    1. 如果一个数据被标识为冷数据,业务代码不会再对它进行写操作;
    2. 不会同时存在读冷/热数据的需求。

    如何触发冷热数据分离

    冷热数据分离的触发逻辑分 3 种。

    1. 直接修改业务代码,每次修改数据时触发冷热分离。
      在这里插入图片描述

    2. 如果不想修改原来的业务代码,可通过监听数据库变更日志 binlog 的方式来触发。
      在这里插入图片描述

    3. 通过定时扫描数据库的方式来触发。
      在这里插入图片描述
      冷热分离 3 种触发逻辑优缺点对比表
      在这里插入图片描述
      根据以上表格内容对比,我们可以得出每种触发逻辑的建议场景。

    4. 修改写操作的业务代码:建议在业务代码比较简单,并且不按照时间区分冷热数据时使用。

    5. 监听数据库变更日志:建议在业务代码比较复杂,不敢随意变更,并且不按照时间区分冷热数据时使用。

    6. 定时扫描数据库:建议在按照时间区分冷热数据时使用。

    如何分离冷热数据?

    分离冷热数据的基本逻辑如下:

    1. 判断数据是冷是热;
    2. 将要分离的数据插入冷数据库中;
    3. 再从热数据库中删除分离的数据。

    在这里插入图片描述

    该注意的问题

    1. 一致性:同时修改多个数据库,如何保证数据的一致性?
      一致性的解决方案为只要保证每一步都可以重试且操作都有幂等性就行,具体逻辑分为四步。
      (1)在热数据库中,给要搬的数据加个标识: ColdFlag=WaittingForMove。(实际处理中标识字段的值用数字就可以,这里是为了方便理解。)
      (2)找出所有待搬的数据(ColdFlag=WaittingForMove):这步是为了确保前面有些线程因为部分原因失败,出现有些待搬的数据没有搬的情况。
      (3)在冷数据库中保存一份数据,但在保存逻辑中需加个判断以此保证幂等性(这里需要用事务包围起来),通俗点说就是假如我们保存的数据在冷数据库已经存在了,也要确保这个逻辑可以继续进行。
      (4)从热数据库中删除对应的数据。

    数据量:假设数据量大,一次性处理不完,该怎么办?是否需要使用批量处理?

    我们每次可以搬 50 条数据:

    1. 在热数据库中给要搬的数据加个标识:ColdFlag=WaittingForMove;
    2. 找出前 50 条待搬的数据(ColdFlag=WaittingForMove);
    3. 在冷数据库中保存一份数据;
    4. 从热数据库中删除对应的数据;
    5. 循环执行 b。

    并发性:假设数据量大到要分到多个地方并行处理,该怎么办?

    在定时搬运冷热数据的场景里(比如每天),假设每天处理的数据量大到连单线程批量处理都来不及,我们该怎么办?这时我们就可以开多个线程并发处理了。(虽然大部分情况下多线程较快,但我曾碰到过这种情况:当单线程 batch size 到一定数值时效率特别高,比多线程任何 batch size 都快。所以,希望同学们到时多留意下,如果遇到多线程速度不快,我们就考虑控制单线程。)

    1. 如何启动多线程。有两种方式,一种多进程中的多线程。即设置多个定时器,每个定时器间隔短一些,每次定时启动一个线程。一种是单进程中的多线程,即线程池。先计算待搬动的热数据的数量,再计算要同时启动的线程数,如果大于线程池的数量就取线程池的线程数,假设这个数量为 N,最后循环 N 次启动线程池的线程搬运冷热数据。
    2. 某线程宣布某个数据正在操作,其他线程不要动(锁)。
      关于这个逻辑,我们需要考虑 3 个特性。
      (1)获取锁的原子性: 当一个线程发现某个待处理的数据没有加锁,然后给它加锁,这 2 步操作必须是原子性的,即要么一起成功,要么一起失败。实际操作为先在表中加上 LockThread 和 LockTime 两个字段,然后通过一条 SQL 语句找出待迁移的未加锁或锁超时的数据,再更新 LockThread=当前线程,LockTime=当前时间,最后利用 MySQL 的更新锁机制实现原子性。
      (2)获取锁必须与开始处理保证一致性: 当前线程开始处理这条数据时,需要再次检查下操作的数据是否由当前线程锁定成功,实际操作为再次查询一下 LockThread= 当前线程的数据,再处理查询出来的数据。
      (3)释放锁必须与处理完成保证一致性: 当前线程处理完数据后,必须保证锁释放出去。
    3. 某线程失败退出了,结果锁没释放怎么办(锁超时)?
      锁无法释放: 如果锁定这个数据的线程异常退出了且来不及释放锁,导致其他线程无法处理这个数据,此时该怎么办?解决方案为给锁设置一个超时时间,如果锁超时了还未释放,其他线程可正常处理该数据。
      设置超时时间时,我们还应考虑如果正在处理的线程并未退出,因还在处理数据导致了超时,此时又该怎么办?解决方案为尽量给超时的时间设置成超过处理数据的合理时间,且处理冷热数据的代码里必须保证是幂等性的。
      最后,我们还得考虑一个极端情况:如果当前线程还在处理数据,此时正在处理的数据的锁超时了,另外一个线程把正在处理的数据又进行了加锁,此时该怎么办?我们只需要在每一步加判断容错即可,因为搬运冷热数据的代码比较简单,通过这样的操作当前线程的处理就不会破坏数据的一致性。
      考虑到前面逻辑比较复杂,这里我们特地画了一个分离的流程图,如下图所示:
      在这里插入图片描述

    如何使用冷热数据?

    在界面上可以使用查询条件进行区分,比如最近3个月就是热数据,这种情况我们可以在业务代码中来区分,前提是不允许同时读冷热库的需求。

    整体方案

    在这里插入图片描述

    历史数据迁移

    一般而言,只要跟持久化层有关的架构方案,我们都需要考虑历史数据的迁移问题。因为前面的分离逻辑在考虑失败重试的场景时,刚好覆盖了这个问题,所以这个问题的解决方案也很简单,我们只需要给所有的历史数据加上标识:ColdFlag=WaittingForMove 后,程序就会自动迁移了。

    场景2:查询分离

    冷热分离存在诸多不足,比如:查询冷数据慢、业务无法再修改冷数据、冷数据多到一定程度系统依旧扛不住,我们如果想把这些问题一一解决掉,可以用另外一种解决方案——查询分离。
    如:SaaS 客服系统的架构优化,系统里有一个工单查询功能,工单表中存放了几千万条数据,且查询工单表数据时需要关联十几个子表,每个子表的数据也是超亿条。
    加上工单表中有些数据是几年前的,客户说这些数据涉及诉讼问题,需要继续保持更新,因此我们无法将这些旧数据封存到别的地方,也就没法通过前面的冷热分离方案来解决。
    我们采用了查询分离的解决方案,才得以将这个问题顺利解决:将更新的数据放在一个数据库里,而查询的数据放在另外一个系统里。因为数据的更新都是单表更新,不需要关联也没有外键,所以更新速度立马得到提升,数据的查询则通过一个专门处理大数据量的查询引擎来解决,也快速地满足了客户的需求。

    何种场景下使用查询分离?

    1. 数据量大;
    2. 所有写数据的请求效率尚可;
    3. 查询数据的请求效率很低;
    4. 所有的数据任何时候都可能被修改;
    5. 业务希望我们优化查询数据的功能。

    查询分离实现思路

    如何触发查询分离?

    (1) 修改业务代码:在写入常规数据后,同步写入重库。
    在这里插入图片描述
    (2) 修改业务代码:在写入常规数据后,异步建立查询数据。可以通过MQ实现异步。
    在这里插入图片描述
    (3) 监控数据库日志:如有数据变更,更新查询数据。
    在这里插入图片描述
    通过观察以上 3 种触发逻辑示意图,你发现了什么吗?3 种触发逻辑的优缺点对比表如下:
    在这里插入图片描述
    什么叫业务逻辑灵活可控? 举个例子:一般来说,写业务代码的人能从业务逻辑中快速判断出何种情况下更新查询数据,而监控数据库日志的人并不能将全部的数据库变更分支穷举,再把所有的可能性关联到对应的更新查询数据逻辑中,最终导致任何数据的变更都需要重新建立查询数据。

    什么叫减缓写操作速度? 建立查询数据的一个动作能减缓多少写操作速度?答案:很多。举个例子:当你只是简单更新了订单的一个标识,本来查询数据时间只需要 2ms,而在查询数据时可能会涉及重建(比如使用 ES 查询数据时会涉及索引、分片、主从备份,其中每个动作又细分为很多子动作,这些内容后面我们会讲到),这时建立查询数据的过程可能就需要 1s 了,从 2ms 变成 1s,你说减缓幅度大不大?

    查询数据更新前,用户可能查询到过时数据。 这里我们结合第 2 种触发逻辑来讲,比如某个操作正处于订单更新状态,状态更新时会通过异步更新查询数据,更新完后订单才从“待审核”状态变为“已审核”状态。假设查询数据的更新时间需要 1 秒,这 1 秒中如果用户正在查询订单状态,这时主数据虽然已变为“已审核”状态,但最终查询的结果还是显示“待审核”状态。

    根据前面的对比表,总结每种触发逻辑的适用场景如下:
    在这里插入图片描述

    如何实现查询分离?

    下面重点讲解第2种方式。

    1. 写操作较多且线程太多,最终撑爆 JVM;
    2. 建查询数据的线程出错了,如何自动重试;
    3. 多线程并发时,很多并发场景需要解决。
      面对以上三种情况,此时使用 MQ 管理这些线程即可解决。
      MQ 的具体操作思路为每次主数据写操作请求处理时,都会发一个通知给 MQ,MQ 收到通知后唤醒一个线程更新查询数据,示意图如下:
      在这里插入图片描述
      问题一:MQ 如何选型?
      (1)召集技术中心所有能做技术决策的人共同投票选型。
      (2)不管我们选择哪个 MQ ,最终都能实现想要的功能,只不过是易用不易用、多写少写业务代码的问题,因此我们从易用性和代码工作量角度考量即可。
      问题二:MQ 宕机了怎么办?
      (1)每次写操作时,在主数据中加个标识:NeedUpdateQueryData=true,这样发到 MQ 的消息就很简单,只是一个简单的信号告知更新数据,并不包含更新的数据 id。
      (2)MQ 的消费者获取信号后,先批量查询待更新的主数据,然后批量更新查询数据,更新完后查询数据的主数据标识 NeedUpdateQueryData 就更新成 false 了。
      (3)当然还存在多个消费者同时搬运动作的情况,这就涉及并发性的问题,因此问题与 01 讲冷热分离中的并发性处理逻辑类似。
      (4)但如果一直失败,我们可以在主数据中多添加一个尝试搬运次数,比如每次尝试搬运时 +1,成功后就清零,以此监控那些尝试搬运次数过多的数据。
      问题三:消息的幂等消费
      RocketMQ和状态控制可以完成幂等操作。

    MQ 的作用
    服务的解耦: 这样主业务逻辑就不会依赖更新查询数据这个服务了。
    控制更新查询数据服务的并发量: 如果我们直接调用更新查询数据服务,因写操作速度快,更新查询数据速度慢,写操作一旦并发量高,会给更新查询数据服务造成超负荷压力。如果通过消息触发更新查询数据服务,我们就可以通过控制消息消费者的线程数来控制负载。

    查询数据如何存储?

    我们应该使用什么技术存储查询数据呢?目前,市面上主要使用 Elasticsearch 实现大数据量的搜索查询,当然还可能会使用到 MongoDB、HBase 这些技术,这就需要我们对各种技术的特性了如指掌,再进行技术选型。比如当初我们设计架构方案时,为什么选择用 Elasticsearch,除 ES 对查询的扩展性支持外,最关键的一点是我们团队对 Elasticsearch 很熟悉。

    查询数据如何使用?

    查询数据不一致怎么办?**这里我分享 2 种解决思路。

    1. 在查询数据更新到最新前,不允许用户查询。(我们没用过这种设计,但我确实见过市面上有这样的设计。)
    2. 给用户提示:您目前查询到的数据可能是 1 秒前的数据,如果发现数据不准确,可以尝试刷新一下,这种提示用户一般比较容易接受。

    整体方案

    在这里插入图片描述

    历史数据迁移

    在这个方案里,我们只需要把所有的历史数据加上这个标识:NeedUpdateQueryData=true,程序就会自动处理了。

    查询分离解决方案的不足

    查询分离这个解决方案虽然能解决一些问题,但我们也要清醒地认识到它的不足。
    不足一: 使用 Elasticsearch 存储查询数据时,注意事项是什么(此方案并未详细展开)?
    不足二: 主数据量越来越大后,写操作还是慢,到时还是会出问题。
    不足三: 主数据和查询数据不一致时,假设业务逻辑需要查询数据保持一致性呢?

    Elasticsearch 注意的三点

    对于 Elasticsearch 而言,我们想掌握好这门技术,除需要对它的用法了如指掌外,还需要对技术中的各种坑了然于心。因此,03 讲我结合个人实战经验总结出了关于 ES 的 3 大知识要点:如何使用 Elasticsearch 设计表结构?Elasticsearch 如何修改表结构?ES 的坑有哪些?

    如何使用Elasticsearch 设计表结构?

    我们知道 ES 是基于索引的设计,它没办法像 MySQL 那样使用 join 查询,所以,查询数据时我们需要把每条主数据及关联子表的数据全部整合在一条记录中。

    ES 的存储结构

    (1)Lucene 和 MySQL 的概念对比
    在这里插入图片描述
    (2)无结构文档的倒排索引(Index)
    在这里插入图片描述
    简单倒排索引后,显示的结果如下表所示:
    在这里插入图片描述
    我们发现:无结构的文档经过简单的倒排索引后,字典表主要存放关键字,而倒排表存放该关键字所在的文档 id。
    (3)有结构文档的倒排索引(Index)
    再来举一个更复杂的例子,比如每个 Doc 都有多个 Field,Field 有不同的值(包含不同的 Term),倒排索引的结构参考如下图所示:
    在这里插入图片描述
    也就是说:有结构的文档经过倒排索引后,字段中的每个值都是一个关键字,存放在左边的 Term Dictionary(词汇表)中,且每个关键字都有对应地址指向所在文档。

    (4) ES 的 Document 怎么定义结构和字段格式

    {
     "mappings": {
     "doc": {
        "properties": {
        "order_id": {
        "type": "text"
     },
        "order_invoice": {
        "type": "nested"
     },
        "order_product_item": {
        "type": "nested",
        "properties": {
        "product_count": {
        "type": "long"
     },
        "product_name": {
        "type": "text"
     },
        "product_price": {
        "type": "float"
     }
     }
     },
        "total_amount": {
        "type": "long"
     },
        "user": {
        "properties": {
        "user_id": {
        "type": "text"
     },
        "user_name": {
        "type": "text"
     }
     }
     }
     }
     }
     }
    }
    
    

    Elasticsearch如何修改表结构?

    我们使用 reindex 功能会导致原来保存的旧字段名的索引数据失效,这种情况该如何解决?此时我们可以使用 alias 索引功能,代码示例如下:

    PUT trips
    {
      "mappings": {
        "properties": {
          "distance": {
            "type": "long"
          },
          "route_length_miles": {
            "type": "alias",
            "path": "distance" 
          },
          "transit_mode": {
            "type": "keyword"
          }
        }
      }
    }
    
    

    说到修改表结构,使用普通 MySQL 时,我并不建议直接修改字段的类型,改名或删字段。因为每次更新版本时,我们都要做好版本回滚的打算,为此设计每个版本对应数据库时,我们会尽量兼容前面版本的代码。

    因 ES 的结构基于 MySQL 而设计,两者之间存在对应关系,所以我也不建议直接修改 ES 的表结构。

    那如果我们真有修改的需求呢?一般而言,我们会先保留旧的字段,然后直接添加并使用新的字段,直到新版本的代码全部稳定工作后,我们再找机会清理旧的不用的字段,即分成 2 个版本完成修改需求。

    ES 的坑有哪些?

    坑一:ES 是准实时的?

    当你更新数据至 ES 且返回成功提示(注意这一瞬间),你会发现通过 ES 查询返回的数据仍然不是最新的。

    数据索引整个过程因涉及 ES 的分片,Lucene Index、Segment、 Document 的三者之间关系等知识点,所以我们有必要先把这部分内容串起来说明。

    ES 的一个分片(这里跳过 ES 分片相关介绍)就是一个 Lucene Index,每一个 Lucene Index 由多个 Segment 构成,即 Lucene Index 的子集就是 Segment,如下图所示:

    在这里插入图片描述
    关于 Lucene Index、Segment、 Document 三者之间的关系,你看完下面这张图就一目了然了,如下图所示:

    在这里插入图片描述
    通过上面这个图,我们知道一个 Lucene Index 可以存放多个 Segment,而每个 Segment 又可以存放多个 Document。

    掌握了以上基础知识点,接下来就进入正题——数据索引的过程详解。

    第一步:当新的 Document 被创建,数据首先会存放到新的 Segment 中,同时旧的 Document 会被删除,并在原来的 Segment 上标记一个删除标识。当 Document 被更新,旧版 Document 会被标识为删除,并将新版 Document 存放新的 Segment 中。
    第二步:Shard 收到写请求时,请求会被写入 Translog 中,然后 Document 被存放 memory buffer (注意:memory buffer 的数据并不能被搜索到)中,最终 Translog 保存所有修改记录。
    在这里插入图片描述
    第三步:每隔 1 秒(默认设置),refresh 操作被执行一次,且 memory buffer 中的数据会被写入一个 Segment 并存放 filesystem cache 中,这时新的数据就可以被搜索到了,如下图所示:
    在这里插入图片描述
    通过以上数据索引过程的说明,我们发现 ES 并不是实时的,而是有 1 秒延时,因延时问题的解决方案我们在 02 讲中介绍过,提示用户查询的数据会有一定延时即可。

    坑二:ES 宕机恢复后,数据丢失
    在数据索引的过程这部分内容,我们提及了每隔 1 秒(根据配置),memory buffer 中的数据会被写入 Segment 中,此时这部分数据可被用户搜索到,但没有被持久化,一旦系统宕机了,数据就会丢失。

    比如下图中灰色的桶,目前它可被搜索到,但还没有持久化,一旦 ES 宕机,数据将会丢失。
    在这里插入图片描述

    如何防止数据丢失呢?使用 Lucene 中的 commit 操作就能轻松解决这个问题。
    commit 具体操作:先将多个 Segment 合并保存到磁盘中,再将灰色的桶变成上图中绿色的桶。
    不过,使用 commit 操作存在一点不足:耗 IO,从而引发 ES 在 commit 之前宕机的问题。一旦系统在 translog fsync 之前宕机,数据也会直接丢失,如何保证 ES 数据的完整性便成了亟待解决的问题。
    遇到这种情况,我们采用 translog 解决就行,因为 Translog 中的数据不会直接保存在磁盘中,只有 fsync 后才保存,这里我分享两种 Translog 解决方案。
    第一种:将 Index.translog.durability 设置成 request ,如果我们发现系统运行得不错,采用这种方式即可;
    第二种:将 Index.translog.durability 设置成 fsync,每次 ES 宕机启动后,先将主数据和 ES 数据进行对比,再将 ES 缺失的数据找出来。

    强调一个知识点:Translog 何时会 fsync ?当 Index.translog.durability 设置成 request 后,每个请求都会 fsync,不过这样影响 ES 性能。这时我们可以把 Index.translog.durability 设置成 fsync,那么每隔 Index.translog.sync_interval 后,每个请求才会 fsync 一次。

    坑三:分页越深,查询效率越慢
    ES 分页这个坑的出现,与 ES 的读操作请求的处理流程密切关联,为此我们有必要先深度剖析下 ES 的读操作请求的处理流程,如下图所示:
    在这里插入图片描述
    关于 ES 的读操作流程主要分为两个阶段:Query Phase、Fetch Phase。
    Query Phase: 协调的节点先把请求分发到所有分片,然后每个分片在本地查询建一个结果集队列,并将命令中的 Document id 以及搜索分数存放队列中,再返回给协调节点,最后协调节点会建一个全局队列,归并收到的所有结果集并进行全局排序。
    Query Phase 我需要强调:在 ES 查询过程中,如果 search 带了 from 和 size 参数,Elasticsearch 集群需要给协调节点返回 shards number * (from + size) 条数据,然后在单机上进行排序,最后给客户端返回 size 大小的数据。比如客户端请求 10 条数据(比如 3 个分片),那么每个分片则会返回 10 条数据,协调节点最后会归并 30 条数据,但最终只返回 10 条数据给客户端。
    Fetch Phase: 协调节点先根据结果集里的 Document id 向所有分片获取完整的 Document,然后所有分片返回完整的 Document 给协调节点,最后协调节点将结果返回给客户端。(关于什么是协调节点,我们先忽略它。)

    在整个 ES 的读操作流程中,Elasticsearch 集群实际上需要给协调节点返回 shards number * (from + size) 条数据,然后在单机上进行排序,最后返回给客户端这个 size 大小的数据。

    比如有 5 个分片,我们需要查询排序序号从 10000 到 10010(from=10000,size=10)的结果,每个分片到底返回多少数据给协调节点计算呢?告诉你不是 10 条,是 10010 条。也就是说,协调节点需要在内存中计算 10010*5=50050 条记录,所以在系统使用中,如果用户分页越深查询速度会越慢,也就是说并不是分页越多越好。

    那如何更好地解决 ES 分页问题呢?为了控制性能,我们主要使用 ES 中的 max_result_window 配置,这个数据默认为 10000,当 from+size > max_result_window ,ES 将返回错误。

    由此可见,在系统设计时,我们一般需要控制用户翻页不能太深,而这在现实场景中用户也能接受,这也是我之前方案采用的设计方式。要是用户确实有深度翻页的需求,我们再使用 ES 中search_after 的功能也能解决,不过就是无法实现跳页了。

    我们举一个例子,查询按照订单总金额分页,上一页最后一条 order 的总金额 total_amount 是 10,那么下一页的查询示例代码如下:

    {
        "query": {
            "bool": {
                "must": [
                    {
                        "term": {
                            "user.user_name.keyword": "李大侠"
                        }
                    }
                ],
                "must_not": [],
                "should": []
            }
        },
        "from": 0,
        "size": 2,
        "search_after": [
            "10"
        ],
        "sort": [
            {
                "total_amount": "asc"
            }
        ],
        "aggs": {}
    }
    
    

    这个 search_after 里的值,就是上次查询结果的排序字段的结果值。

    分表分库:单表数据量大读写缓慢如何解决?

    京东大客户也有采用Client模式,即采用sharding-jdbc。关于sharding-jdbc的更全面的知识点,可以参考相关章节。
    在这里插入图片描述
    学到这,我们已经知道市面上开源中间件的设计模式,那我们到底该选择哪种模式呢?简单对比下这 2 个模式的优缺点,你就能知道答案了。
    在这里插入图片描述

    业务代码如何修改?

    近年来,分表分库操作愈发容易,不过我们需要注意几个要点。

    1. 我们已经习惯微服务了,对于特定表的分表分库,其影响面只在该表所在的服务中,如果是一个单体架构的应用做分表分库,那真是伤脑筋。
    2. 在互联网架构中,我们基本不使用外键约束。
    3. 随着查询分离的流行,后台系统中有很多操作需要跨库查询,导致系统性能非常差,这时分表分库一般会结合查询分离一起操作:先将所有数据在 ES 索引一份,再使用 ES 在后台直接查询数据。如果订单详情数据量很大,还有一个常见做法,即先在 ES 中存储索引字段(作为查询条件的字段),再将详情数据存放在 HBase 中(这个方案我们就不展开了)。

    一般来说,业务代码的修改不会很复杂,最麻烦的是历史数据的迁移。

    历史数据迁移?

    在这里插入图片描述
    此数据迁移方案的基本思路:存量数据直接迁移,增量数据监听 binlog,然后通过 canal 通知迁移程序搬运数据,新的数据库拥有全量数据,且校验通过后逐步切换流量。

    数据迁移解决方案详细的步骤如下:

    1. 上线 canal,通过 canal 触发增量数据的迁移;
    2. 迁移数据脚本测试通过后,将老数据迁移到新的分表分库中;
    3. 注意迁移增量数据与迁移老数据的时间差,确保全部数据都被迁移过去,无遗漏;
    4. 第二步、第三步都运行完后,新的分表分库中已经拥有全量数据了,这时我们可以运行数据验证的程序,确保所有数据都存放在新数据库中;
    5. 到这步数据迁移就算完成了,之后就是新版本代码上线了,至于是灰度上还是直接上,需要根据你们的实际情况决定,回滚方案也是一样。

    未来的扩容方案是什么?

    随着业务的发展,如果原来的分片设计已经无法满足日益增长的数据需求,我们就需要考虑扩容了,扩容方案主要依赖以下两点。
    分片策略是否可以让新表数据的迁移源只是 1 个旧表,而不是多个旧表,这就是前面我们建议使用 2 的 N 次方分表的原因;
    数据迁移:我们需要把旧分片的数据迁移到新的分片上,这个方案与上面提及的历史数据迁移一样,我们就不重复啰唆了。

    展开全文
  • 持久化和持久

    2021-05-22 09:07:53
    所谓持久就是专门负责持久化工作的逻辑,由它统一与数据库打交道。这样一来,便可以将以前的三模型(表示、业务逻辑和数据库)修改成四模型(表示、业务逻辑、持久和数据库)。四模型的...
  • 一、概述-临时状态:刚用new语句创建对象,还没有被持久化,并且不处于Session缓存中。处于临时状态的java对象被称为临时对象。-持久化状态:已经被持久化,并且加入到Session的缓存中。处于持久化状态的java对象被...
  • 介绍bean注解标识表现层、业务层、持久化层关键代码://bean@ComponentpublicclassUser{}//表现层@ControllerpublicclassUserController{}//业务层@ServicepublicclassUserService{}//持久化层(可以重写名称)@...
  • Spring的JdbcTemplate可以被看作是一个小型的轻量级持久化层框架,为了使JDBC操作更加便捷,Spring在JDBC API上定义了一个抽象层,以此来建立了一个JDBC存取框架。 它作为Spring JDBC框架的核心, 设计目的是为不同...
  • 对于需要被持久化的Java对象,在它的生命周期中,可处于以下三个状态之一:(1) 临时状态(transient):刚刚用new语句创建,还没有被持久化,不处于Session的缓存中。处于临时状态的Java对象被称为临时对象。(2) 持久....
  • 什么是持久化? 要想说明这个名词,还要从上世纪70年代说起,数据库技术兴起,这时的软件结构发展为双层结构。在双层结构中实现了的数据存放与应用程序分离,构成了现代软件模型的雏形。但是,随着软件体量越来越大...
  • 使用mybatis拦截器实现业务层和持久化层的数据处理、加密、解密、脱敏。(就像pagehelper分页插件实现路数一样),刚好有个公众号推了一篇文章苞米豆开发了商业项目mybatis-mate-starter,问了下需要收费(2021-11-...
  • 站在持久化层的角度,一个Java对象在它的生命周期中,可处于以下四个状态之一: (1) 临时状态(transient):刚用new语句创建,还没有被持久化,并且不处于持久 (2) 化缓存中。处于临时状态的Java对象被称为临时...
  • 经常需要将数据进行持久化,而我们的文件系统是最古老也是最可靠的保存方式。这里就给出一个在JAVA中把字符串保存到文件中的例子。如下:package test.base;import java.io.BufferedWriter;import java.io....
  • 为了实现web(struts)和持久层(Hibernate)之间的松散耦合,我们采用业务代表(Business Delegate)和DAO(Data Access Object)两种模式。DAO模式为了减少业务逻辑和数据访问逻辑之间的耦合,当一个持久曾框架被应用时...
  • 事务管理器派生类型 对应持久层版本 org.springframework.jdbc.datasource.DataSourceTransactionManager 使用Spring JDBC或者MyBatis时使用 org.springframework.orm.hibernate3.HibernateTransactionManager ...
  • 先说结论:2020年了,推荐mybatis-plus面向对象or程序逻辑和sql解耦?在之前hibernate和mybatis比较时,两者最主要的区别是hibernate用面相对象的方式解决数据库操作问题,sql相对来说是不透明的,甚至你换数据库都...
  • 灰小猿
  • 我们的 workers 可以理解为多进程/多线程,然后通过Celery来进行智能的调度,至于他怎么调度的我们暂时不关心,然后我们使用RabbitMQ作为消息队列缓存的中间服务,当我们接收到生产者的数据的时候就会统一访问到...
  • 序列定义:为了方便数据的传输,我们将对象转换为其他形式,比如字节的过程。 反序列定义:将其他形式的数据转换为对象的过程。 举例:在淘宝上买了一件衣服,卖家为了方便快递送货,将衣服打包,这个打包的过程...
  • java模拟数据持久化

    2021-02-26 16:27:21
    数据持久化可:把程序中的数据以某种形式保存到某存储介质中,以达到持久化的目的(把数据保存到硬盘或数据库中)序列化(Serialization)也叫串行化,是java内置的持久化java对象机制,只要某个类实现了java.io....
  • 的文章,文中比较了四种流行的持久化框架:CMP Entity EJBs、JPA、Hibernate和TopLink.Acharya讨论了每种技术并在一个表格中总结了他的结论,其结论归结为:JPA适合J2SE和J2EE的简单框架,并入了其他框架...
  • 什么是持久化

    2021-02-06 18:51:13
    有一个人揪着问我什么是数据持久化,简单来说:对于概念上来说,就是数据保存到硬盘系统重启可恢复,对于开发人员来说,就是对象保存到数据库。但是拿这个问题一直问下去的话,显然以上解释不足以应付,一狠心,我从...
  • MySQL 持久化保障机制

    2021-01-19 22:02:25
    为什么 InnoDB 引擎会引入 redo 日志作为中间来保证 MySQL 持久化,而不是直接持久化到磁盘?我们先来看看《MySQL实战45讲》中提到的一个故事。在《孔乙己》这篇文章,酒店掌柜有一个粉板,专门...
  • 考虑到我们项目微博卡以后的数据安全性问题,翻阅了很多关于持久化这块的资料。因为大家知道redis我们的项目打算使用Redis来做一些缓存和计数的工作,加上redis本身就支持pub/sub模式,设计消息系统也变得简单。...
  • Spark的三种持久化

    2021-02-25 19:34:22
    Spark的持久化有三种,使用时应当选择最合适的哪一个 cache 使用方法如下 var sprakconf=new SparkConf().setMaster("local").setAppName("log") var sc=new SparkContext(sprakconf) var linesRdd= sc.textFile...
  • [Java教程]持久化类(状态介绍)0 2018-09-15 12:00:31叙:之前介绍了持久化持久化类的相关信息,关于持久化类只讲了相关编写规则等(文章地址:https://www.cnblogs.com/Email-qtl777777/p/9650239.html),关于其...
  • 你必须得用 redis 的持久化机制,将数据写入内存的同时,异步的慢慢的将数据写入磁盘文件里,进行持久化。 如果 redis 宕机重启,自动从磁盘上加载之前持久化的一些数据就可以了,也许会丢失少许数据,但是至少不会...
  • 1:序列技术:序列的过程就是将对象写入字节流和从字节流中读取对象。将对象状态转换成字节流之后,可以用java.io包中的各种字节流类将其保存到文件中,可以通过管道或线程读取,或通过网络连接将对象数据发送到...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 280,300
精华内容 112,120
关键字:

持久化层