为什么从 MVC 到 DDD,架构的本质是什么? - 小傅哥 - 博客园

mikel阅读(432)

来源: 为什么从 MVC 到 DDD,架构的本质是什么? – 小傅哥 – 博客园

本文来自于小傅哥新编写的《Java简明教程》系列内容,本教程意在于通过简单、明了、清晰的成体系内容,教会Java学习伙伴,可以在学习后能进行Java项目开发。

今天要分享的是 MVC 和 DDD 的架构本质,通过由浅入深的介绍讲解和视频带着手把手操作创建工程架构。让无论是学习 MVC 的小白码农还是希望了解更多关于 DDD 内容的老白码农,都可以学习到一点自己需要的内容。

一、MVC 架构

如果我们尝试把编程的复杂架构缩小到最容易理解的程度,那么编程开发其实只做3件事:”定义属性创建方法调用展示“。但因为同类所需的内容较多,如一系列的属性,一堆的方法实现,一组的接口封装,那么就需要合理的把这些内容分配到不同的层次中去实现,因此有了分层架构的设计。

那么本文小傅哥会向大家介绍一套MVC架构的分层设计以及如何创建使用,并提供相应的简单的案例。你可以复制这套架构在自己的场景中使用,也更能方便编程的小白可以更快的上手开发。

注意:此套MVC架构模型适合提供HTTP服务的工程架构,适合简单的小场景开发使用。特点;轻便、简单、学习成本低。

1. 编程三步

如果说你是一个特别小的玩具项目,你甚至可以把编程的3步写到一个类里。但因为你做的是正经项目,你的各种类;对象类、库表类、方法类,就会成群结队的来。如果你想把这些成群结队的类的内容,都写到一个类里去,那么就是几万行的代码了。—— 当然你也可以吹牛逼,你一个人做过一个项目,这项目大到啥程度呢。就是有一个类里有上万行代码。

所以,为了不至于让一个类撑到爆💥,需要把黄色的对象、绿色的方法、红色的接口,都分配到不同的包结构下。这就是你编码人生中所接触到的第一个解耦操作。

2. 分层框架

MVC 是一种非常常见且常用的分层架构,主要包括;M – mode 对象层,封装到 domain 里。V – view 展示层,但因为目前都是前后端分离的项目,几乎不会在后端项目里写 JSP 文件了。C – Controller 控制层,对外提供接口实现类。DAO 算是单独拿出来用户处理数据库操作的层。

  • 如图,在 MVC 的分层架构下。我们编程3步的所需各类对象、方法、接口,都分配到 MVC 的各个层次中去。
  • 因为这样分层以后,就可以很清晰明了的知道各个层都在做什么内容,也更加方便后续的维护和迭代。
  • 对于一个真正的项目来说,是没有一锤子买卖的,最开始的开发远不是成本所在。最大的开发成本是后期的维护和迭代。而架构设计的意义更多的就是在解决系统的反复的维护和迭代时,如何降低成本,这也是架构分层的意义所在。

3. 调用流程

接下来我们再看下一套 MVC 架构中各个模块在调用时的串联关系;

  • 以用户发起 HTTP 请求开始,Controller 在接收到请求后,调用由 Spring 注入到类里的 Service 方法,进入 Service 方法后有些逻辑会走数据库,有些逻辑是直接内部自己处理后就直接返回给 Controller 了。最后由 Controller 封装结果返回给 HTTP 响应。
  • 同时我们也可以看到各个对象在这些请求间的一个作用,如;请求对象、库表对象、返回对象。

4. 架构源码

4.1 环境

  • JDK 1.8
  • Maven 3.8.6 – 下载安装maven后,本地记得配置阿里云镜像,方便快速拉取jar包。源码中 docs/maven/settings.xml 有阿里云镜像地址。
  • SpringBoot 2.7.2
  • MySQL 5.7 – 如果你使用 8.0 记得更改 pom.xml 中的 mySQL 引用

4.2 架构

.
├── docs
│   └── mvc.drawio - 架构文档
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── cn
│   │   │       └── bugstack
│   │   │           └── xfg
│   │   │               └── frame
│   │   │                   ├── Application.java
│   │   │                   ├── common
│   │   │                   │   ├── Constants.java
│   │   │                   │   └── Result.java
│   │   │                   ├── controller
│   │   │                   │   └── UserController.java
│   │   │                   ├── dao
│   │   │                   │   └── IUserDao.java
│   │   │                   ├── domain
│   │   │                   │   ├── po
│   │   │                   │   │   └── User.java
│   │   │                   │   ├── req
│   │   │                   │   │   └── UserReq.java
│   │   │                   │   ├── res
│   │   │                   │   │   └── UserRes.java
│   │   │                   │   └── vo
│   │   │                   │       └── UserInfo.java
│   │   │                   └── service
│   │   │                       ├── IUserService.java
│   │   │                       └── impl
│   │   │                           └── UserServiceImpl.java
│   │   └── resources
│   │       ├── application.yml
│   │       └── mybatis
│   │           ├── config
│   │           │   └── mybatis-config.xml
│   │           └── mapper
│   │               └── User_Mapper.xml
│   └── test
│       └── java
│           └── cn
│               └── bugstack
│                   └── xfg
│                       └── frame
│                           └── test
│                               └── ApiTest.java
└── road-map.sql

以上是整个🏭工程架构的 tree 树形图。整个工程由 SpringBoot 驱动。

  • Application.java 是启动程序的 SpringBoot 应用
  • common 是额外添加的一个层,用于定义通用的类
  • controller 控制层,提供接口实现。
  • dao 数据库操作层
  • domain 对象定义层
  • service 服务实现层

5. 测试验证

  • 首先;整个工程由 SpringBoot 驱动,提供了 road-map.sql 测试 SQL 库表语句。你可以在自己的本地mysql上进行执行。它会创建库表。
  • 之后;在 application.yml 配置数据库链接信息。
  • 之后就可以打开 ApiTest 进行测试了。你可以点击 Application 类的绿色箭头启动工程,使用 UserController 类提供接口的方式调用程序;http://localhost:8089/queryUserInfo

– 如果你正常获取了这样的结果信息,那么说明你已经启动成功。接下来就可以对照着MVC的结构进行学习,以及使用这样的工程结构开发自己的项目。

二、DDD 架构

从最早接触 DDD 架构,到后来用 DDD 架构不断的承接项目开发,一次次在项目开发中的经验积累。对 DDD 有了不少的理解。DDD 是一种思想,落地的形态和结构会有不同的方式,甚至在编码上也会有风格的差异。但终期目标就一个;”提供代码的可维护性,降低迭代开发成本。“也是康威定律所述:”任何组织在设计一套系统时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。“

但 DDD 与 MVC 相比的概率较多,贸然用理论驱动代码开发,会让整个工程变得非常混乱,甚至可能虽然是用的 DDD 但最后写出来了一片四不像的 MVC 代码。所以对于程序员👨🏻‍💻来说,先能上手一个工程,在从工程了解理论会更加容易。为此小傅哥想以此文,通过实战编码的方式向大家分享 DDD 架构,并能让大家上手的 DDD 架构。

1. 问题碰撞

你用 MVC 写代码,遇到过最大的问题是什么?🤔

简单、容易、好理解,是 MVC 架构的特点,但也正因为简单的分层逻辑,在适配较复杂的场景并且需要长周期的维护时,代码的迭代成本就会越来越高。如图;

  • 如果你接触过较大型且已经长期维护项目的 MVC 架构,你就会发现这里的 DAO、PO、VO 对象,在 Service 层相互调用。那么长期开发后,就导致了各个 PO 里的属性字段数量都被撑的特别大。这样的开发方式,将”状态”“行为“分离到不同的对象中,代码的意图渐渐模糊,膨胀、臃肿和不稳定的架构,让迭代成本增加。
  • 而 DDD 架构首先以解决此类问题为主,将各个属于自己领域范围内的行为和逻辑封装到自己的领域包下处理。这也是 DDD 架构设计的精髓之一。它希望在分治层面合理切割问题空间为更小规模的若干子问题,而问题越小就容易被理解和处理,做到高内聚低耦合。这也是康威定律所提到的,解决复杂场景的设计主要分为:分治、抽象和知识。

2. 简化理解

在给大家讲解 MVC 架构的时候,小傅哥提到了一个简单的开发模型。开发代码可以理解为:“定义属性 -> 创建方法 -> 调用展示”但这个模型结构过于简单,不太适合运用了各类分布式技术栈以及更多逻辑的 DDD 架构。所以在 DDD 这里,我们把开发代码可以抽象为:“触发 -> 函数 -> 连接” 如图;

  • DDD 架构常用于微服务场景,因此也一个系统的调用方式就不只是 HTTP 还包括;RPC 远程MQ 消息TASK 任务,因此这些种方式都可以理解为触发。
  • 通过触发调用函数方法,我们这里可以把各个服务都当成一个函数方法来看。而函数方法通过连接,调用到其他的接口、数据库、缓存来完成函数逻辑。

接下来,小傅哥在带着大家把这些所需的模块,拆分到对应的DDD系统架构中。

3. 架构分层

如下是 DDD 架构的一种分层结构,也可以有其他种方式,核心的重点在于适合你所在场景的业务开发。以下的分层结构,是小傅哥在使用 DDD 架构多种的方式开发代码后,做了简化和处理的。右侧的连线是各个模块的依赖关系。接下来小傅哥就给大家做一下模块的介绍。

  • 接口定义 – xfg-frame-api:因为微服务中引用的 RPC 需要对外提供接口的描述信息,也就是调用方在使用的时候,需要引入 Jar 包,让调用方好能依赖接口的定义做代理。
  • 应用封装 – xfg-frame-app:这是应用启动和配置的一层,如一些 aop 切面或者 config 配置,以及打包镜像都是在这一层处理。你可以把它理解为专门为了启动服务而存在的。
  • 领域封装 – xfg-frame-domain:领域模型服务,是一个非常重要的模块。无论怎么做DDD的分层架构,domain 都是肯定存在的。在一层中会有一个个细分的领域服务,在每个服务包中会有【模型、仓库、服务】这样3部分。
  • 仓储服务 – xfg-frame-infrastructure:基础层依赖于 domain 领域层,因为在 domain 层定义了仓储接口需要在基础层实现。这是依赖倒置的一种设计方式。
  • 领域封装 – xfg-frame-trigger:触发器层,一般也被叫做 adapter 适配器层。用于提供接口实现、消息接收、任务执行等。所以对于这样的操作,小傅哥把它叫做触发器层。
  • 类型定义 – xfg-frame-types:通用类型定义层,在我们的系统开发中,会有很多类型的定义,包括;基本的 Response、Constants 和枚举。它会被其他的层进行引用使用。
  • 领域编排【可选】 – xfg-frame-case:领域编排层,一般对于较大且复杂的的项目,为了更好的防腐和提供通用的服务,一般会添加 case/application 层,用于对 domain 领域的逻辑进行封装组合处理。

4. 架构源码

4.1 环境

  • JDK 1.8
  • Maven 3.8.6
  • SpringBoot 2.7.2
  • MySQL 5.7 – 如果你使用 8.0 记得更改 pom.xml 中的 mysql 引用

4.2 架构

.
├── README.md
├── docs
│   ├── dev-ops
│   │   ├── environment
│   │   │   └── environment-docker-compose.yml
│   │   ├── siege.sh
│   │   └── skywalking
│   │       └── skywalking-docker-compose.yml
│   ├── doc.md
│   ├── sql
│   │   └── road-map.sql
│   └── xfg-frame-ddd.drawio
├── pom.xml
├── xfg-frame-api
│   ├── pom.xml
│   ├── src
│   │   └── main
│   │       └── java
│   │           └── cn
│   │               └── bugstack
│   │                   └── xfg
│   │                       └── frame
│   │                           └── api
│   │                               ├── IAccountService.java
│   │                               ├── IRuleService.java
│   │                               ├── model
│   │                               │   ├── request
│   │                               │   │   └── DecisionMatterRequest.java
│   │                               │   └── response
│   │                               │       └── DecisionMatterResponse.java
│   │                               └── package-info.java
│   └── xfg-frame-api.iml
├── xfg-frame-app
│   ├── Dockerfile
│   ├── build.sh
│   ├── pom.xml
│   ├── src
│   │   ├── main
│   │   │   ├── bin
│   │   │   │   ├── start.sh
│   │   │   │   └── stop.sh
│   │   │   ├── java
│   │   │   │   └── cn
│   │   │   │       └── bugstack
│   │   │   │           └── xfg
│   │   │   │               └── frame
│   │   │   │                   ├── Application.java
│   │   │   │                   ├── aop
│   │   │   │                   │   ├── RateLimiterAop.java
│   │   │   │                   │   └── package-info.java
│   │   │   │                   └── config
│   │   │   │                       ├── RateLimiterAopConfig.java
│   │   │   │                       ├── RateLimiterAopConfigProperties.java
│   │   │   │                       ├── ThreadPoolConfig.java
│   │   │   │                       ├── ThreadPoolConfigProperties.java
│   │   │   │                       └── package-info.java
│   │   │   └── resources
│   │   │       ├── application-dev.yml
│   │   │       ├── application-prod.yml
│   │   │       ├── application-test.yml
│   │   │       ├── application.yml
│   │   │       ├── logback-spring.xml
│   │   │       └── mybatis
│   │   │           ├── config
│   │   │           │   └── mybatis-config.xml
│   │   │           └── mapper
│   │   │               ├── RuleTreeNodeLine_Mapper.xml
│   │   │               ├── RuleTreeNode_Mapper.xml
│   │   │               └── RuleTree_Mapper.xml
│   │   └── test
│   │       └── java
│   │           └── cn
│   │               └── bugstack
│   │                   └── xfg
│   │                       └── frame
│   │                           └── test
│   │                               └── ApiTest.java
│   └── xfg-frame-app.iml
├── xfg-frame-ddd.iml
├── xfg-frame-domain
│   ├── pom.xml
│   ├── src
│   │   └── main
│   │       └── java
│   │           └── cn
│   │               └── bugstack
│   │                   └── xfg
│   │                       └── frame
│   │                           └── domain
│   │                               ├── order
│   │                               │   ├── model
│   │                               │   │   ├── aggregates
│   │                               │   │   │   └── OrderAggregate.java
│   │                               │   │   ├── entity
│   │                               │   │   │   ├── OrderItemEntity.java
│   │                               │   │   │   └── ProductEntity.java
│   │                               │   │   ├── package-info.java
│   │                               │   │   └── valobj
│   │                               │   │       ├── OrderIdVO.java
│   │                               │   │       ├── ProductDescriptionVO.java
│   │                               │   │       └── ProductNameVO.java
│   │                               │   ├── repository
│   │                               │   │   ├── IOrderRepository.java
│   │                               │   │   └── package-info.java
│   │                               │   └── service
│   │                               │       ├── OrderService.java
│   │                               │       └── package-info.java
│   │                               ├── rule
│   │                               │   ├── model
│   │                               │   │   ├── aggregates
│   │                               │   │   │   └── TreeRuleAggregate.java
│   │                               │   │   ├── entity
│   │                               │   │   │   ├── DecisionMatterEntity.java
│   │                               │   │   │   └── EngineResultEntity.java
│   │                               │   │   ├── package-info.java
│   │                               │   │   └── valobj
│   │                               │   │       ├── TreeNodeLineVO.java
│   │                               │   │       ├── TreeNodeVO.java
│   │                               │   │       └── TreeRootVO.java
│   │                               │   ├── repository
│   │                               │   │   ├── IRuleRepository.java
│   │                               │   │   └── package-info.java
│   │                               │   └── service
│   │                               │       ├── engine
│   │                               │       │   ├── EngineBase.java
│   │                               │       │   ├── EngineConfig.java
│   │                               │       │   ├── EngineFilter.java
│   │                               │       │   └── impl
│   │                               │       │       └── RuleEngineHandle.java
│   │                               │       ├── logic
│   │                               │       │   ├── BaseLogic.java
│   │                               │       │   ├── LogicFilter.java
│   │                               │       │   └── impl
│   │                               │       │       ├── UserAgeFilter.java
│   │                               │       │       └── UserGenderFilter.java
│   │                               │       └── package-info.java
│   │                               └── user
│   │                                   ├── model
│   │                                   │   └── valobj
│   │                                   │       └── UserVO.java
│   │                                   ├── repository
│   │                                   │   └── IUserRepository.java
│   │                                   └── service
│   │                                       ├── UserService.java
│   │                                       └── impl
│   │                                           └── UserServiceImpl.java
│   └── xfg-frame-domain.iml
├── xfg-frame-infrastructure
│   ├── pom.xml
│   ├── src
│   │   └── main
│   │       └── java
│   │           └── cn
│   │               └── bugstack
│   │                   └── xfg
│   │                       └── frame
│   │                           └── infrastructure
│   │                               ├── dao
│   │                               │   ├── IUserDao.java
│   │                               │   ├── RuleTreeDao.java
│   │                               │   ├── RuleTreeNodeDao.java
│   │                               │   └── RuleTreeNodeLineDao.java
│   │                               ├── package-info.java
│   │                               ├── po
│   │                               │   ├── RuleTreeNodeLineVO.java
│   │                               │   ├── RuleTreeNodeVO.java
│   │                               │   ├── RuleTreeVO.java
│   │                               │   └── UserPO.java
│   │                               └── repository
│   │                                   ├── RuleRepository.java
│   │                                   └── UserRepository.java
│   └── xfg-frame-infrastructure.iml
├── xfg-frame-trigger
│   ├── pom.xml
│   ├── src
│   │   └── main
│   │       └── java
│   │           └── cn
│   │               └── bugstack
│   │                   └── xfg
│   │                       └── frame
│   │                           └── trigger
│   │                               ├── http
│   │                               │   ├── Controller.java
│   │                               │   └── package-info.java
│   │                               ├── mq
│   │                               │   └── package-info.java
│   │                               ├── rpc
│   │                               │   ├── AccountService.java
│   │                               │   ├── RuleService.java
│   │                               │   └── package-info.java
│   │                               └── task
│   │                                   └── package-info.java
│   └── xfg-frame-trigger.iml
└── xfg-frame-types
    ├── pom.xml
    ├── src
    │   └── main
    │       └── java
    │           └── cn
    │               └── bugstack
    │                   └── xfg
    │                       └── frame
    │                           └── types
    │                               ├── Constants.java
    │                               ├── Response.java
    │                               └── package-info.java
    └── xfg-frame-types.iml

以上是整个🏭工程架构的 tree 树形图。整个工程由 xfg-frame-app 模的 SpringBoot 驱动。这里小傅哥在 domain 领域模型下提供了 order、rule、user 三个领域模块。并在每个模块下提供了对应的测试内容。这块是整个模型的重点,其他模块都可以通过测试看到这里的调用过程。

4.3 领域

一个领域模型中包含3个部分;model、repository、service 三部分;

  • model 对象的定义
  • repository 仓储的定义
  • service 服务实现

以上3个模块,一般也是大家在使用 DDD 时候最不容易理解的分层。比如 model 里还分为;valobj – 值对象、entity 实体对象、aggregates 聚合对象;

  • 值对象:表示没有唯一标识的业务实体,例如商品的名称、描述、价格等。
  • 实体对象:表示具有唯一标识的业务实体,例如订单、商品、用户等;
  • 聚合对象:是一组相关的实体对象的根,用于保证实体对象之间的一致性和完整性;

关于model中各个对象的拆分,尤其是聚合的定义,会牵引着整个模型的设计。当然你可以在初期使用 DDD 的时候不用过分在意领域模型的设计,可以把整个 domain 下的一个个包当做充血模型结构,这样编写出来的代码也是非常适合维护的。

4.4 环境(开发/测试/上线)

源码xfg-frame-ddd/pom.xml

<profile>
    <id>dev</id>
    <activation>
        <activeByDefault>true</activeByDefault>
    </activation>
    <properties>
        <profileActive>dev</profileActive>
    </properties>
</profile>
<profile>
    <id>test</id>
    <properties>
        <profileActive>test</profileActive>
    </properties>
</profile>
<profile>
    <id>prod</id>
    <properties>
        <profileActive>prod</profileActive>
    </properties>
</profile>
  • 定义环境;开发、测试、上线。

源码xfg-frame-app/application.yml

spring:
  config:
    name: xfg-frame
  profiles:
    active: dev # dev、test、prod
  • 除了 pom 的配置,还需要在 application.yml 中指定环境。这样就可以对应的加载到;application-dev.ymlapplication-prod.ymlapplication-test.yml 这样就可以很方便的加载对应的配置信息了。尤其是各个场景中切换会更加方便。

4.5 切面

一个工程开发中,有时候可能会有很多的统一切面和启动配置的处理,这些内容都可以在 xfg-frame-app 完成。

源码cn.bugstack.xfg.frame.aop.RateLimiterAop

@Slf4j
@Aspect
public class RateLimiterAop {

    private final long timeout;
    private final double permitsPerSecond;
    private final RateLimiter limiter;

    public RateLimiterAop(double permitsPerSecond, long timeout) {
        this.permitsPerSecond = permitsPerSecond;
        this.timeout = timeout;
        this.limiter = RateLimiter.create(permitsPerSecond);
    }

    @Pointcut("execution(* cn.bugstack.xfg.frame.trigger..*.*(..))")
    public void pointCut() {
    }

    @Around(value = "pointCut()", argNames = "jp")
    public Object around(ProceedingJoinPoint jp) throws Throwable {
        boolean tryAcquire = limiter.tryAcquire(timeout, TimeUnit.MILLISECONDS);
        if (!tryAcquire) {
            Method method = getMethod(jp);
            log.warn("方法 {}.{} 请求已被限流,超过限流配置[{}/秒]", method.getDeclaringClass().getCanonicalName(), method.getName(), permitsPerSecond);
            return Response.<Object>builder()
                    .code(Constants.ResponseCode.RATE_LIMITER.getCode())
                    .info(Constants.ResponseCode.RATE_LIMITER.getInfo())
                    .build();
        }
        return jp.proceed();
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

}

使用

# 限流配置
rate-limiter:
  permits-per-second: 1
  timeout: 5
  • 这样你所有的通用配置,又和业务没有太大的关系的,就可以直接写到这里了。—— 具体可以参考代码。

5. 测试验证

  • 首先;整个工程由 SpringBoot 驱动,提供了 road-map.sql 测试 SQL 库表语句。你可以在自己的本地mysql上进行执行。它会创建库表。
  • 之后;在 application.yml 配置数据库链接信息。
  • 之后就可以打开 ApiTest 进行测试了。你可以点击 Application 类的绿色箭头启动工程,使用触发器里的接口调用测试,或者单元测试RPC接口,小傅哥也提供了泛化调用的方式。
  • 如果你正常获取了这样的结果信息,那么说明你已经启动成功。接下来就可以对照着DDD的结构进行学习,以及使用这样的工程结构开发自己的项目。

三、实战 – DDD 项目

纸上得来终觉浅,码农学习要实战!

无论是 MVC 还是各类 DDD 所呈现的架构,还是需要看到实际的代码,以及参与实战开发才能更好的吸收。否则都是理论仍旧难以让人下手。

所以小傅哥为大家准备了一些学习项目,这些项目都是非常具有架构思维以及设计模式的应用级实战项目架构设计和落地。对于一些小白来说,如果能早早的接触到这样的项目,就相当于是提前进入企业实习了。可以极大的提到编程思维以及开发能力。

这些项目包括:《Lottery 抽奖系统 – 基于领域驱动设计的四层架构实践》、《API网关:中间件设计和落地》、《ChatGPT 微服务应用体系搭建》、《IM 仿微信》、《SpringBoot Starter 中间件设计和落地》等。这里小傅哥只列3张图,你就知道有多牛皮了!

第1张:Lottery

架构

工程

第2张:API网关

架构

工程

第3张:ChatGPT


实战项目:https://bugstack.cn/md/zsxq/introduce.html

此外,小傅哥还给大家准备了一系列的《Java简明教程》视频,进入B站即可学习!

https://www.bilibili.com/video/BV1kV411g7GX

Vue3从入门到精通(三) - 明志德道 - 博客园

mikel阅读(402)

来源: Vue3从入门到精通(三) – 明志德道 – 博客园

vue3插槽Slots

在 Vue3 中,插槽(Slots)的使用方式与 Vue2 中基本相同,但有一些细微的差异。以下是在 Vue3 中使用插槽的示例:

// ChildComponent.vue
<template>
  <div>
    <h2>Child Component</h2>
    <slot></slot>
  </div>
</template>
​
// ParentComponent.vue
<template>
  <div>
    <h1>Parent Component</h1>
    <ChildComponent>
      <p>This is the content of the slot.</p>
    </ChildComponent>
  </div>
</template><script>
  import { defineComponent } from 'vue'
  import ChildComponent from './ChildComponent.vue'export default defineComponent({
    name: 'ParentComponent',
    components: {
      ChildComponent
    }
  })
</script>

在上面的示例中,ChildComponent 组件定义了一个默认插槽,使用 <slot></slot> 标签来表示插槽的位置。在 ParentComponent 组件中,使用 <ChildComponent> 标签包裹了一段内容 <p>This is the content of the slot.</p>,这段内容将被插入到 ChildComponent 组件的插槽位置。

需要注意的是,在 Vue3 中,默认插槽不再具有具名插槽的概念。如果需要使用具名插槽,可以使用 v-slot 指令。以下是一个示例:

// ChildComponent.vue
<template>
  <div>
    <h2>Child Component</h2>
    <slot name="header"></slot>
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>
​
// ParentComponent.vue
<template>
  <div>
    <h1>Parent Component</h1>
    <ChildComponent>
      <template v-slot:header>
        <h3>This is the header slot</h3>
      </template>
      <p>This is the content of the default slot.</p>
      <template v-slot:footer>
        <p>This is the footer slot</p>
      </template>
    </ChildComponent>
  </div>
</template><script>
  import { defineComponent } from 'vue'
  import ChildComponent from './ChildComponent.vue'export default defineComponent({
    name: 'ParentComponent',
    components: {
      ChildComponent
    }
  })
</script>

在上面的示例中,ChildComponent 组件定义了三个插槽,分别是名为 header、默认插槽和名为 footer 的插槽。在 ParentComponent 组件中,使用 <template v-slot:header> 来定义 header 插槽的内容,使用 <template v-slot:footer> 来定义 footer 插槽的内容。默认插槽可以直接写在组件标签内部。

需要注意的是,在 Vue3 中,v-slot 只能用在 <template> 标签上,不能用在普通的 HTML 标签上。如果要在普通 HTML 标签上使用插槽,可以使用 v-slot 的缩写语法 #。例如,<template v-slot:header> 可以简写为 #header

vue3组件生命周期

在 Vue3 中,组件的生命周期钩子函数与 Vue2 中有一些变化。以下是 Vue3 中常用的组件生命周期钩子函数:

  1. beforeCreate: 在实例初始化之后、数据观测之前被调用。

  2. created: 在实例创建完成之后被调用。此时,实例已完成数据观测、属性和方法的运算,但尚未挂载到 DOM 中。

  3. beforeMount: 在挂载开始之前被调用。在此阶段,模板已经编译完成,但尚未将模板渲染到 DOM 中。

  4. mounted: 在挂载完成之后被调用。此时,组件已经被挂载到 DOM 中,可以访问到 DOM 元素。

  5. beforeUpdate: 在数据更新之前被调用。在此阶段,虚拟 DOM 已经重新渲染,并将计算得到的变化应用到真实 DOM 上,但尚未更新到视图中。

  6. updated: 在数据更新之后被调用。此时,组件已经更新到最新的状态,DOM 也已经更新完成。

  7. beforeUnmount: 在组件卸载之前被调用。在此阶段,组件实例仍然可用,可以访问到组件的数据和方法。

  8. unmounted: 在组件卸载之后被调用。此时,组件实例已经被销毁,无法再访问到组件的数据和方法。

需要注意的是,Vue3 中移除了一些生命周期钩子函数,如 beforeDestroy 和 destroyed。取而代之的是 beforeUnmount 和 unmounted

另外,Vue3 中还引入了新的生命周期钩子函数 onRenderTracked 和 onRenderTriggered,用于追踪组件的渲染过程和触发的依赖项。

需要注意的是,Vue3 推荐使用 Composition API 来编写组件逻辑,而不是依赖于生命周期钩子函数。Composition API 提供了 setup 函数,用于组件的初始化和逻辑组织。在 setup 函数中,可以使用 onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmount 等函数来替代相应的生命周期钩子函数。

vue3生命周期应用

Vue3 的生命周期钩子函数可以用于在组件的不同生命周期阶段执行相应的操作。以下是一些 Vue3 生命周期的应用场景示例:

  1. beforeCreate 和 created:在组件实例创建之前和创建之后执行一些初始化操作,如设置初始数据、进行异步请求等。

export default {
  beforeCreate() {
    console.log('beforeCreate hook');
    // 执行一些初始化操作
  },
  created() {
    console.log('created hook');
    // 执行一些初始化操作
  },
};
  1. beforeMount 和 mounted:在组件挂载之前和挂载之后执行一些 DOM 操作,如获取 DOM 元素、绑定事件等。

export default {
  beforeMount() {
    console.log('beforeMount hook');
    // 执行一些 DOM 操作
  },
  mounted() {
    console.log('mounted hook');
    // 执行一些 DOM 操作
  },
};
  1. beforeUpdate 和 updated:在组件数据更新之前和更新之后执行一些操作,如更新 DOM、发送请求等。

export default {
  beforeUpdate() {
    console.log('beforeUpdate hook');
    // 执行一些操作
  },
  updated() {
    console.log('updated hook');
    // 执行一些操作
  },
};
  1. beforeUnmount 和 unmounted:在组件卸载之前和卸载之后执行一些清理操作,如取消订阅、清除定时器等。

export default {
  beforeUnmount() {
    console.log('beforeUnmount hook');
    // 执行一些清理操作
  },
  unmounted() {
    console.log('unmounted hook');
    // 执行一些清理操作
  },
};

这些示例展示了 Vue3 生命周期钩子函数的一些常见应用场景。根据具体需求,你可以在相应的生命周期钩子函数中执行适当的操作。

vue3动态组件

在 Vue3 中,可以使用动态组件来根据不同的条件或状态动态地渲染不同的组件。使用动态组件可以使应用更加灵活和可扩展。以下是使用动态组件的示例:

  1. 使用 component 元素和 :is 属性来实现动态组件的渲染:

<template>
  <div>
    <component :is="currentComponent"></component>
    <button @click="toggleComponent">Toggle Component</button>
  </div>
</template><script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';export default {
  data() {
    return {
      currentComponent: 'ComponentA',
    };
  },
  methods: {
    toggleComponent() {
      this.currentComponent = this.currentComponent === 'ComponentA' ? 'ComponentB' : 'ComponentA';
    },
  },
  components: {
    ComponentA,
    ComponentB,
  },
};
</script>

在上面的示例中,根据 currentComponent 的值动态地渲染 ComponentA 或 ComponentB 组件。点击按钮时,切换 currentComponent 的值,从而实现动态组件的切换。

  1. 使用 v-if 和 v-else 来根据条件渲染不同的组件:

<template>
  <div>
    <component-a v-if="showComponentA"></component-a>
    <component-b v-else></component-b>
    <button @click="toggleComponent">Toggle Component</button>
  </div>
</template><script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';export default {
  data() {
    return {
      showComponentA: true,
    };
  },
  methods: {
    toggleComponent() {
      this.showComponentA = !this.showComponentA;
    },
  },
  components: {
    ComponentA,
    ComponentB,
  },
};
</script>

在上面的示例中,根据 showComponentA 的值使用 v-if 和 v-else 来渲染 ComponentA 或 ComponentB 组件。点击按钮时,切换 showComponentA 的值,从而实现动态组件的切换。

这些示例演示了在 Vue3 中如何使用动态组件来根据条件或状态动态地渲染不同的组件。你可以根据具体需求选择适合的方式来使用动态组件。

vue3组件保持存活

在 Vue3 中,可以使用 <keep-alive> 组件来保持组件的存活状态,即使组件在组件树中被切换或销毁,它的状态仍然会被保留。这对于需要在组件之间共享状态或缓存数据的场景非常有用。以下是使用 <keep-alive> 组件来保持组件存活的示例:

<template>
  <div>
    <button @click="toggleComponent">Toggle Component</button>
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </div>
</template><script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';export default {
  data() {
    return {
      currentComponent: 'ComponentA',
    };
  },
  methods: {
    toggleComponent() {
      this.currentComponent = this.currentComponent === 'ComponentA' ? 'ComponentB' : 'ComponentA';
    },
  },
  components: {
    ComponentA,
    ComponentB,
  },
};
</script>

在上面的示例中,使用 <keep-alive> 组件将 <component> 包裹起来,这样在切换组件时,被包裹的组件的状态将会被保留。点击按钮时,切换 currentComponent 的值,从而切换要渲染的组件。

需要注意的是,被 <keep-alive> 包裹的组件在切换时会触发一些特定的生命周期钩子函数,如 activated 和 deactivated。你可以在这些钩子函数中执行一些特定的操作,如获取焦点、发送请求等。

<template>
  <div>
    <h2>Component A</h2>
  </div>
</template><script>
export default {
  activated() {
    console.log('Component A activated');
    // 执行一些操作
  },
  deactivated() {
    console.log('Component A deactivated');
    // 执行一些操作
  },
};
</script>

在上面的示例中,当组件 A 被激活或停用时,分别在 activated 和 deactivated 钩子函数中输出相应的信息。

使用 <keep-alive> 组件可以方便地保持组件的存活状态,并在组件之间共享状态或缓存数据。

vue3异步组件

在 Vue3 中,可以使用异步组件来延迟加载组件的代码,从而提高应用的性能和加载速度。异步组件在需要时才会被加载,而不是在应用初始化时就加载所有组件的代码。以下是使用异步组件的示例:

  1. 使用 defineAsyncComponent 函数来定义异步组件:

<template>
  <div>
    <button @click="loadComponent">Load Component</button>
    <component v-if="isComponentLoaded" :is="component"></component>
  </div>
</template><script>
import { defineAsyncComponent } from 'vue';const AsyncComponent = defineAsyncComponent(() =>
  import('./Component.vue')
);export default {
  data() {
    return {
      isComponentLoaded: false,
      component: null,
    };
  },
  methods: {
    loadComponent() {
      this.isComponentLoaded = true;
      this.component = AsyncComponent;
    },
  },
};
</script>

在上面的示例中,使用 defineAsyncComponent 函数来定义异步组件 AsyncComponent。当点击按钮时,设置 isComponentLoaded 为 true,并将 component 设置为 AsyncComponent,从而加载异步组件。

  1. 使用 Suspense 组件来处理异步组件的加载状态:

<template>
  <div>
    <Suspense>
      <template #default>
        <component :is="component"></component>
      </template>
      <template #fallback>
        <div>Loading...</div>
      </template>
    </Suspense>
    <button @click="loadComponent">Load Component</button>
  </div>
</template><script>
import { defineAsyncComponent, Suspense } from 'vue';const AsyncComponent = defineAsyncComponent(() =>
  import('./Component.vue')
);export default {
  data() {
    return {
      component: null,
    };
  },
  methods: {
    loadComponent() {
      this.component = AsyncComponent;
    },
  },
};
</script>

在上面的示例中,使用 Suspense 组件来处理异步组件的加载状态。在 default 插槽中,渲染异步组件,而在 fallback 插槽中,渲染加载状态的提示信息。当点击按钮时,加载异步组件。

这些示例演示了在 Vue3 中如何使用异步组件来延迟加载组件的代码。使用异步组件可以提高应用的性能和加载速度,特别是在应用中有大量组件时。

vue3依赖注入

在 Vue3 中,可以使用依赖注入来在组件之间共享数据或功能。Vue3 提供了 provide 和 inject 两个函数来实现依赖注入。

  1. 使用 provide 函数在父组件中提供数据或功能:

<template>
  <div>
    <ChildComponent></ChildComponent>
  </div>
</template><script>
import { provide } from 'vue';
import MyService from './MyService';export default {
  setup() {
    provide('myService', new MyService());
  },
};
</script>

在上面的示例中,使用 provide 函数在父组件中提供了一个名为 myService 的数据或功能,它的值是一个 MyService 的实例。

  1. 使用 inject 函数在子组件中注入提供的数据或功能:

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template><script>
import { inject } from 'vue';export default {
  setup() {
    const myService = inject('myService');
    const message = myService.getMessage();return {
      message,
    };
  },
};
</script>

在上面的示例中,使用 inject 函数在子组件中注入了父组件提供的名为 myService 的数据或功能。通过注入的 myService 实例,可以调用其中的方法或访问其中的属性。

通过使用 provide 和 inject 函数,可以在组件之间实现依赖注入,从而实现数据或功能的共享。这在多个组件需要访问相同的数据或功能时非常有用。

vue3应用

Vue3 是一个用于构建用户界面的现代化 JavaScript 框架。它具有响应式数据绑定、组件化、虚拟 DOM 等特性,使得开发者可以更高效地构建交互式的 Web 应用。

下面是一些使用 Vue3 开发应用的步骤:

  1. 安装 Vue3:使用 npm 或 yarn 安装 Vue3 的最新版本。

npm install vue@next
  1. 创建 Vue3 应用:创建一个新的 Vue3 项目。

vue create my-app
  1. 编写组件:在 src 目录下创建组件文件,例如 HelloWorld.vue

<template>
  <div>
    <h1>{{ message }}</h1>
    <button @click="changeMessage">Change Message</button>
  </div>
</template>
​
<script>
import { ref } from 'vue';
​
export default {
  setup() {
    const message = ref('Hello, Vue3!');
​
    const changeMessage = () => {
      message.value = 'Hello, World!';
    };
​
    return {
      message,
      changeMessage,
    };
  },
};
</script>

在上面的示例中,使用 ref 函数创建了一个响应式的数据 message,并在模板中使用它。通过点击按钮,可以改变 message 的值。

  1. 使用组件:在 App.vue 中使用自定义的组件。

<template>
  <div>
    <HelloWorld></HelloWorld>
  </div>
</template>
​
<script>
import HelloWorld from './components/HelloWorld.vue';
​
export default {
  components: {
    HelloWorld,
  },
};
</script>

在上面的示例中,导入并注册了自定义的 HelloWorld 组件,并在模板中使用它。

  1. 运行应用:在命令行中运行以下命令启动应用。

npm run serve

这将启动开发服务器,并在浏览器中打开应用。

这只是一个简单的示例,你可以根据实际需求编写更复杂的组件和应用逻辑。Vue3 还提供了许多其他功能和工具,如路由、状态管理、单文件组件等,以帮助你构建更强大的应用。

希望这个简单的示例能帮助你入门 Vue3 应用的开发!

献给转java的c#和java程序员的数据库orm框架 - 薛家明 - 博客园

mikel阅读(347)

来源: 献给转java的c#和java程序员的数据库orm框架 – 薛家明 – 博客园

献给转java的C#和java程序员的数据库orm框架

一个好的程序员不应被语言所束缚,正如我现在开源java的orm框架一样,如果您是一位转java的C#程序员,那么这个框架可以带给你起码没有那么差的业务编写和强类型体验。如果您是一位java程序员,那么该框架可以提供比Mybatis-Plus功能更加丰富、性能更高,更加轻量和完全免费的体验来做一个happy coding crud body。

背景

easy-query该框架是我在使用Mybatis-Plus(下面统称MP) 2年后开发的,因为MP不支持多表(不要提join插件(逻辑删除子表不支持)),并且Mybatis原本的xml十分恶心,导致项目中有非常多的代码需要编写SQL,并且整体数据库架构因为存在逻辑删除字段和多租户字段所以编写的SQL基本上多多少少都会有问题,我不相信大家没遇到过,而且MP得一些功能还需要收费这大大让我坚定还是自己开发一款。

介绍

easy-query 🚀 是一款无任何依赖的JAVA ORM 框架,十分轻量,拥有非常高的性能,支持单表查询、多表查询、union、子查询、分页、动态表名、VO对象查询返回、逻辑删、全局拦截、数据库列加密(支持高性能like查询)、数据追踪差异更新、乐观锁、多租户、自动分库、自动分表、读写分离,支持框架全功能外部扩展定制,拥有强类型表达式。

📚 文档

GITHUB地址 | GITEE地址

缺点

先说一下缺点,目前只适配了MySQL,不过基本上如果你是pgsql很少需要改动就直接可以用了,其他数据库可能因为自己的语法和特性会需要稍微做一下修改但是整体而言无需过多的变动,框架已经全部抽象好了。

功能点

  • 实体对象insert,update,delete全部支持
  • 单表查询、多表join查询,in子查询,exists子查询,连表统计(select a,(select count(1) from b) from c),联合查询union | all,分组group | having
  • 分页
  • 动态表名:运行时修改表名
  • 原生sql执行,查询
  • select查询map结果返回
  • select支持直接返回DTO对象实现自定义列查询返回,而不是全部列返回
  • select支持标记large字段不返回(默认返回)
  • 逻辑删除,自定义逻辑删除,支持多字段逻辑删除填充,支持运行时禁用
  • 全局拦截器,支持运行时选择性使用某几个或者不使用,支持entity操作 insert,update,条件拦截 select、update、delete的where条件拦截,update set字段拦截器
  • 多租户,支持表的列范围多租户模式
  • 数据库列加密,支持高性能的like模糊搜索匹配(不是单纯的调用数据库加密函数或者单纯的调用框架加密解密函数)
  • 数据追踪差异更新,而不是全列更新,用过efcore的肯定很熟悉
  • 版本号、乐观锁,支持自定义乐观锁
  • 支持分库分表(身为sharding-core作者不支持说不过去),全自动分库分表,仅需用户新增表和告知easy-query系统中有的表
  • 高性能分库分表分页,支持顺序分页,反向分页,支持高性能顺序分页和反向分页
  • 分库分表多字段分片
  • 分库分表自定义分片路由规则
  • 支持读写分离,一主多从支持分片下读写分离

目前项目正处于起步阶段后续会随着用户不断地完善各数据库的适配和功能的支持

开始使用

安装

以下是spring-boot环境和控制台模式的安装

spring-boot

<properties>
    <easy-query.version>0.8.10</easy-query.version>
</properties>
<dependency>
    <groupId>com.easy-query</groupId>
    <artifactId>sql-springboot-starter</artifactId>
    <version>${easy-query.version}</version>
</dependency>

console

以mysql为例

<properties>
    <easy-query.version>0.8.10</easy-query.version>
</properties>
<dependency>
    <groupId>com.easy-query</groupId>
    <artifactId>sql-mysql</artifactId>
    <version>${easy-query.version}</version>
</dependency>
//初始化连接池
 HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/easy-query-test?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true");
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setMaximumPoolSize(20);
//创建easy-query
 EasyQuery easyQuery = EasyQueryBootstrapper.defaultBuilderConfiguration()
                .setDefaultDataSource(dataSource)
                .useDatabaseConfigure(new MySQLDatabaseConfiguration())
                .build();

开始

sql脚本

create table t_topic
(
    id varchar(32) not null comment '主键ID'primary key,
    stars int not null comment '点赞数',
    title varchar(50) null comment '标题',
    create_time datetime not null comment '创建时间'
)comment '主题表';

create table t_blog
(
    id varchar(32) not null comment '主键ID'primary key,
    deleted tinyint(1) default 0 not null comment '是否删除',
    create_by varchar(32) not null comment '创建人',
    create_time datetime not null comment '创建时间',
    update_by varchar(32) not null comment '更新人',
    update_time datetime not null comment '更新时间',
    title varchar(50) not null comment '标题',
    content varchar(256) null comment '内容',
    url varchar(128) null comment '博客链接',
    star int not null comment '点赞数',
    publish_time datetime null comment '发布时间',
    score decimal(18, 2) not null comment '评分',
    status int not null comment '状态',
    `order` decimal(18, 2) not null comment '排序',
    is_top tinyint(1) not null comment '是否置顶',
    top tinyint(1) not null comment '是否置顶'
)comment '博客表';

查询对象




@Data
public class BaseEntity implements Serializable {
    private static final long serialVersionUID = -4834048418175625051L;

    @Column(primaryKey = true)
    private String id;
    /**
     * 创建时间;创建时间
     */
    private LocalDateTime createTime;
    /**
     * 修改时间;修改时间
     */
    private LocalDateTime updateTime;
    /**
     * 创建人;创建人
     */
    private String createBy;
    /**
     * 修改人;修改人
     */
    private String updateBy;
    /**
     * 是否删除;是否删除
     */
    @LogicDelete(strategy = LogicDeleteStrategyEnum.BOOLEAN)
    private Boolean deleted;
}


@Data
@Table("t_topic")
@ToString
public class Topic {

    @Column(primaryKey = true)
    private String id;
    private Integer stars;
    private String title;
    private LocalDateTime createTime;
}

@Data
@Table("t_blog")
public class BlogEntity extends BaseEntity{

    /**
     * 标题
     */
    private String title;
    /**
     * 内容
     */
    private String content;
    /**
     * 博客链接
     */
    private String url;
    /**
     * 点赞数
     */
    private Integer star;
    /**
     * 发布时间
     */
    private LocalDateTime publishTime;
    /**
     * 评分
     */
    private BigDecimal score;
    /**
     * 状态
     */
    private Integer status;
    /**
     * 排序
     */
    private BigDecimal order;
    /**
     * 是否置顶
     */
    private Boolean isTop;
    /**
     * 是否置顶
     */
    private Boolean top;
}

单表查询

Topic topic = easyQuery
                .queryable(Topic.class)
                .where(o -> o.eq(Topic::getId, "3"))
                .firstOrNull();      
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE t.`id` = ? LIMIT 1
==> Parameters: 3(String)
<== Time Elapsed: 15(ms)
<== Total: 1     

多表查询

Topic topic = easyQuery
                .queryable(Topic.class)
                .leftJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
                .where(o -> o.eq(Topic::getId, "3"))
                .firstOrNull();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t LEFT JOIN `t_blog` t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t.`id` = ? LIMIT 1
==> Parameters: false(Boolean),3(String)
<== Time Elapsed: 2(ms)
<== Total: 1

复杂查询

join + group +分页


EasyPageResult<BlogEntity> page = easyQuery
        .queryable(Topic.class).asTracking()
        .innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
        .where((t, t1) -> t1.isNotNull(BlogEntity::getTitle))
        .groupBy((t, t1)->t1.column(BlogEntity::getId))
        .select(BlogEntity.class, (t, t1) -> t1.column(BlogEntity::getId).columnSum(BlogEntity::getScore))
        .toPageResult(1, 20);

==> Preparing: SELECT t1.`id`,SUM(t1.`score`) AS `score` FROM `t_topic` t INNER JOIN `t_blog` t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL GROUP BY t1.`id` LIMIT 20
==> Parameters: false(Boolean)
<== Time Elapsed: 5(ms)
<== Total: 20

动态表名


String sql = easyQuery.queryable(BlogEntity.class)
        .asTable(a->"aa_bb_cc")
        .where(o -> o.eq(BlogEntity::getId, "123"))
        .toSQL();
     
 SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM `aa_bb_cc` t WHERE t.`deleted` = ? AND t.`id` = ?  

新增


Topic topic = new Topic();
topic.setId(String.valueOf(0));
topic.setStars(100);
topic.setTitle("标题0");
topic.setCreateTime(LocalDateTime.now().plusDays(i));

long rows = easyQuery.insertable(topic).executeRows();

//返回结果rows1
==> Preparing: INSERT INTO `t_topic` (`id`,`stars`,`title`,`create_time`) VALUES (?,?,?,?) 
==> Parameters: 0(String),100(Integer),标题0(String),2023-03-16T21:34:13.287(LocalDateTime)
<== Total: 1

修改

//实体更新
 Topic topic = easyQuery.queryable(Topic.class)
        .where(o -> o.eq(Topic::getId, "7")).firstNotNull("未找到对应的数据");
        String newTitle = "test123" + new Random().nextInt(100);
        topic.setTitle(newTitle);

long rows=easyQuery.updatable(topic).executeRows();
==> Preparing: UPDATE t_topic SET `stars` = ?,`title` = ?,`create_time` = ? WHERE `id` = ?
==> Parameters: 107(Integer),test12364(String),2023-03-27T22:05:23(LocalDateTime),7(String)
<== Total: 1
//表达式更新
long rows = easyQuery.updatable(Topic.class)
                .set(Topic::getStars, 12)
                .where(o -> o.eq(Topic::getId, "2"))
                .executeRows();
//rows为1
easyQuery.updatable(Topic.class)
                    .set(Topic::getStars, 12)
                    .where(o -> o.eq(Topic::getId, "2"))
                    .executeRows(1,"更新失败");
//判断受影响行数并且进行报错,如果当前操作不在事务内执行那么会自动开启事务!!!会自动开启事务!!!会自动开启事务!!!来实现并发更新控制,异常为:EasyQueryConcurrentException 
//抛错后数据将不会被更新
==> Preparing: UPDATE t_topic SET `stars` = ? WHERE `id` = ?
==> Parameters: 12(Integer),2(String)
<== Total: 1

删除

long l = easyQuery.deletable(Topic.class)
                    .where(o->o.eq(Topic::getTitle,"title998"))
                    .executeRows();
==> Preparing: DELETE FROM t_topic WHERE `title` = ?
==> Parameters: title998(String)
<== Total: 1
Topic topic = easyQuery.queryable(Topic.class).whereId("997").firstNotNull("未找到当前主题数据");
long l = easyQuery.deletable(topic).executeRows();
==> Preparing: DELETE FROM t_topic WHERE `id` = ?
==> Parameters: 997(String)
<== Total: 1

联合查询

Queryable<Topic> q1 = easyQuery
                .queryable(Topic.class);
Queryable<Topic> q2 = easyQuery
        .queryable(Topic.class);
Queryable<Topic> q3 = easyQuery
        .queryable(Topic.class);
List<Topic> list = q1.union(q2, q3).where(o -> o.eq(Topic::getId, "123321")).toList();

==> Preparing: SELECT t1.`id`,t1.`stars`,t1.`title`,t1.`create_time` FROM (SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t UNION SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t UNION SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t) t1 WHERE t1.`id` = ?
==> Parameters: 123321(String)
<== Time Elapsed: 19(ms)
<== Total: 0

子查询

in子查询

Queryable<String> idQueryable = easyQuery.queryable(BlogEntity.class)
        .where(o -> o.eq(BlogEntity::getId, "1"))
        .select(String.class,o->o.column(BlogEntity::getId));
List<Topic> list = easyQuery
        .queryable(Topic.class, "x").where(o -> o.in(Topic::getId, idQueryable)).toList();
==> Preparing: SELECT x.`id`,x.`stars`,x.`title`,x.`create_time` FROM `t_topic` x WHERE x.`id` IN (SELECT t.`id` FROM `t_blog` t WHERE t.`deleted` = ? AND t.`id` = ?) 
==> Parameters: false(Boolean),1(String)
<== Time Elapsed: 3(ms)
<== Total: 1    

exists子查询

Queryable<BlogEntity> where1 = easyQuery.queryable(BlogEntity.class)
                .where(o -> o.eq(BlogEntity::getId, "1"));
List<Topic> x = easyQuery
        .queryable(Topic.class, "x").where(o -> o.exists(where1.where(q -> q.eq(o, BlogEntity::getId, Topic::getId)))).toList();
==> Preparing: SELECT x.`id`,x.`stars`,x.`title`,x.`create_time` FROM `t_topic` x WHERE EXISTS (SELECT 1 FROM `t_blog` t WHERE t.`deleted` = ? AND t.`id` = ? AND t.`id` = x.`id`) 
==> Parameters: false(Boolean),1(String)
<== Time Elapsed: 10(ms)
<== Total: 1

分片

easy-query支持分表、分库、分表+分库

分表

//创建分片对象
@Data
@Table(value = "t_topic_sharding_time",shardingInitializer = TopicShardingTimeShardingInitializer.class)
@ToString
public class TopicShardingTime {

    @Column(primaryKey = true)
    private String id;
    private Integer stars;
    private String title;
    @ShardingTableKey
    private LocalDateTime createTime;
}
//分片初始化器很简单 假设我们是2020年1月到2023年5月也就是当前时间进行分片那么要生成对应的分片表每月一张
public class TopicShardingTimeShardingInitializer extends AbstractShardingMonthInitializer<TopicShardingTime> {

    @Override
    protected LocalDateTime getBeginTime() {
        return LocalDateTime.of(2020, 1, 1, 1, 1);
    }

    @Override
    protected LocalDateTime getEndTime() {
        return LocalDateTime.of(2023, 5, 1, 0, 0);
    }


    @Override
    public void configure0(ShardingEntityBuilder<TopicShardingTime> builder) {

////以下条件可以选择配置也可以不配置用于优化分片性能
//        builder.paginationReverse(0.5,100)
//                .ascSequenceConfigure(new TableNameStringComparator())
//                .addPropertyDefaultUseDesc(TopicShardingTime::getCreateTime)
//                .defaultAffectedMethod(false, ExecuteMethodEnum.LIST,ExecuteMethodEnum.ANY,ExecuteMethodEnum.COUNT,ExecuteMethodEnum.FIRST)
//                .useMaxShardingQueryLimit(2,ExecuteMethodEnum.LIST,ExecuteMethodEnum.ANY,ExecuteMethodEnum.FIRST);

    }
}
//分片时间路由规则按月然后bean分片属性就是LocalDateTime也可以自定义实现
public class TopicShardingTimeTableRule extends AbstractMonthTableRule<TopicShardingTime> {

    @Override
    protected LocalDateTime convertLocalDateTime(Object shardingValue) {
        return (LocalDateTime)shardingValue;
    }
}

数据库脚本参考源码

其中shardingInitializer为分片初始化器用来初始化告诉框架有多少分片的表名(支持动态添加)

ShardingTableKey表示哪个字段作为分片键(分片键不等于主键)

执行sql

LocalDateTime beginTime = LocalDateTime.of(2021, 1, 1, 1, 1);
LocalDateTime endTime = LocalDateTime.of(2021, 5, 2, 1, 1);
Duration between = Duration.between(beginTime, endTime);
long days = between.toDays();
List<TopicShardingTime> list = easyQuery.queryable(TopicShardingTime.class)
        .where(o->o.rangeClosed(TopicShardingTime::getCreateTime,beginTime,endTime))
        .orderByAsc(o -> o.column(TopicShardingTime::getCreateTime))
        .toList();


==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202101` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> SHARDING_EXECUTOR_3, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202102` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> SHARDING_EXECUTOR_2, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
==> SHARDING_EXECUTOR_3, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
<== SHARDING_EXECUTOR_3, name:ds2020, Time Elapsed: 3(ms)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 3(ms)
==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202103` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> SHARDING_EXECUTOR_3, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202104` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> SHARDING_EXECUTOR_2, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
==> SHARDING_EXECUTOR_3, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
<== SHARDING_EXECUTOR_3, name:ds2020, Time Elapsed: 2(ms)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 2(ms)
==> main, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202105` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> main, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
<== main, name:ds2020, Time Elapsed: 2(ms)
<== Total: 122

分库


@Data
@Table(value = "t_topic_sharding_ds",shardingInitializer = DataSourceAndTableShardingInitializer.class)
@ToString
public class TopicShardingDataSource {

    @Column(primaryKey = true)
    private String id;
    private Integer stars;
    private String title;
    @ShardingDataSourceKey
    private LocalDateTime createTime;
}
public class DataSourceShardingInitializer implements EntityShardingInitializer<TopicShardingDataSource> {
    @Override
    public void configure(ShardingEntityBuilder<TopicShardingDataSource> builder) {
        EntityMetadata entityMetadata = builder.getEntityMetadata();
        String tableName = entityMetadata.getTableName();
        List<String> tables = Collections.singletonList(tableName);
        LinkedHashMap<String, Collection<String>> initTables = new LinkedHashMap<String, Collection<String>>() {{
            put("ds2020", tables);
            put("ds2021", tables);
            put("ds2022", tables);
            put("ds2023", tables);
        }};
        builder.actualTableNameInit(initTables);


    }
}
//分库数据源路由规则
public class TopicShardingDataSourceRule extends AbstractDataSourceRouteRule<TopicShardingDataSource> {
    @Override
    protected RouteFunction<String> getRouteFilter(TableAvailable table, Object shardingValue, ShardingOperatorEnum shardingOperator, boolean withEntity) {
        LocalDateTime createTime = (LocalDateTime) shardingValue;
        String dataSource = "ds" + createTime.getYear();
        switch (shardingOperator){
            case GREATER_THAN:
            case GREATER_THAN_OR_EQUAL:
                return ds-> dataSource.compareToIgnoreCase(ds)<=0;
            case LESS_THAN:
            {
                //如果小于月初那么月初的表是不需要被查询的
                LocalDateTime timeYearFirstDay = LocalDateTime.of(createTime.getYear(),1,1,0,0,0);
                if(createTime.isEqual(timeYearFirstDay)){
                    return ds->dataSource.compareToIgnoreCase(ds)>0;
                }
                return ds->dataSource.compareToIgnoreCase(ds)>=0;
            }
            case LESS_THAN_OR_EQUAL:
                return ds->dataSource.compareToIgnoreCase(ds)>=0;

            case EQUAL:
                return ds->dataSource.compareToIgnoreCase(ds)==0;
            default:return t->true;
        }
    }
}

LocalDateTime beginTime = LocalDateTime.of(2020, 1, 1, 1, 1);
LocalDateTime endTime = LocalDateTime.of(2023, 5, 1, 1, 1);
Duration between = Duration.between(beginTime, endTime);
long days = between.toDays();
EasyPageResult<TopicShardingDataSource> pageResult = easyQuery.queryable(TopicShardingDataSource.class)
        .orderByAsc(o -> o.column(TopicShardingDataSource::getCreateTime))
        .toPageResult(1, 33);

==> SHARDING_EXECUTOR_23, name:ds2022, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_ds` t ORDER BY t.`create_time` ASC LIMIT 33
==> SHARDING_EXECUTOR_11, name:ds2021, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_ds` t ORDER BY t.`create_time` ASC LIMIT 33
==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_ds` t ORDER BY t.`create_time` ASC LIMIT 33
==> SHARDING_EXECUTOR_4, name:ds2023, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_ds` t ORDER BY t.`create_time` ASC LIMIT 33
<== SHARDING_EXECUTOR_4, name:ds2023, Time Elapsed: 4(ms)
<== SHARDING_EXECUTOR_23, name:ds2022, Time Elapsed: 4(ms)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 4(ms)
<== SHARDING_EXECUTOR_11, name:ds2021, Time Elapsed: 6(ms)
<== Total: 33

最后

希望看到这边的各位大佬给我点个star谢谢这对我很重要

javaer你还在手写分表分库?来看看这个框架怎么做的 干货满满 - 薛家明 - 博客园

mikel阅读(316)

来源: javaer你还在手写分表分库?来看看这个框架怎么做的 干货满满 – 薛家明 – 博客园

java orm框架easy-query分库分表之分表

高并发三驾马车:分库分表、MQ、缓存。今天给大家带来的就是分库分表的干货解决方案,哪怕你不用我的框架也可以从中听到不一样的结局方案和实现。

一款支持自动分表分库的orm框架easy-query 帮助您解脱跨库带来的复杂业务代码,并且提供多种结局方案和自定义路由来实现比中间件更高性能的数据库访问。

目前市面上有的分库分表JAVA组件有很多:中间件代理有:sharding-sphere(proxy),mycat 客户端JDBC:sharding-sphere(jdbc)等等,中间件因为代理了一层会导致所有的SQL执行都要经过中间件,性能会大大折扣,但是因为中间部署可以提供更加省的连接池,客户端无需代理,仅需对SQL进行分析即可实现,但是越靠近客户的模式可以优化的性能越高,所以本次带来的框架可以提供前所未有的分片规则自由和前所未有的便捷高性能。

本文 demo地址 https://github.com/xuejmnet/easy-sharding-test

怎么样的orm算是支持分表分库

首先orm是否支持分表分库不仅仅是看框架是否支持动态修改表名,让数据正确存入对应的表或者修改对应的数据,这些说实话都是最最简单的实现,真正需要支持分库分表那么需要orm实现复杂的跨表聚合查询,这才是分表分库的精髓,很显然目前的orm很少有支持的。接下来我将给大家演示基于springboot3.x的分表分库演示,取模分片和时间分片。本章我们主要以使用为主后面下一章我们来讲解优化方案,包括原理解析,后续有更多的关于分表分库的经验是博主多年下来的实战经验分享给大家保证大家的happy coding。

初始化项目

进入 https://start.spring.io/ 官网直接下载

安装依赖


		<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.2.15</version>
		</dependency>
		<!-- mysql驱动 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.17</version>
		</dependency>
		<dependency>
			<groupId>com.easy-query</groupId>
			<artifactId>sql-springboot-starter</artifactId>
			<version>0.9.7</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.18</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

application.yml配置

server:
  port: 8080

spring:

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/easy-sharding-test?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
    username: root
    password: root

logging:
  level:
    com.easy.query.core: debug

easy-query:
  enable: true
  name-conversion: underlined
  database: mysql

取模

常见的分片方式之一就是取模分片,取模分片可以让以分片键为条件的处理完美路由到对应的表,性能上来说非常非常高,但是局限性也是很大的因为无意义的id路由会导致仅支持这一个id条件而不支持其他条件的路由,只能全分片表扫描来获取对应的数据,但是他的实现和理解也是最容易的,当然后续还有基因分片一种可以部分解决仅支持id带来的问题不过也并不是非常的完美。

简单的取模分片

我们本次测试案例采用order表对其进行5表拆分:order_00,order_01,order_02,order_03,order_04,采用订单id取模进行分表
数据库脚本

CREATE DATABASE IF NOT EXISTS `easy-sharding-test` CHARACTER SET 'utf8mb4';
USE `easy-sharding-test`;
create table order_00
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
create table order_01
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
create table order_02
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
create table order_03
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
create table order_04
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
//定义了一个对象并且设置表名和分片初始化器`shardingInitializer`,设置id为主键,并且设置id为分表建
@Data
@Table(value = "order",shardingInitializer = OrderShardingInitializer.class)
public class OrderEntity {
    @Column(primaryKey = true)
    @ShardingTableKey
    private String id;
    private String uid;
    private Integer orderNo;
}
//编写订单取模初始化器,只需要实现两个方法,当然你也可以自己实现对应的`EntityShardingInitializer`这边是继承`easy-query`框架提供的分片取模初始化器
@Component
public class OrderShardingInitializer extends AbstractShardingModInitializer<OrderEntity> {
     /**
     * 设置模几我们模5就设置5
     * @return
     */
    @Override
    protected int mod() {
        return 5;
    }

    /**
     * 编写模5后的尾巴长度默认我们设置2就是左补0
     * @return
     */
    @Override
    protected int tailLength() {
        return 2;
    }
}
//编写分片规则`AbstractModTableRule`由框架提供取模分片路由规则,如果需要自己实现可以继承`AbstractTableRouteRule`这个抽象类
@Component
public class OrderTableRouteRule extends AbstractModTableRule<OrderEntity> {
    @Override
    protected int mod() {
        return 5;
    }

    @Override
    protected int tailLength() {
        return 2;
    }
}

初始化工作做好了开始编写代码

新增初始化


@RestController
@RequestMapping("/order")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class OrderController {

    private final EasyQuery easyQuery;

    @GetMapping("/init")
    public Object init() {
        ArrayList<OrderEntity> orderEntities = new ArrayList<>(100);
        List<String> users = Arrays.asList("xiaoming", "xiaohong", "xiaolan");

        for (int i = 0; i < 100; i++) {
            OrderEntity orderEntity = new OrderEntity();
            orderEntity.setId(String.valueOf(i));
            int i1 = i % 3;
            String uid = users.get(i1);
            orderEntity.setUid(uid);
            orderEntity.setOrderNo(i);
            orderEntities.add(orderEntity);
        }
        long l = easyQuery.insertable(orderEntities).executeRows();
        return "成功插入:"+l;
    }
}

查询单条

按分片键查询

可以完美的路由到对应的数据库表和操作单表拥有一样的性能

    @GetMapping("/first")
    public Object first(@RequestParam("id") String id) {
        OrderEntity orderEntity = easyQuery.queryable(OrderEntity.class)
                .whereById(id).firstOrNull();
        return orderEntity;
    }
http://localhost:8080/order/first?id=20
{"id":"20","uid":"xiaolan","orderNo":20}


http-nio-8080-exec-1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_03` t WHERE t.`id` = ? LIMIT 1
==> http-nio-8080-exec-1, name:ds0, Parameters: 20(String)
<== Total: 1

日志稍微解释一下

  • http-nio-8080-exec-1表示当前语句执行的线程,默认多个分片聚合后需要再线程池中查询数据后聚合返回。
  • name:ds0 表示数据源叫做ds0,如果不分库那么这个数据源可以忽略,也可以自己指定配置文件中或者设置defaultDataSourceName

全程无需您去计算路由到哪里,并且规则和业务代码已经脱离解耦

不按分片键查询

当我们的查询为非分片键查询那么会导致路由需要进行全分片扫描然后来获取对应的数据进行判断哪个时我们要的


    @GetMapping("/firstByUid")
    public Object firstByUid(@RequestParam("uid") String uid) {
        OrderEntity orderEntity = easyQuery.queryable(OrderEntity.class)
                .where(o->o.eq(OrderEntity::getUid,uid)).firstOrNull();
        return orderEntity;
    }

http://localhost:8080/order/firstByUid?uid=xiaoming
{"id":"18","uid":"xiaoming","orderNo":18}

//这边把日志精简了一下可以看到他是开启了5个线程进行分片查询
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_00` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_03` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_04` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_02` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_01` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: xiaoming(String)
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: xiaoming(String)
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: xiaoming(String)
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: xiaoming(String)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: xiaoming(String)
<== Total: 1

因为uid不是分片键所以在分片查询的时候需要遍历所有的表然后返回对应的数据,可能有同学会问就这?当然这只是简单演示后续下一篇我会给出具体的优化方案来进行处理。

分页查询

分片后的分页查询是分片下的一个难点,这边框架自带功能,分片后分页之所以难是因为如果是自行实现业务代码会变得非常复杂,有一种非常简易的方式就是把分页重写pageIndex永远为1,然后全部取到内存后在进行stream过滤,但是带来的另一个问题就是pageIndex不能便宜过大不然内存会完全存不下导致内存爆炸,并且如果翻页到最后几页那将是灾难性的,给程序带来极其不稳定,但是easy-query提供了和sharding-sphere一样的分片聚合方式并且因为靠近业务的关系所以可以有效的优化深度分页pageIndex过大


    @GetMapping("/page")
    public Object page(@RequestParam("pageIndex") Integer pageIndex,@RequestParam("pageSize") Integer pageSize) {
        EasyPageResult<OrderEntity> pageResult = easyQuery.queryable(OrderEntity.class)
                .orderByAsc(o -> o.column(OrderEntity::getOrderNo))
                .toPageResult(pageIndex, pageSize);
        return pageResult;
    }


http://localhost:8080/order/page?pageIndex=1&pageSize=10

{"total":100,"data":[{"id":"0","uid":"xiaoming","orderNo":0},{"id":"1","uid":"xiaohong","orderNo":1},{"id":"2","uid":"xiaolan","orderNo":2},{"id":"3","uid":"xiaoming","orderNo":3},{"id":"4","uid":"xiaohong","orderNo":4},{"id":"5","uid":"xiaolan","orderNo":5},{"id":"6","uid":"xiaoming","orderNo":6},{"id":"7","uid":"xiaohong","orderNo":7},{"id":"8","uid":"xiaolan","orderNo":8},{"id":"9","uid":"xiaoming","orderNo":9}]}
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT COUNT(1) FROM `order_02` t
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT COUNT(1) FROM `order_03` t
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT COUNT(1) FROM `order_04` t
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT COUNT(1) FROM `order_01` t
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT COUNT(1) FROM `order_00` t
<== Total: 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_04` t ORDER BY t.`order_no` ASC LIMIT 10
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_03` t ORDER BY t.`order_no` ASC LIMIT 10
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_00` t ORDER BY t.`order_no` ASC LIMIT 10
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_01` t ORDER BY t.`order_no` ASC LIMIT 10
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_02` t ORDER BY t.`order_no` ASC LIMIT 10
<== Total: 10

这边可以看到一行代码实现分页,下面是第二页

http://localhost:8080/order/page?pageIndex=2&pageSize=10
{"total":100,"data":[{"id":"10","uid":"xiaohong","orderNo":10},{"id":"11","uid":"xiaolan","orderNo":11},{"id":"12","uid":"xiaoming","orderNo":12},{"id":"13","uid":"xiaohong","orderNo":13},{"id":"14","uid":"xiaolan","orderNo":14},{"id":"15","uid":"xiaoming","orderNo":15},{"id":"16","uid":"xiaohong","orderNo":16},{"id":"17","uid":"xiaolan","orderNo":17},{"id":"18","uid":"xiaoming","orderNo":18},{"id":"19","uid":"xiaohong","orderNo":19}]}

==> SHARDING_EXECUTOR_9, name:ds0, Preparing: SELECT COUNT(1) FROM `order_02` t
==> SHARDING_EXECUTOR_8, name:ds0, Preparing: SELECT COUNT(1) FROM `order_01` t
==> SHARDING_EXECUTOR_10, name:ds0, Preparing: SELECT COUNT(1) FROM `order_04` t
==> SHARDING_EXECUTOR_7, name:ds0, Preparing: SELECT COUNT(1) FROM `order_03` t
==> SHARDING_EXECUTOR_6, name:ds0, Preparing: SELECT COUNT(1) FROM `order_00` t
<== Total: 1
==> SHARDING_EXECUTOR_9, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_01` t ORDER BY t.`order_no` ASC LIMIT 20
==> SHARDING_EXECUTOR_8, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_03` t ORDER BY t.`order_no` ASC LIMIT 20
==> SHARDING_EXECUTOR_10, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_04` t ORDER BY t.`order_no` ASC LIMIT 20
==> SHARDING_EXECUTOR_6, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_02` t ORDER BY t.`order_no` ASC LIMIT 20
==> SHARDING_EXECUTOR_7, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_00` t ORDER BY t.`order_no` ASC LIMIT 20
<== Total: 10

按时间分表

这边我们简单还是以order订单为例,按月进行分片假设我们从2022年1月到2023年5月一共17个月表名为t_order_202201t_order_202202t_order_202203t_order_202304t_order_202305

数据库脚本

create table t_order_202201
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int not null comment '订单号',
    create_time datetime not null comment '创建时间'
)comment '订单表';
create table t_order_202202
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int not null comment '订单号',
    create_time datetime not null comment '创建时间'
)comment '订单表';
....
create table t_order_202304
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int not null comment '订单号',
    create_time datetime not null comment '创建时间'
)comment '订单表';
create table t_order_202305
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int not null comment '订单号',
    create_time datetime not null comment '创建时间'
)comment '订单表';

@Data
@Table(value = "t_order",shardingInitializer = OrderByMonthShardingInitializer.class)
public class OrderByMonthEntity {

    @Column(primaryKey = true)
    private String id;
    private String uid;
    private Integer orderNo;
    /**
     * 分片键改为时间
     */
    @ShardingTableKey
    private LocalDateTime createTime;
}

//路由规则可以直接继承AbstractShardingMonthInitializer也可以自己实现
@Component
public class OrderByMonthShardingInitializer extends AbstractShardingMonthInitializer<OrderByMonthEntity> {
   /**
     * 开始时间不可以使用LocalDateTime.now()因为会导致每次启动开始时间都不一样
     * @return
     */
    @Override
    protected LocalDateTime getBeginTime() {
        return LocalDateTime.of(2022,1,1,0,0);
    }

    /**
     * 如果不设置那么就是当前时间,用于程序启动后自动计算应该有的表包括最后时间
     * @return
     */
    @Override
    protected LocalDateTime getEndTime() {
        return LocalDateTime.of(2023,5,31,0,0);
    }

    @Override
    public void configure0(ShardingEntityBuilder<OrderByMonthEntity> builder) {
        //后续用来实现优化分表
    }
}
//按月分片路由规则也可以自己实现因为框架已经封装好了所以可以用框架自带的
@Component
public class OrderByMonthTableRouteRule extends AbstractMonthTableRule<OrderByMonthEntity> {
    @Override
    protected LocalDateTime convertLocalDateTime(Object shardingValue) {
        return (LocalDateTime)shardingValue;
    }
}

初始化


@RestController
@RequestMapping("/orderMonth")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class OrderMonthController {

    private final EasyQuery easyQuery;

    @GetMapping("/init")
    public Object init() {
        ArrayList<OrderByMonthEntity> orderEntities = new ArrayList<>(100);
        List<String> users = Arrays.asList("xiaoming", "xiaohong", "xiaolan");
        LocalDateTime beginTime=LocalDateTime.of(2022,1,1,0,0);
        LocalDateTime endTime=LocalDateTime.of(2023,5,31,0,0);
        int i=0;
        while(!beginTime.isAfter(endTime)){

            OrderByMonthEntity orderEntity = new OrderByMonthEntity();
            orderEntity.setId(String.valueOf(i));
            int i1 = i % 3;
            String uid = users.get(i1);
            orderEntity.setUid(uid);
            orderEntity.setOrderNo(i);
            orderEntity.setCreateTime(beginTime);
            orderEntities.add(orderEntity);
            beginTime=beginTime.plusDays(1);
            i++;
        }
        long l = easyQuery.insertable(orderEntities).executeRows();
        return "成功插入:"+l;
    }
}

http://localhost:8080/orderMonth/init
成功插入:516

获取第一条数据

    @GetMapping("/first")
    public Object first(@RequestParam("id") String id) {
        OrderEntity orderEntity = easyQuery.queryable(OrderEntity.class)
                .whereById(id).firstOrNull();
        return orderEntity;
    }

http://localhost:8080/orderMonth/first?id=11
{"id":"11","uid":"xiaolan","orderNo":11,"createTime":"2022-01-12T00:00:00"}
//以每5组一个次并发执行聚合

==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202205` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202207` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202303` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202212` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202302` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202304` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202206` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202305` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202209` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202204` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202208` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202201` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202210` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202202` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202211` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202203` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202301` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 11(String)
<== Total: 1

获取范围内的数据

    @GetMapping("/range")
    public Object first() {
        List<OrderByMonthEntity> list = easyQuery.queryable(OrderByMonthEntity.class)
                .where(o -> o.rangeClosed(OrderByMonthEntity::getCreateTime, LocalDateTime.of(2022, 3, 1, 0, 0), LocalDateTime.of(2022, 9, 1, 0, 0)))
                .toList();
        return list;
    }
http://localhost:8080/orderMonth/range
[{"id":"181","uid":"xiaohong","orderNo":181,"createTime":"2022-07-01T00:00:00"},{"id":"182","uid":"xiaolan","orderNo":182,"createTime":"2022-07-02T00:00:00"},{"id":"183","uid":"xiaoming","orderNo":183,"createTime":"2022-07-03T00:00:00"},...........,{"id":"239","uid":"xiaolan","orderNo":239,"createTime":"2022-08-28T00:00:00"},{"id":"240","uid":"xiaoming","orderNo":240,"createTime":"2022-08-29T00:00:00"},{"id":"241","uid":"xiaohong","orderNo":241,"createTime":"2022-08-30T00:00:00"},{"id":"242","uid":"xiaolan","orderNo":242,"createTime":"2022-08-31T00:00:00"}]

//可以精准定位到对应的分片路由上获取数据
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202207` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202209` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202206` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202203` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202205` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202208` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202204` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
<== Total: 185

最后

目前为止你已经看到了easy-query对于分片的便捷性,但是本章只是开胃小菜,相信了解分库分表的小伙伴肯定会说就这?不是和sharding-jdbc一样吗为什么要用你的呢。我想说第一篇只是给大家了解一下如何使用,后续的文章才是分表分库的精髓相信我你一定没看过

demo地址 https://github.com/xuejmnet/easy-sharding-test

你没见过的分库分表原理解析和解决方案(一) - 薛家明 - 博客园

mikel阅读(297)

来源: 你没见过的分库分表原理解析和解决方案(一) – 薛家明 – 博客园

你没见过的分库分表原理解析和解决方案(一)

高并发三驾马车:分库分表、MQ、缓存。今天给大家带来的就是分库分表的干货解决方案,哪怕你不用我的框架也可以从中听到不一样的结局方案和实现。

一款支持自动分表分库的orm框架easy-query 帮助您解脱跨库带来的复杂业务代码,并且提供多种结局方案和自定义路由来实现比中间件更高性能的数据库访问。

上篇文章简单的带大家了解了框架如何使用分片本章将会以理论为主加实践的方式呈现不一样的分表分库。

介绍

分库分表一直是老生常谈的问题,市面上也有很多人侃侃而谈,但是大部分的说辞都是一样,甚至给不出一个实际的解决方案,本人经过多年的深耕在其他语言里面多年的维护和实践下来秉着happy coding的原则希望更多的人可以了解和认识到该框架并且给大家一个全新的针对分库分表的认识。
我们也经常戏称项目一开始就用了分库分表结果上线没多少数据,并且整个开发体验来说非常繁琐,对于业务而言也是极其不友好,大大拉长开发周期不说,bug也是更加容易产生,针对上述问题该框架给出了一个非常完美的实现来极大程度上的给用户完美的体验

分片存储

分库分表简单的实现目前大部分框架已经都可以实现了,就是动态表名来实现分表下的简单存储,如果是分库下面的那么就使用动态数据源来切换实现,如果是分库加分表就用动态数据源加动态表名来实现,听上去是不是很完美,但是实际情况下你需要表写非常繁多的业务代码,并且会让整个开发精力全部集中在分库分表下,针对后期的维护也是非常麻烦的一件事。
但是分库分表的分片规则又是和具体业务耦合的所以合理的解耦分片路由是一件非常重要的事情。

插入

假设我们按订单id进行分表存储

通过上述图片我们可以很清晰的了解到分片插入的执行原理,通过拦截执行SQL分析对应的值计算出所属表名,然后改写表名进行插入。该实现方法有一个弊端就是如果插入数据是increment的自增类型,那么这种方法将不适合,因为自增主键只有在插入数据库后才会正真的被确定是什么值,可以通过拦截器设置自定义自增拨号器来实现伪自增,这样也可以实现“自增”列。

更新删除)

这边假设我们也是按照订单id进行分表更新

更新分片键


一模一样的处理,将SQL进行拦截后解析where和分片字段id然后计算后将结果发送到对应路由的表中进行执行。

那么如果我们没办法进行路由确定呢,如果我们使用created字段来更新的那么会发生生呢

更新非分片键


为了得到正确的结果需要将每条sql进行改写分别发送到对应的表中,然后将各自表的执行结果进行聚合返回最终受影响行数

分片查询

众所周知分库分表的难点并不在如何存储数据到对应的db,也不在于如何更新指定实体数据,因为他们都可以通过分片键的计算来重新路由,可以让分片的操作降为单表操作,所以orm只需要支持动态表名那么以上所有功能都是支持的,
但是实际情况缺是如果orm或者中间件只支持到了这个级别那么对于稍微复杂一点的业务你必须要编写大量的业务代码来实现业务需要的查询,并且会浪费大量的重复工作和精力

单分片表查询

加下来我来讲解单分片表查询,其实原理和上面的insert一样

到这里为止其实都是ok的并没有什么问题.但是如果我们的本次查询需要跨分片呢比如跨两个分片那么应该如何处理

跨分片表查询

到这一步我们已经将对应的数据路由到对应的数据库了,那么我们应该如何获取自己想要的结果呢

通过上图我们可以了解到在跨分片的聚合下我们可以分表通过对a,b两张表进行查询可以并行可以串行,最终将结果汇聚到同一个集合那么返回给用户端就是一个完整的数据包,并没有缺少任何数据

跨分片排序

基于上述分片聚合方式我们清晰的了解到如何可以进行跨分片下降数据获取到内存中,但是通过图中结果可以清晰的了解到返回的数据并不像我们预期的那样有序,那是因为各个节点下的所有数据都是仅遵循各自节点的数据库排序而不受其他节点分片影响。
那么如果我们对数据进行分片聚合+排序那么又会是什么样的场景呢

方案一内存排序

首先我们将执行sql分别路由到t_order_1t_order_2两张表,并且执行order by id desc将其数据id大的排在前面这样可以保证单个ConnectionResultSet肯定是大的先被返回
所以在单个Connection下结果是正确的但是因为多个分片节点间没有交互所以当取到内存中后数据依然是乱的,所以这边需要对sql进行拦截获取排序字段并且将其在内存中的集合里面实现,这样我们就做到了和排序字段一样的返回结果

方案二流式排序

大部分orm到这边就为止了,毕竟已经实现了完美的节点处理,但是我们来看他需要消耗的性能事多少,假设我们分片命中2个节点,每个节点各自返回2条数据,我们对于整个ResultSet的遍历将是每个链接都是2那么就是4次,然后在内存中在进行排序如果性能差一点还需要多次所以这个是相对比较浪费性能的,因为如果我们有1000条数据返回那么内存中的排序是很高效的但是这个也是我们这次需要讲解的更加高效的排序处理流式排序

相较于内存排序这种方式十分复杂并且繁琐,而且对于用户也很不好理解,但是如果你获取的数据是分页,那么内存排序进行获取结果将会变得非常危险,有可能导致内存数据过大从而导致程序崩溃

无order字段

到这边不要以为跨分片聚合已经结束了因为当你的sql查询order by了一个select不存在的字段,那么上述两种排序方式都将无法使用,因为程序获取到的结果集并没有排序字段,这个时候一般我们会改写sql让其select的时候必须要带上对应的order by字段这样就可以保证我们数据的正确返回

以下两个问题因为涉及到过多内容本章节无法呈现所以将会在下一章给出具体解决方案

跨分片分组

如果我们程序遇到了这个那么我们该如何处理呢

跨分片分页

业务中常常需要的跨分片分页我们该如何解决,easy-query又如何处理这种情况,如果跨的分片过多我们又该怎么办,

  • 如何解决深分页问题
  • 如何解决流式瀑布问题
  • 如何进行分页缓存高效获取问题

接下来将在下篇文章中一一解答近

最后

我这边将演示easy-query在本次分片理论中的实际应用
这次采用h2数据库作为演示

CREATE TABLE IF NOT EXISTS `t_order_0`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );
CREATE TABLE IF NOT EXISTS `t_order_1`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );
CREATE TABLE IF NOT EXISTS `t_order_2`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );
CREATE TABLE IF NOT EXISTS `t_order_3`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );
CREATE TABLE IF NOT EXISTS `t_order_4`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );

安装maven依赖


        <dependency>
            <groupId>com.easy-query</groupId>
            <artifactId>sql-h2</artifactId>
            <version>0.9.32</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.easy-query</groupId>
            <artifactId>sql-api4j</artifactId>
            <version>0.9.32</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.199</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>

创建实体对象对应数据库


@Data
@Table(value = "t_order",shardingInitializer = H2OrderShardingInitializer.class)
public class H2Order {
    @Column(primaryKey = true)
    @ShardingTableKey
    private Integer id;
    private Integer status;
    private String created;
}
// 分片初始化器

public class H2OrderShardingInitializer extends AbstractShardingTableModInitializer<H2Order> {
    @Override
    protected int mod() {
        return 5;//模5
    }

    @Override
    protected int tailLength() {
        return 1;//表后缀长度1位
    }
}
//分片路由规则

public class H2OrderRule extends AbstractModTableRule<H2Order> {
    @Override
    protected int mod() {
        return 5;
    }

    @Override
    protected int tailLength() {
        return 1;
    }
}

创建datasource和easyquery

   orderShardingDataSource=DataSourceFactory.getDataSource("dsorder","h2-dsorder.sql");
   EasyQueryClient easyQueryClientOrder = EasyQueryBootstrapper.defaultBuilderConfiguration()
                .setDefaultDataSource(orderShardingDataSource)
                .optionConfigure(op -> {
                    op.setMaxShardingQueryLimit(10);
                    op.setDefaultDataSourceName("ds2020");
                    op.setDefaultDataSourceMergePoolSize(20);
                })
                .build();
      EasyQuery   easyQueryOrder = new DefaultEasyQuery(easyQueryClientOrder);

        QueryRuntimeContext runtimeContext = easyQueryOrder.getRuntimeContext();
        QueryConfiguration queryConfiguration = runtimeContext.getQueryConfiguration();
        queryConfiguration.applyShardingInitializer(new H2OrderShardingInitializer());//添加分片初始化器
        TableRouteManager tableRouteManager = runtimeContext.getTableRouteManager();
        tableRouteManager.addRouteRule(new H2OrderRule());//添加分片路由规则

插入代码


  ArrayList<H2Order> h2Orders = new ArrayList<>();
  for (int i = 0; i < 100; i++) {
      H2Order h2Order = new H2Order();
      h2Order.setId(i);
      h2Order.setStatus(i%3);
      h2Order.setCreated(String.valueOf(i));
      h2Orders.add(h2Order);
  }
  easyQueryOrder.insertable(h2Orders).executeRows();
==> main, name:ds2020, Preparing: INSERT INTO t_order_3 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 0(Integer),0(Integer),0(String)
<== main, name:ds2020, Total: 1
==> main, name:ds2020, Preparing: INSERT INTO t_order_4 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 1(Integer),1(Integer),1(String)
<== main, name:ds2020, Total: 1
==> main, name:ds2020, Preparing: INSERT INTO t_order_0 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 2(Integer),2(Integer),2(String)
<== main, name:ds2020, Total: 1
==> main, name:ds2020, Preparing: INSERT INTO t_order_1 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 3(Integer),0(Integer),3(String)
<== main, name:ds2020, Total: 1
==> main, name:ds2020, Preparing: INSERT INTO t_order_2 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 4(Integer),1(Integer),4(String)
.....省略
       List<H2Order> list = easyQueryOrder.queryable(H2Order.class)
                .where(o -> o.in(H2Order::getId, Arrays.asList(1, 2, 6, 7)))
                .toList();
        Assert.assertEquals(4,list.size());
==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT id,status,created FROM t_order_3 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_4, name:ds2020, Preparing: SELECT id,status,created FROM t_order_0 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_3, name:ds2020, Preparing: SELECT id,status,created FROM t_order_4 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_1, name:ds2020, Preparing: SELECT id,status,created FROM t_order_2 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_4, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_5, name:ds2020, Preparing: SELECT id,status,created FROM t_order_1 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_3, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_5, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_1, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_2, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_5, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_1, name:ds2020, Time Elapsed: 1(ms)
<== SHARDING_EXECUTOR_4, name:ds2020, Time Elapsed: 1(ms)
<== SHARDING_EXECUTOR_3, name:ds2020, Time Elapsed: 1(ms)
<== Total: 4
``
通过上述sql展示我们可以清晰的看到哪个线程执行了哪个数据源(分片下会不一样),执行了什么sql,最终执行消耗多少时间参数是多少,一共返回多少条数据
分片排序
```java
  List<H2Order> list = easyQueryOrder.queryable(H2Order.class)
                .where(o -> o.in(H2Order::getId, Arrays.asList(1, 2, 6, 7)))
                .orderByDesc(o->o.column(H2Order::getId))
                .toList();
  Assert.assertEquals(4,list.size());
  Assert.assertEquals(7,(int)list.get(0).getId());
  Assert.assertEquals(6,(int)list.get(1).getId());
  Assert.assertEquals(2,(int)list.get(2).getId());
  Assert.assertEquals(1,(int)list.get(3).getId());
==> SHARDING_EXECUTOR_1, name:ds2020, Preparing: SELECT id,status,created FROM t_order_1 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_5, name:ds2020, Preparing: SELECT id,status,created FROM t_order_3 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_4, name:ds2020, Preparing: SELECT id,status,created FROM t_order_2 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_3, name:ds2020, Preparing: SELECT id,status,created FROM t_order_4 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_5, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_1, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_4, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT id,status,created FROM t_order_0 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_3, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_2, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
<== SHARDING_EXECUTOR_1, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_5, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_4, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_3, name:ds2020, Time Elapsed: 0(ms)
<== Total: 4

最后的最后

附上源码地址,源码中有文档和对应的qq群,如果决定有用请点击star谢谢大家了

你没见过的分库分表原理解析和解决方案(二) - 薛家明 - 博客园

mikel阅读(407)

来源: 你没见过的分库分表原理解析和解决方案(二) – 薛家明 – 博客园

你没见过的分库分表原理解析和解决方案(二)

高并发三驾马车:分库分表、MQ、缓存。今天给大家带来的就是分库分表的干货解决方案,哪怕你不用我的框架也可以从中听到不一样的结局方案和实现。

一款支持自动分表分库的orm框架easy-query 帮助您解脱跨库带来的复杂业务代码,并且提供多种结局方案和自定义路由来实现比中间件更高性能的数据库访问。

上篇文章简单的带大家了解了分表分库的原理和聚合解析,但是还留了两个坑一个是分组如何实现一个是分页如何实现

介绍

分库分表的难题一直不是如何插入一直都是如何实现聚合查询,让用户无感知的使用才是分库分表的最终形态,所以数据坐落和数据聚合将是分库分表的重中之重,随着版本迭代easy-query正式发布了1.0.0版本相对的api基本已经稳定,分库分表和之前稍微有点不一样但是大部分都是一样的,那么这次我们将使用1.0.6来实现分库分表下的数据分组和分页。

数据准备

本次我们以订单为例,然后以订单创建时间进行按年分库,按月分表,最后来实现上述的分组和分页的功能

默认配置项

数据源名称 对应数据库 对应的订单年份 对应的订单表
ds0 sharding-order 2020年 t_order_202001,t_order_202002…..,t_order_202011,t_order_202012
ds1 sharding-order1 2021年 t_order_202101,t_order_202102…..,t_order_202111,t_order_202112
ds2 sharding-order2 2022年 t_order_202201,t_order_202202…..,t_order_202211,t_order_202212
ds3 sharding-order3 2023年 t_order_202301,t_order_202302…..,t_order_202311,t_order_202312

添加依赖

        <!--druid依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.15</version>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
        </dependency>
        <dependency>
            <groupId>com.easy-query</groupId>
            <artifactId>sql-processor</artifactId>
            <version>1.1.7</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.easy-query</groupId>
            <artifactId>sql-springboot-starter</artifactId>
            <version>1.1.7</version>
            <scope>compile</scope>
        </dependency>

添加配置文件

server:
  port: 8081

spring:
  profiles:
    active: dev

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/sharding-order?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
    username: root
    password: root
    druid:
      initial-size: 10
      max-active: 100


easy-query:
  enable: true
  name-conversion: underlined
  database: mysql
  default-data-source-merge-pool-size: 60
  default-data-source-name: ds0

新建一个订单QOrderEntity按季度进行分表分库

//分片表
@Data
@Table(value = "t_order", shardingInitializer = OrderInitializer.class)
@EntityProxy
public class OrderEntity {
    @Column(primaryKey = true)
    private String id;
    private Integer orderNo;
    private String userId;
    @ShardingTableKey
    @ShardingDataSourceKey
    private LocalDateTime createTime;
}


//分片初始化器
@Component
public class OrderInitializer extends AbstractShardingMonthInitializer<OrderEntity> {
    /**
     * 分片起始时间
     * @return
     */
    @Override
    protected LocalDateTime getBeginTime() {
        return LocalDateTime.of(2020,1,1,0,0,0);
    }

    /**
     * 格式化时间到数据源
     * @param time
     * @param defaultDataSource
     * @return
     */
    @Override
    protected String formatDataSource(LocalDateTime time, String defaultDataSource) {
        String year = DateTimeFormatter.ofPattern("yyyy").format(time);
        int i = Integer.parseInt(year)-2020;
        
        return "ds"+i;
    }
    @Override
    public void configure0(ShardingEntityBuilder<OrderEntity> builder) {

    }
}

//动态添加spring 启动后的动态数据源额外的ds1、ds2、ds3

@Component
public class ShardingInitRunner implements ApplicationRunner {
    @Autowired
    private EasyQuery easyQuery;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Map<String, DataSource> dataSources = createDataSources();
        DataSourceManager dataSourceManager = easyQuery.getRuntimeContext().getDataSourceManager();
        for (Map.Entry<String, DataSource> stringDataSourceEntry : dataSources.entrySet()) {

            dataSourceManager.addDataSource(stringDataSourceEntry.getKey(), stringDataSourceEntry.getValue(), 60);
        }
        System.out.println("初始化完成");
    }

    private Map<String, DataSource> createDataSources() {
        HashMap<String, DataSource> stringDataSourceHashMap = new HashMap<>();
        for (int i = 1; i < 4; i++) {
            DataSource dataSource = createDataSource("ds" + i, "jdbc:mysql://127.0.0.1:3306/sharding-order" + i + "?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true", "root", "root");
            stringDataSourceHashMap.put("ds" + i, dataSource);
        }
        return stringDataSourceHashMap;
    }

    private DataSource createDataSource(String dsName, String url, String username, String password) {

        // 设置properties
        Properties properties = new Properties();
        properties.setProperty("name", dsName);
        properties.setProperty("driverClassName", "com.mysql.cj.jdbc.Driver");
        properties.setProperty("url", url);
        properties.setProperty("username", username);
        properties.setProperty("password", password);
        properties.setProperty("initialSize", "10");
        properties.setProperty("maxActive", "100");
        try {
            return DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            throw new EasyQueryException(e);
        }
    }
}

//新建分库路由

@Component
public class OrderDataSourceRoute extends AbstractDataSourceRoute<OrderEntity> {
    protected Integer formatShardingValue(LocalDateTime time) {
        String year = time.format(DateTimeFormatter.ofPattern("yyyy"));
        return Integer.parseInt(year);
    }
    public boolean lessThanTimeStart(LocalDateTime shardingValue) {
        LocalDateTime timeYearFirstDay = EasyUtil.getYearStart(shardingValue);
        return shardingValue.isEqual(timeYearFirstDay);
    }

    protected Comparator<String> getDataSourceComparator(){
        return IgnoreCaseStringComparator.DEFAULT;
    }
    @Override
    protected RouteFunction<String> getRouteFilter(TableAvailable table, Object shardingValue, ShardingOperatorEnum shardingOperator, boolean withEntity) {
        //将分片键转成对应的类型
        LocalDateTime shardingTime = (LocalDateTime)shardingValue ;
        Integer intYear = formatShardingValue(shardingTime);
        String dataSourceName="ds"+String.valueOf((intYear-2020));//ds0 ds1 ds2 ds3....
        switch (shardingOperator) {
            case GREATER_THAN:
            case GREATER_THAN_OR_EQUAL:
                return ds -> getDataSourceComparator().compare(dataSourceName, ds) <= 0;
            case LESS_THAN: {
                //如果小于月初那么月初的表是不需要被查询的 如果小于年初也不需要查询
                if (lessThanTimeStart(shardingTime)) {
                    return ds -> getDataSourceComparator().compare(dataSourceName, ds) > 0;
                }
                return ds -> getDataSourceComparator().compare(dataSourceName, ds) >= 0;
            }
            case LESS_THAN_OR_EQUAL:
                return ds -> getDataSourceComparator().compare(dataSourceName, ds) >= 0;

            case EQUAL:
                return ds -> getDataSourceComparator().compare(dataSourceName,ds) == 0;
            default:
                return ds -> true;
        }
    }
}

//新建分表路由
//分表路由由系统提供默认按月分片
@Component
public class OrderTableRoute extends AbstractMonthTableRoute<OrderEntity> {

    @Override
    protected LocalDateTime convertLocalDateTime(Object shardingValue) {
        return (LocalDateTime)shardingValue;
    }
}
```
通过sql脚本我们创建好对应的数据库表结构
![](https://img2023.cnblogs.com/blog/1346660/202306/1346660-20230626215913917-1277133380.png)

初始化项目代码
``java

    private final EasyProxyQuery easyProxyQuery;
    @GetMapping("/init")
    public Object init() {

        long start = System.currentTimeMillis();
        LocalDateTime beginTime = LocalDateTime.of(2020, 1, 1, 0, 0, 0);
        LocalDateTime now = LocalDateTime.now();
        ArrayList<OrderEntity> orderEntities = new ArrayList<>();
        List<String> userIds = Arrays.asList("小明", "小红", "小蓝", "小黄", "小绿");
        int i=0;
        do {
            OrderEntity orderEntity = new OrderEntity();
            String timeFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(beginTime);
            orderEntity.setId(timeFormat);
            orderEntity.setOrderNo(i);
            orderEntity.setUserId(userIds.get(i%5));
            orderEntity.setCreateTime(beginTime);
            orderEntities.add(orderEntity);
            i++;
            beginTime=beginTime.plusMinutes(1);
        } while (beginTime.isBefore(now));

        long end = System.currentTimeMillis();

        long insertStart = System.currentTimeMillis();
        long rows = easyProxyQuery.insertable(orderEntities).executeRows();
        long insertEnd = System.currentTimeMillis();

        return "成功插入:" + rows+",其中路由对象生成耗时:"+(end-start)+"(ms),插入耗时:"+(insertEnd-insertStart)+"(ms)";
    }
```
![](https://img2023.cnblogs.com/blog/1346660/202306/1346660-20230626223148693-872730237.png)
数据初始化成功,接下来演示如何进行分组
# 分组聚合

分表分库下我们应该如何分组聚合

代码很简单就是查询userId in ["小明", "小绿"]的然后对userId分组求对应的订单号求和
````java

    @GetMapping("/groupByWithSumOrderNo")
    public Object groupByWithSumOrderNo() {
        long start = System.currentTimeMillis();
        List<String> userIds = Arrays.asList("小明", "小绿");
        List<OrderGroupWithSumOrderNoVO> list = easyProxyQuery.queryable(OrderEntityProxy.DEFAULT)
                .where((filter, t) -> filter.in(t.userId(), userIds))
                .groupBy((group, t) -> group.column(t.userId()))
                .select(OrderGroupWithSumOrderNoVOProxy.DEFAULT, (selector, t) -> selector.columnAs(t.userId(), r -> r.userId()).columnSumAs(t.orderNo(), r -> r.orderNoSum()))
                .toList();
        long end = System.currentTimeMillis();
        return Arrays.asList(list,(end-start)+"(ms)");
    }

[[{"orderNoSum":993365517,"userId":"小明"},{"orderNoSum":992998911,"userId":"小绿"}],"1768(ms)"]共计耗时约1.8秒

    @GetMapping("/groupByWithSumOrderNoOrderByUserId")
    public Object groupByWithSumOrderNoOrderByUserId() {
        long start = System.currentTimeMillis();
        List<String> userIds = Arrays.asList("小明", "小绿");
        List<OrderGroupWithSumOrderNoVO> list = easyProxyQuery.queryable(OrderEntityProxy.DEFAULT)
                .where((filter, t) -> filter.in(t.userId(), userIds))
                .groupBy((group, t) -> group.column(t.userId()))
                .orderByAsc((order,t)->order.column(t.userId()))
                .select(OrderGroupWithSumOrderNoVOProxy.DEFAULT, (selector, t) -> selector.columnAs(t.userId(), r -> r.userId()).columnSumAs(t.orderNo(), r -> r.orderNoSum()))
                .toList();
        long end = System.currentTimeMillis();
        return Arrays.asList(list,(end-start)+"(ms)");
    }

[[{"orderNoSum":993365517,"userId":"小明"},{"orderNoSum":992998911,"userId":"小绿"}],"1699(ms)"]
我们非常快速的获取了查询结果,那么这个结果是如何获取的呢接下来我将讲解分组聚合的原理并且会讲解order by对group在分片中的影响是有多大的影响

分组求和

原理解析这边以两个分片来进行聚合

SQL进行路由解析后分别对两个分片节点进行查询聚合,然后合并到内存中分别对groupsum进行处理实现无感知分片聚合group,但是大家可能已经发现了这个是sum那么各个节点的数据可以相加如果是avg呢应该怎么办,接下来我来讲解group下如何进行avg的数据聚合查询。

分组取平均数

首先我们来假设一个表a和表b两个表里面数据如下


结果错误!!!
可以看到单纯的通过内存来进行平均值的聚合是不正确的,因为只有当各个分片内的数据和分片数一样才可以简单的avg,那么我们应该如何实现分组求平均呢。

1.avg本质等于什么?

我们都知道avg=sum/count通过这个公式avg是不可以简单分片聚合的那么如果我们知道sumcount呢,是不是就可以知道avg了,在退一万步我们已经知道avg的情况下是不是只需要知道sum或者count也可以算出对应的第三个值

2.如何实现

  • 如果用户存在group+avg那么强制要求进行对应avg字段也进行sum或者count的其中一个
  • 如果发现用户存在group+avg那么就自动重写添加sum或者count的查询

那么之前的group+avg就会变成group+avg+sum

通过上述描述我们应该可以清晰的看到应该如何针对各个节点的分组求平均值来处理正确的方法

内存分组聚合

上述所有例子我们都是通过内存分组聚合来实现各个节点的分组聚合,缺点就是需要先把各个节点的数据存储到内存中然后再次进行分组,那么如果各个节点的数据过多那么在分组聚合的第一阶段可能会导致内存的大量消耗,所以接下来我将给大家讲解流式分组聚合

流式分组聚合

上个文章我们讲解过流式聚合,那么流式分组聚合流式聚合的差别在哪呢,很明显就是分组这个关键字上,我们如何保证下一个next所需的数据就是我上一个需要group的呢

答案就是order by

只要各个节点的order by后的排序字段和编程语言在内存中的一样即可保证

easy-query是如何实现的

        List<String> userIds = Arrays.asList("小明", "小绿");
        List<OrderGroupWithAvgOrderNoVO> list = easyProxyQuery.queryable(OrderEntityProxy.DEFAULT)
                .where((filter, t) -> filter.in(t.userId(), userIds))
                .groupBy((group, t) -> group.column(t.userId()))
                .orderByAsc((order, t) -> order.column(t.userId()))
                .select(OrderGroupWithAvgOrderNoVOProxy.DEFAULT, (selector, t) -> selector.columnAs(t.userId(), r -> r.userId()).columnAvgAs(t.orderNo(), r -> r.orderNoAvg()))
                .toList();

//生成的sql 会自动补齐count和sum来保证数据结果的正确性
SELECT t.`user_id` AS `user_id`,AVG(t.`order_no`) AS `order_no_avg`,COUNT(t.`order_no`) AS `orderNoRewriteCount`,SUM(t.`order_no`) AS `orderNoRewriteSum` 
FROM `t_order_202211` t 
WHERE t.`user_id` IN (?,?) 
GROUP BY t.`user_id` 
ORDER BY t.`user_id` ASC
[[{"orderNoAvg":916515.0000,"userId":"小明"},{"orderNoAvg":916516.5000,"userId":"小绿"}],"1924(ms)"]

分别对其进行求和和求count
[[{"orderNoSum":336000814605,"userId":"小明"}],"914(ms)"]
[[{"orderNoAvg":366607,"userId":"小明"}],"777(ms)"]
916515.0000=336000814605/366607 所以结果是正确的

分页聚合

如果您在项目中使用过分库分表,那么一定知道分库分表的难点在哪里,那么就是聚合数据范围和跨表数据返回,如果跨表数据返回再有一个难点那么就是分片数据跨分片分页,并且支持条件排序等处理操作。

内存分页

内存分页作为最简单的分页方法,在前几页的处理中有着非常方便的和高效的实用,具体原理如下

--原始sql
select * from order where time between 2020 and 2021 order by time limit 1,5
假如他被路由到20202021两张表那么要获取前10条数据应该怎么写
select * from order_2020 where time between 2020 and 2021 order by time limit 1,5
select * from order_2021 where time between 2020 and 2021 order by time limit 1,5

分别对两张表进行前10条数据的获取然后再内存中就有20条数据,针对这20条数据进行order by time的相同操作,然后获取前10条

那么如果是获取第二页呢

--原始sql
select * from order where time between 2020 and 2021 order by time limit 2,5
假如他被路由到20202021两张表那么要获取前10条数据应该怎么写

--错误的做法
select * from order_2020 where time between 2020 and 2021 order by time limit 2,5
select * from order_2021 where time between 2020 and 2021 order by time limit 2,5

通过上图我们清晰地可以知道这个解析是错误那么正确的应该是怎么样的呢

--原始sql
select * from order where time between 2020 and 2021 order by time limit 2,5
假如他被路由到20202021两张表那么要获取前10条数据应该怎么写

--正确的做法
select * from order_2020 where time between 2020 and 2021 order by time limit 1,10
select * from order_2021 where time between 2020 and 2021 order by time limit 1,10

考虑到最坏的情况就是6-10全部在左侧或者全部在右侧,因为数据的分布无法知晓所以我们应该以最坏的情况来获取数据然后获取6-10条数据

好了这样我们就实现了如何用内存来实现

 int pageIndex=x
int pageSize=y;
那么重写后的sql应该是
 int pageIndex=1
int pageSize=x*y;

虽然我们发现了如何正确的获取分页数据但是也存在一个非常严重的问题,就是深度分页导致的内存爆炸,因为每个分片的获取对象都是xy那么如果有n个分片被本次查询覆盖那么就需要获取至多xy*n条数据到内存,其中pageIndex就是x是用户自行选择的所以会存在x的大小不确定这样就会导致内存的严重消耗甚至oom。

流式分页

既然我们已经知道了内存分页的缺点那么是否有办法针对上述缺点进行规避或者优化呢,答案是有的就是流式分页,所谓流式分页就是利用ResultSet的延迟获取特点,配合之前的流式获取来适当性的放弃头部数据来达到节省内存的效果.

流式分页是如何优化程序的

 int pageIndex=x
int pageSize=y;
那么重写后的sql应该是
 int pageIndex=1
int pageSize=x*y;

因为jdbc的resultset延迟获取的特性,所以每次调用next才会将数据取到客户端,利用这个特性可以将前5条数据获取到并且放弃来实现内存严格控制,并且满足获取条数后后面的11-20是不需要获取的,有效的避免网络I/O的浪费和大大提高性能。

虽然流式分页可以大大的提高内存利用率,并且可以用最少的I/O次数来获取正确的分页数量由原先的xyn变成x*y,但是我们会发现在深度分页的情况下网络io还是需要实打实的获取到客户端进行判断,所以在深分页下不仅数据库压力大,客户端网络I/O压力也大,并且在页数很大的情况下默认起始页和结束页是默认显示在分页组件上的那么就就会导致用户很容易点到页尾导致程序进入卡死状态并且响应变慢从而拖慢应用

反排分页

跨分片深度分页解决方案

  • 1.让用户妥协只支持瀑布流分页,就是app滚动相似的分页,不支持跳页只支持next页放弃count仅limit获取,并且无法自定义排序,业务上直接禁止页尾跳页直接避免问题
  • 2.反向排序分页依然是count+limit的组合

我们都知道跨分片的聚合是因为深度分页慢是因为网络I/O的大量读取,所以如果我们可以保证网络I/O的读取次数变少那么是否就能解决这个问题

何谓反向分页首先我们来看一张图

原来我们需要跳过大量的网络I/O才能获取的正确数据如果我们有反向分页那么只需要跳过少量的数据就可以实现深度分页,并且因为大部分业务场景都支持跳页所以count的查询是一定会有的,我们只需要对各个分片的count进行第一次查询的获取那么就可以保证在深度分页下的反向排序分页

通过对order by的反向置换并且将offset重新计算来四线深度跨分片分页下I/O的极大减少保证正序的健壮性

顺序分页

到目前为止我们的页首和页尾节点的分页已经解决了,那么针对分页的中间部分改怎么办呢,是否还有优化方案呢,答案是有的但是这个优化方案对于分片方式有特殊的要求并没有前两种的通用化,顺序分片。
什么叫做顺序分页,顺序分页就是例如按时间按月分表,按年分表,按天分表,每张的内部数据永远是有一个特殊的排序字段可以让其依次从小到大排列。如果分片是这种特性的分片那么可以保证在order by这个特殊字段的时候几乎可以做到除了页首和页尾甚至中间任意节点的高性能

到目前为止如果您是顺序分页并且排序字段是顺序字段那么可以保证跨分片的查询和普通查询基本没有两样,但是我们其实还是发现了一个问题就是每次查询都需要count一下这个其实是很费时间的,并且基本上如果是大数量的情况下基本大致数据不需要更新或者只需要更新最新的一页即可

指定分页

基于上述问题easy-query实现了指定分页,就是可以通过第一次的分片记录下当前条件的各个节点的count数据,那么接下来的查询如果条件没有变化就不需要再进行count了,并且针对最新节点依然可以选择单独查询count从而来保证数据的准确性。

最后

通过上述几个讲述您应该已经对分表分库有了一个全新的理解和优化,接下来的几个篇章我将带你通过简单的实现和高级的抽象来完成easy-query的全新orm的变成之旅让分表分库变得非常简单且非常高效,并且会提出多种解决方案来实现老旧数据的迁移,数据分片不均匀,多字段分片索引的种种解决方案。

如果觉得有用请点击star谢谢大家了

QQ群:170029046

给美女换衣服!做完这个 AI 教程被老婆暴打了一顿...... - 零度解说

mikel阅读(813)

来源: 给美女换衣服!做完这个 AI 教程被老婆暴打了一顿…… – 零度解说

1.下载最新版 stable-diffusion-webui 【点击下载
2.安装Python 3.10.6(较新版本的 Python 不支持 torch),勾选“Add Python to PATH”。
3.安装Git
4.以普通非管理员用户身份从 Windows 资源管理器运行webui-user.bat。
5.安装中文语言:

https://github.com/VinsonLaro/stable-diffusion-webui-chinese

6.暗黑模式:访问这个地址:

http://127.0.0.1:7860/?__theme=dark

7.安装 sd-webui-controlnet 外挂程序,新版本在插件中心搜索安装即可,如果你是旧版本,可以通过手动安装

https://github.com/Mikubill/sd-webui-controlnet

8.下载模型:【点击获取

 

9.所需主模型:chilloutmix_NiPrunedFp32Fix

Lora模型: 【cuteGirlMix4_v10】、【seeThroughSilhouette_v10

extremely detailed CG unity 8k wallpaper,(masterpiece),(best quality),(ultra detailed),(ultra realistic),(Best character details:1.36),nikon d750 f/1.4 55mm,dynamic angle,professional lighting, photon mapping, radiosity, physically-based rendering,
outdoors,looking at viewer,blush,(taut shirt), jeans,
1girl,(mature female:0.2),tall body,golden proportions,(Kpop idol),(shiny skin:1.2),(oil skin:1.1),makeup,[:(high detailed face:1.2):0.2]:, <lora:cuteGirlMix4_v10:0.8>, (close up), park, depth of field, <lora:seeThroughSilhouette_v10:0.5>,( closed mouth: 0.5)
((wavy gray hair and a sophisticated sense of style)),(aegyo sal:1),(puffy eyes),(eyelashes:1.1),(parted lips:1.1),red lipstick,wide shoulders,
Negative prompt: Multiple people,More than one person,2girl,DeepNegative,
sketches,lowres,polar lowres,(worst quality:2),(low quality:2),(normal quality:2),((monochrome)),((grayscale)),blurry,cropped,mutation,deformed,text,error,signature,watermark,username,extra digit,fewer digits,jpeg artifacts,
skin spots, acnes, skin blemishes,
bad anatomy,bad anatomy,bad proportions,gross proportions,long neck,cross-eyed,malformed limbs,blurred hands,fused fingers,poorly drawn face,poorly drawn hands,
(mutated hands and fingers:1.3),(mutated legs and foots:1.3),bad body,bad limbs,bad arms,bad hands,bad fingers,bad leg,bad feet,missing limbs,missing arms,missing hands,missing fingers,missing legs,missing footextra limbs,extra arms,extra fingers,extra leg,extra foot,
Steps: 28, Sampler: DPM++ SDE Karras, CFG scale: 7.5, Seed: 1340860639, Face restoration: CodeFormer, Size: 640x960, Model hash: fc2511737a, Model: chilloutmix_NiPrunedFp32Fix, Denoising strength: 0.4, Hires upscale: 1.5, Hires steps: 30, Hires upscaler: Latent (bicubic antialiased)

 

 

 

 

8.更多模板下载:

majicMIX sombre

XXMix_9realistic

 

majicMIX realistic

 

B2


B3

B4

 

8.进阶模型

推荐使用control_v11p_sd15_openpose.pth

期待下次更新…..

Stable Diffusion 常用模型下载与说明(保姆级) - 知乎

mikel阅读(824)

来源: Stable Diffusion 常用模型下载与说明(保姆级) – 知乎

之前咱们一系列的文章介绍了AI绘画以及AI绘画的两大扛把子:Stable Diffusion 和 Midjourney,

之后又介绍了Stable Diffusion 的操作界面和基础参数:

那么,接下来我们就要学习怎么使用Stable Diffusion 中最重要的各类模型了。

因为,相比于Midjourney,Stable Diffusion最大的优势就是开源。相比于Midjourney靠开发人员开发的少数模型,SD则每时每刻都有人在世界各地训练自己的模型并免费公开共享给全世界的使用者。(当然你可以通过训练自己的专有模型而专门用于某一用途,这也将成为你作为AI绘画者的最重要的核心竞争力之一)

因此,学会使用各类模型对于学习使用Stable Diffusion非常重要。

常用模型下载网址推荐

目前,模型数量最多的两个网站civitai.com/huggingface.co/。civitai又称c站,有非常多精彩纷呈的模型,有了这些模型,我们分分钟就可以变成绘画大师,用AI画出各种我们想要的效果。

C站长这样:

你会看到很多模型的预览图被屏蔽了,需要你认证为成人才能浏览。至于为什么要成人才能浏览,想必大家也是懂的都懂。

也正是如此,网站在国内是被屏蔽的。登录需要科学上网。

Huggingface则相对朴实无华一些,对模型的审核也会更加严格一些。但是好处在于不需要科学上网,而且网速很快

Huggingface界面如上。

它是一个综合性的网站,如果我们需要下载模型的话,选择Models。

进入之后,选择Text-to-Image,出来的就都是SD可以用的模型了。

除了C站和huggingface,其他的模型网站还有:

cyberes.github.io/stabl

(SD的基础模型,不用科学上网,但是这些模型都一般般,意义不大)

rentry.co/sdmodels

(模型很多,但是界面没有C站友好,需要科学上网)

炼丹阁 (www.liandange.com)

(国内的网站,很多都是搬运的C站的模型,合规性未知,通过百度网盘下载)

LiblibAI(www.liblibai.com)

LiblibAI,号称是国内最大的原创AI模型分享网站,但其实很多都是搬运的C站的模型,不过确实也有不少人气原创模型发布者入驻了该网站。

不同模型的说明

如果你去自己下载模型,就会发现有各种不同类型的模型。

具体模型类型有checkpoint、Textual lnversion、Hypernetwork、Aesthetic Gradient、LoRA、LyCORIS、Controlnet、Poses、wildcards等等,看得人眼花缭乱。这些都是什么意思呢?

Checkpoint/大模型/底模型/主模型

Checkpoint模型是SD能够绘图的基础模型,因此被称为大模型、底模型或者主模型,WebUI上就叫它Stable Diffusion模型。安装完SD软件后,必须搭配主模型才能使用。不同的主模型,其画风和擅长的领域会有侧重。

checkpoint模型包含生成图像所需的一切,不需要额外的文件。但是它们体积很大,通常为2G-7G。

常见文件模式:尾缀ckpt、safetensors(如果都有提供的话建议下载safetensors,下同)

存放路径: \sd-webui-aki-v4\models\Stable-diffusion

模型的切换界面:

目前比较流行和常见的checkpoint模型有Anything系列(v3、v4.5、v5.0)、AbyssOrangeMix3、ChilloutMix、Deliberate、国风系列等等。这些checkpoint模型是从Stable Diffusion基本模型训练而来的,相当于基于原生安卓系统进行的二次开发。目前,大多数模型都是从 v1.4 或 v1.5 训练的。它们使用其他数据进行训练,以生成特定风格或对象的图像。这个我们后面还会专门开一个专题进行讲解。

不同模型在同一参数下的表现有时候可以用天差地别来形容,下面是个例子:

LoRA

当下最火的微调模型,可以将某一类型的人物或者事物的风格固定下来。它们通常为10-200 MB。必须与checkpoint模型一起使用。

现在比较火的Korean Doll Likeness、Taiwan Doll Likenes、Cute Girl mix都是真人美女LoRA模型,效果很惊艳。还有一些特定风格的LoRA也非常受欢迎,最著名的有墨心等。这个我们后面也会再开一个专题讲解。

常见文件模式:尾缀ckpt、safetensors、pt

存放路径: \sd-webui-aki-v4\models\Lora

有多个方式可以使用

方法1是在生成界面调取选用。这个的好处是可以自己设置预览图,从而有直观的感受。

而且部分LORA只支持这种方式使用(不过AI绘画日新月异,说不定哪天规则又变了~)

方法2是以插件形式使用。好处是可以很方便的灵活调用多个LORA,并对他们按着不同比例进行混合。

在启动器界面选择模型管理,点击LoRA模型(插件),点击添加模型,选择你要添加的LoRA模型,重启启动器。然后在WebUI界面选择相应的插件和权重比例即可。

VAE美化模型/变分自编码器

VAE,全名Variational autoenconder,中文叫变分自编码器。作用是:滤镜+微调。

有的大模型是会自带VAE的,比如Chilloutmix。如果再加VAE则可能画面效果不会更好,甚至适得其反。

顺便说一句,系统自带的VAE是animevae,效果一般,建议可以使用kl-f8-anime2或者vae-ft-mse-840000-ema-pruned。anime2适合画二次元,840000适合画写实人物。

常见文件模式: 尾缀ckpt、pt

存放路径: \sd-webui-aki-v4\ models\ VAE

模型的切换:

Embedding/Textual lnversion/文本反转模型和Hypernetworks

Embeddings 和 Hypernetworks 都属于微调模型,但目前Hypernetworks已经不太用了。

Embeddings/Textual lnversion中文翻译过来叫文本反转,通过仅使用的几张图像,就可以向模型教授新的概念。用于个性化图像生成。Embeddings是定义新关键字以生成新人物或图片风格的小文件。它们很小,通常为10-100 KB。必须将它们与checkpoint模型一起使用。

Embeddings 由于训练简单,文件小,因此一度很受大家欢迎。而且Embeddings 使用方法很简单,在安装之后,只要在提示词中提到它就相当于调用了,很方便。但由于Embeddings使用的训练集较小,因此出来的图片常常只是神似,做不到”形似“,所以目前很多人还是喜欢使用LORA模型。而且Embeddings 是一级目录,每次打开webui时都要加载一遍,太多了会影响webui的“开机速度”(但是不影响运行速度)。

不过有一些Embeddings 还是值得安装,比如EasyNegative这个Embeddings,里面包含了大量的负面词,可以减少你每次打一堆负面词的痛苦。

Embedding

常见文件模式: 尾缀pt

存放路径: \sd-webui-aki-v4\ embeddings

模型的切换通过文件名称来触发

Hypernetworks

常见文件模式: 尾缀pt

存放路径: \sd-webui-aki-v4\ models\ Hypernetworks

模型的切换通过文件名称来触发

DreamBooth模型

DreamBooth,可用于训练预调模型用的。是使用指定主题的图像进行演算,训练后可以让模型产生更精细和个性化的输出图像。

常见模式:尾缀ckpt、safetensors

常见大小:2G-7G

最新版本的DreamBooth是可以把那个Lora算法然后融合进来的

可以训练角色、画风、物件等,使用方法和主模型相同

训练路径:

LyCORIS模型

此类模型也可以归为Lora模型,也是属于微调模型的一种。一般文件大小在340M左右。不同的是训练方式与常见的lora不同,但效果似乎会更好不少。

其中本人较喜欢的“Miniature world style 微缩世界风格”就属于这类模型。

但要使用此类微调模型,需要先安装一个locon插件,直接将压缩包解压后放到StableDiffusion目录的extensions目录里。

插件地址

github.com/KohakuBluele

下载后直接解压缩在extensions中。

使用时注意,除了要将lora调入,还要在正向tag开头添加触发词

例如:这个微缩世界风格的lyCORIS的调用,正向描述语如下

mini\(ttp\), (8k, RAW photo, best quality, masterpiece:1.2), island, cinematic lighting,UHD,miniature, landscape, Crystal ball,on rock, <lora:miniatureWorldStyle_v10:0.8>

小技巧

如果你下载了一个模型,却不知道怎么安装,打开这个网站

spell.novelai.dev/

把你下载的模型拖进去,立马就会帮你解析,告诉你应该放在那里。

不过,由于AI绘画日新月异,有的模型,网站可能还来不及收集和解析,会无法解读。

最后,如果你也对AI绘画感兴趣的话,欢迎关注本专栏,关注AI时代社,也欢迎评论转发点赞分享。

Stable Diffusion 操作界面及基础参数介绍 - 知乎

mikel阅读(410)

来源: Stable Diffusion 操作界面及基础参数介绍 – 知乎

操作界面介绍

典型的Stable Diffusion操作界面如下图:

可以看到里面的参数非常多。

但新手小白其实只需要知道两个参数就行。

基础参数介绍

提示词(Prompt)和反向提示词(Negative Prompt)

提示词内输入的东西就是你想要画的东西,反向提示词内输入的就是你不想要画的东西

提示框内只能输入英文,所有符号都要使用英文半角,词语之间使用半角逗号隔开

反向提示词

这里重点提一下反向提示词,与提示词相反,反向提示词输入的是你不希望SD产生的。这是SD的一个非常强大但未被充分利用的功能。有时候你正面提示词写一堆,出来的效果也不理想,但是加上一个反向提示词就能获得理想的结果。

一般负面提示:低分辨率、错误、裁剪、最差质量、低质量、jpeg伪像、帧外、水印、签名

General: lowres, error, cropped, worst quality, low quality, jpeg artifacts, out of frame, watermark, signature

人物肖像的负面提示:变形、丑陋、残缺、毁容、文本、额外的四肢、面部切割、头部切割、额外的手指、额外的手臂、绘制不佳的脸、突变、比例不良、头部裁剪、四肢畸形、手突变、融合手指、长脖子

Negative prompts for people portraits: deformed, ugly, mutilated, disfigured, text, extra limbs, face cut, head cut, extra fingers, extra arms, poorly drawn face, mutation, bad proportions, cropped head, malformed limbs, mutated hands, fused fingers, long neck

逼真图像的负面提示:逼真:插图、绘画、素描、艺术、素描’

Negative prompts for photorealistic images: Photorealistic: illustration, painting, drawing, art, sketch’

基本上,掌握了提示词和反向提示词的书写,AI绘画就算是入门了(至少可以画出东西来了!)但如果想要更加一步的,请继续往下看(哪怕你是老手,下面的文章也值得你一看哦~)。

采样迭代步数(Steps)

Stable-Diffusion通过从充满噪点的画布开始创建图像,然后逐渐去噪以达到最终输出。Steps就是控制这些去噪步骤的数量。通常,越高越好,但一般情况下,我们使用的默认值是20个步骤,这其实已经足以生成任何类型的图像。

以下是有关在不同情况下使用steps的一般指南:

a 如果你正在测试新提示并希望获得快速结果来调整输入,请使用10-15个steps。

b 找到所需的提示后,将步骤增加到20-30,很多人的习惯是28

c 如果你正在创建带有毛皮或任何具有详细纹理的主题的面部或动物,并且觉得生成的图像缺少其中一些细节,请尝试将其提高到40或者更高

特别提示:

有些人习惯于一上来就创建具有100或150步的图像,这对于LMS等采样器很有用,但除非你有很强的显卡,否则很多时候都是浪费时间。先用小步骤去测试,找到合适的提示词后再提升步数才是正确的方法。

而且,使用改进的快速采样器(如 DDIM 和 DPM++系列)一般用100以内的步数就完全OK了,通过对这些采样器使用大量步骤,很可能只会浪费时间和GPU算力,而不会提高图像质量

采样方法(Sampler)

正如我们之前提到的,SD通过对起始噪声画布进行降噪来工作。这就是扩散采样器发挥作用的地方。简单来说,这些采样器是算法,它们在每个步骤后获取生成的图像并将其与文本提示请求的内容进行比较,然后对噪声进行一些更改,直到它逐渐达到与文本描述匹配的图像。

用户最常用的三个采样器分别是Euler a,DDIM和DPM++系列。你可以尝试这三个,看看哪个更适合你的提示。

由于采样器的规则过于学术化,仔细讲解也未必能说出一个123来,这里以相同的参数,不同的采样器去测试了同一张图,分别出来的效果如上,仅供参考。

总体而言,欧拉采样器(Euler a)具有更平滑的颜色和较少定义的边缘,使其更具“梦幻”外观,因此如果这是你在生成的图像中喜欢的效果,请使用Euler a。

DPM2和DPM++系列更加写实。

LMS、DPM fast 虽然出图快,但有可能人是不完整的。

生成批次和生成数量

生成批次是显卡一共生成几批图片。

每批数量是显卡每批生成几张图片。

也就是说你每点击一次生成按钮,生成的图片数量=批次*数量

需要注意的是每批数量是显卡一次所生成的图片数量,速度要比调高批次快一点,但是调的太高可能会导致显存不足导致生成失败,而生成批次不会导致显存不足,只要时间足够会一直生成直到全部输出完毕。

输出分辨率(宽度和高度)

图片分辨率非常重要,直接决定了你的图片内容的构成和细节的质量。

a.输出大小

输出大小决定了画面内容的信息量,很多细节例如全身构图中的脸部,饰品,复杂纹样等只有在大图上才能有足够的空间表现,如果图片过小,像是脸部则只会缩成一团,是没有办法充分表现的。

但是图片越大ai就越倾向于往里面塞入更多的东西,绝大多数模型都是在512*512分辨率下训练的,少数在768*768下训练,所以当输出尺寸比较大比如说1024*1024的时候,ai就会尝试在图中塞入两到三张图片的内容量,于是会出现各种肢体拼接,不受词条控制的多人,多角度等情况,增加词条可以部分缓解,但是更关键的还是控制好画幅,先画中小图,再放大为大图

大致的输出大小和内容关系参考:

1.约30w像素,如512*512,大头照和半身为主

2.约60w像素,如768*768,单人全身为主,站立或躺坐都有

3.越100w像素,如1024*1024,单人和两三人全身,站立为主

4.更高像素,群像,或者直接画面崩坏

b.宽高比例

宽高比例会直接决定画面内容,同样是1girl的例子:

1.方图512*512,会倾向于出脸和半身像

2.高图512*768,会倾向于出站着和坐着的全身像

3.宽图768*512,会倾向于出斜构图的半躺像

所以要根据想要的内容来调整输出比例。

提示词相关性(CFG Scale)

CFG这个参数可以看作是“创造力与提示”量表。较低的数字使AI有更多的自由发挥创造力,而较高的数字迫使它更多地坚持提示词的内容。

默认的CFG是7,这在创造力和生成你想要的东西之间是最佳平衡。通常不建议低于5,因为图像可能开始看起来更像AI的幻觉,而高于16可能会开始产生带有丑陋伪影的图像。

那么何时使用不同的CFG刻度值呢?CFG量表可以分为不同的范围,每个范围适用于不同的提示类型和目标。

1.CFG 2–6:创意,但可能过于失真,没有按照提示进行操作。对于简短的提示可能很有趣且有用;

2.CFG 7–10:建议用于大多数提示。创造力和引导式生成之间的良好平衡;

3.CFG 10–15:当你确定提示很详细并且非常清楚你希望图像的外观时(有些纯风景和建筑类的图片可能需要的CFG会比较高,大家可以参考相应的模型说明);

4.CFG 16–20:除非提示非常详细,否则通常不建议使用。可能会影响一致性和质量;

5.CFG >20:几乎从不使用

随机种子(seed)

随机种子是一个决定我们之前讨论的初始随机噪声的数字,由于随机噪声决定了最终图像,这就是为什么每次在StableDiffusion系统上运行完全相同的提示时都会得到不同的图像,以及为什么如果多次使用相同的提示运行相同的seed的时候,你会得到相同的生成图像。

由于相同的种子和提示组合每次都提供相同的图像,因此我们可以通过多种方式利用此属性:

a.控制角色的特定特征:在这个例子中,我们改变了情绪,但这也适用于其他物理特征,如头发颜色或肤色,但变化越小,它就越有可能被改变。

b.测试特定单词的效果:如果你想知道提示中的特定单词发生了哪些变化,则可以使用相同的种子和修改后的提示进行测试,最好通过每次更改单个单词或短语来测试提示。

c.更改样式:如果你喜欢图像的构图,但想知道它以不同的样式显示效果。这可用于肖像、风景或你创建的任何场景。

IMG 2 IMG参数

Img2img 功能的工作方式与 txt2img 完全相同,唯一的区别是你提供了一个用作起点的图像,而不是种子编号产生的噪声。

噪点被添加到你用作 img2img 的初始化图像的图像中,然后根据提示继续扩散过程。添加的噪声量取决于“重绘幅度(Denoising)”这个参数,该参数的范围从0到1,其中0根本不添加噪声,你将获得添加的确切图像,1完全用噪声替换图像,几乎就像你使用普通的txt2img而不是img2img一样。

那么如何决定使用什么力量呢?这是一个带有示例的简单指南:

a.要创建图像的变体,建议使用的强度为 0.5-0.75,并且具有相同的提示。当你喜欢创建的图像的构图但某些细节看起来不够好,或者你想创建与你在其他软件(如 Blender或 photoshop)中创建的图像相似的图像时,这可能很有用(在这种情况下,提示将是对原有图像的描述)。

b.要更改图像样式,同时使其与原始图像相似,你可以多次使用较低强度的img2img,与具有较高强度的单个img2img相比,可以获得更好的图像保真度。

在这个例子中,我们使用0.25的强度4次,所以每次我们生成图像时,我们都会将生成的图像重新插入img2img中,并以相同的提示和强度重新运行它,直到我们得到我们需要的样式。如果在img2img中使用相同的图像,强度更高,你将很快失去图像相似性。

基本上到这里的话,基础的SD参数我们都算是学完了。接下来要学的东西会更为进阶,欢迎大家持续关注。

C#使用企业微信群机器人推送生产数据 - Hello-MOMO - 博客园

mikel阅读(444)

来源: C#使用企业微信群机器人推送生产数据 – Hello-MOMO – 博客园

在日常的工作生产中,经常会有将将生产数据或者一些信息主动推送给相关的管理人员,我们公司在开发WMS系统时,为了仓库的储存安全,需要在危废品库存达到一定的储量时,自动通知仓管员去处理危废品,所以就需要程序自动的通过企业微信告知仓管员,这个时候就需要用到企业微信的机器人了。

现在我所知道的企业微信机器人分为两种,一种是机器人,一种是群机器人,机器人开发比较复杂,但是可以像一个企业微信账号一样可以给企业微信中的任意一个人发送信息,第二种群机器人比较简单,只能在群里推送消息。下面要讲的就是群机器人的开发。

第一步,先创建一个企业微信群(好像需要三个人才能达到建群的最小人数),添加一个群机器人,如图:

 

 

然后点击机器人的头像,记住Webhook(这个很重要,记住一定要保密,不能发到网上,不然其他人可以通过这个利用机器人给企业微信群发任何信息)

后面上代码,我写了一个方法如下:

public void WeChatRobot(string message)
{
string cttStr = “”;
cttStr += “# <font color=\\\”warning\\\”>”+ message + “</font>\n”;
string param = “{\”msgtype\”:\”markdown\”,\”markdown\”:{\”content\”:\”” + cttStr + “\”}}”;
string webhookUrl = “此处替换为企业微信群机器人的Webhook”;

using (var client = new RestClient(webhookUrl))
{
var Req = new RestRequest(webhookUrl, Method.Post);
Req.AddHeader(“Content-Type”, “application/json”);
Req.AddJsonBody(param);
var Rsp = client.ExecuteAsync(Req).Result;

}
}

代码这个地方记得替换为企业微信群机器人的Webhook

如果有报错记得引用一下RestSharp库。

将需要推送的信息赋值给该方法的message,就可以使用企业微信机器人将信息推送到群里了。

补充:下面是一些推送文字的格式,现在似乎只支持三种颜色的字体。

 

 

思考(不用看):

在生产过程中经常有订单需要返回上一步,比如有些订单在该工序已经点击生产完工了,但是由于需要补充一些生产信息,需要将订单重新返回到正在生产的状态,这在正常的生产流程中肯定是不被允许的,但是实体制造业的IT部门都是服务于生产的,无论无何都不能耽误生产,最终还是需要IT去数据库改数据,有时常常下班了还要远程电脑改订单的固定数据,就很烦,明明知道有这个需求,又不能放权写个功能让生产自己回退订单,所以就思考写一个企业微信群机器人的推送功能:

生产需要将订单退回上一步时,将订单号和回退原因填写在MES系统上,这是系统后台会生成一个随机的验证码(后台生成,MES操作员不知道),将验证码和订单号、订单回退信息、操作人和时间等信息保存在数据库,并通过企业微信群机器人将验证码和订单回退信息推送到IT群,IT评估后若允许订单回退就将验证码转发给操作员,由操作员在MES上填写验证码,确认后填写验证码与数据库保存的验证码相同时,执行订单回退操作。这样遇到订单回退的突发情况,即使电脑不在身边,也可以通过手机企业微信对订单回退进行管控,运维人员也可以少掉头发。

(使用文章请标明来源——Hello-MOMO