作者: chenyl

  • Docker 大势已去,Podman 即将崛起

    快乐分享,Java干货及时送达👇

    文章来源:https://blog.csdn.net/qq_48289488/article/details/121905018


      Podman


      什么是Podman?

      Podman 是一个开源的容器运行时项目,可在大多数 Linux 平台上使用。Podman 提供与 Docker 非常相似的功能。正如前面提到的那样,它不需要在你的系统上运行任何守护进程,并且它也可以在没有 root 权限的情况下运行。

      Podman 可以管理和运行任何符合 OCI(Open Container Initiative)规范的容器和容器镜像。Podman 提供了一个与 Docker 兼容的命令行前端来管理 Docker 镜像。

      Podman 官网地址:https://podman.io/

      Podman和Docker的主要区别是什么?

      • dockers在实现CRI的时候,它需要一个守护进程,其次需要以root运行,因此这也带来了安全隐患。
      • podman不需要守护程序,也不需要root用户运行,从逻辑架构上,比docker更加合理。
      • 在docker的运行体系中,需要多个daemon才能调用到OCI的实现RunC。
      • 在容器管理的链路中,Docker Engine的实现就是dockerd
      • daemon,它在linux中需要以root运行,dockerd调用containerd,containerd调用containerd-shim,然后才能调用runC。顾名思义shim起的作用也就是“垫片”,避免父进程退出影响容器的运训
      • podman直接调用OCI,runtime(runC),通过common作为容器进程的管理工具,但不需要dockerd这种以root身份运行的守护进程。
      • 在podman体系中,有个称之为common的守护进程,其运行路径通常是/usr/libexec/podman/conmon,它是各个容器进程的父进程,每个容器各有一个,common的父则通常是1号进程。podman中的common其实相当于docker体系中的containerd-shim。

      图中所体现的事情是,podman不需要守护进程,而dorker需要守护进程。在这个图的示意中,dorcker的containerd-shim与podman的common被归在Container一层。

      Podman的使用与docker有什么区别?

      podman的定位也是与docker兼容,因此在使用上面尽量靠近docker。在使用方面,可以分成两个方面来说,一是系统构建者的角度,二是使用者的角度。

      在系统构建者方面,用podman的默认软件,与docker的区别不大,只是在进程模型、进程关系方面有所区别。如果习惯了docker几个关联进程的调试方法,在podman中则需要适应。可以通过pstree命令查看进程的树状结构。总体来看,podman比docker要简单。由于podman比docker少了一层daemon,因此重启的机制也就不同了。

      在使用者方面,podman与docker的命令基本兼容,都包括容器运行时(run/start/kill/ps/inspect),本地镜像(images/rmi/build)、镜像仓库(login/pull/push)等几个方面。因此podman的命令行工具与docker类似,比如构建镜像、启停容器等。甚至可以通过alias

      docker=podman可以进行替换。因此,即便使用了podman,仍然可以使用docker.io作为镜像仓库,这也是兼容性最关键的部分。


      Podman常用命令


      容器

      podman run           创建并启动容器
      podman start         启动容器
      podman ps            查看容器
      podman stop          终止容器
      podman restart       重启容器
      podman attach        进入容器
      podman exec          进入容器
      podman export        导出容器
      podman import        导入容器快照
      podman rm            删除容器
      podman logs          查看日志

      镜像

      podman search                检索镜像
      podman pull                  获取镜像
      podman images                列出镜像
      podman image Is              列出镜像
      podman rmi                   删除镜像
      podman image rm              删除镜像
      podman save                  导出镜像
      podman load                  导入镜像
      podmanfile                   定制镜像(三个)
       podman build             构建镜像
          podman run               运行镜像
          podmanfile               常用指令(四个)
           COPY                 复制文件
              ADD                  高级复制
              CMD                  容器启动命令
              ENV                  环境变量
              EXPOSE               暴露端口

      部署 Podman

      //安装podman
      [root@localhost ~]# yum -y install podman

      Podman 加速器

      版本7配置加速器

      //仓库配置
      [root@localhost ~]# vim /etc/containers/registries.conf
      [registries.search]
      #registries = ["registry.access.redhat.com", "registry.redhat.io", "docker.io"]  #这个是查找,从这三个地方查找

      registries = ["docker.io"]  #如果只留一个,则只在一个源里查找
      [[docker.io]]
      location="j3m2itm3.mirror.aliyuncs.com"

      版本8配置加速器

      #unqualified-search-registries = ["registry.fedoraproject.org", "registry.access.redhat.com", "registry.centos.org", "docker.io"]   #直接注释掉
      unqualified-search-registries = ["docker.io"]  #添加一个docker.io
      [[registry]]
      prefix = "docker.io"
      location = "j3m2itm3.mirror.aliyuncs.com" (不用加https://  直接加地址)

      使用 Podman

      使用 Podman 非常的简单,Podman 的指令跟 Docker 大多数都是相同的。下面我们来看几个常用的例子:

      运行一个容器

      [root@localhost ~]# podman run -d --name httpd docker.io/library/httpd
      Trying to pull docker.io/library/httpd...
      Getting image source signatures
      Copying blob e5ae68f74026 done  
      Copying blob d3576f2b6317 done  
      Copying blob bc36ee1127ec done  
      Copying blob f1aa5f54b226 done  
      Copying blob aa379c0cedc2 done  
      Copying config ea28e1b82f done  
      Writing manifest to image destination
      Storing signatures
      0492e405b9ecb05e6e6be1fec0ac1a8b6ba3ff949df259b45146037b5f355035

      //查看镜像
      [root@localhost ~]# podman images
      REPOSITORY                  TAG      IMAGE ID       CREATED       SIZE
      docker.io/library/httpd     latest   ea28e1b82f31   11 days ago   148 MB

      列出运行的容器

      [root@localhost ~]# podman ps
      CONTAINER ID  IMAGE                             COMMAND           CREATED             STATUS                 PORTS  NAMES
      0492e405b9ec  docker.io/library/httpd:latest    httpd-foreground  About a minute ago  Up About a minute ago         httpd

      注意:如果在ps命令中添加-a,Podman 将显示所有容器。

      检查正在运行的容器

      您可以“检查”正在运行的容器的元数据和有关其自身的详细信息。我们甚至可以使用 inspect 子命令查看分配给容器的 IP 地址。由于容器以无根模式运行,因此未分配 IP 地址,并且该值将在检查的输出中列为“无”。

      [root@localhost ~]# podman inspect -l | grep IPAddress": 
                  "SecondaryIPAddresses": null, 
                  "IPAddress""10.88.0.5",

      [root@localhost ~]# curl 10.88.0.5

      It works!


      注意:-l 是最新容器的便利参数。您还可以使用容器的 ID 代替 -l。

      查看一个运行中容器的日志

      选项
       --latest  #最近的
       
      [root@localhost ~]# podman logs --latest
      AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.88.0.5. Set the 'ServerName' directive globally to suppress this message
      AH00558: httpd: Could not reliably determine the server'
      s fully qualified domain name, using 10.88.0.5. Set the 'ServerName' directive globally to suppress this message
      [Mon Dec 13 15:17:53.690844 2021] [mpm_event:notice] [pid 1:tid 140665160166720] AH00489: Apache/2.4.51 (Unix) configured -- resuming normal operations
      [Mon Dec 13 15:17:53.690946 2021] [core:notice] [pid 1:tid 140665160166720] AH00094: Command line: 'httpd -D FOREGROUND'
      10.88.0.1 - - [13/Dec/2021:15:19:48 +0000] "GET / HTTP/1.1" 200 45
      10.88.0.1 - - [13/Dec/2021:15:20:47 +0000] "GET / HTTP/1.1" 200 45

      查看一个运行容器中的进程资源使用情况,可以使用top观察容器中的 nginx pid

      语法:
       podman top   
       
      [root@localhost ~]# podman top httpd
      USER       PID   PPID   %CPU    ELAPSED            TTY   TIME   COMMAND
      root       1     0      0.000   15m38.599711321s   ?     0s     httpd -DFOREGROUND 
      www-data   7     1      0.000   15m38.599783256s   ?     0s     httpd -DFOREGROUND 
      www-data   8     1      0.000   15m38.599845342s   ?     0s     httpd -DFOREGROUND 
      www-data   9     1      0.000   15m38.599880444s   ?     0s     httpd -DFOREGROUND 

      停止一个运行中的容器

      [root@localhost ~]# podman stop --latest
      2f3edf712621d3a41e03fa8c7f6a5cdba56fbbad43a7a59ede26cc88f31006c4
      [root@localhost ~]# podman ps
      CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES

      删除一个容器

      [root@localhost ~]# podman rm --latest
      2f3edf712621d3a41e03fa8c7f6a5cdba56fbbad43a7a59ede26cc88f31006c4
      [root@localhost ~]# podman ps -a
      CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES

      以上这些特性基本上都和 Docker 一样,Podman 除了兼容这些特性外,还支持了一些新的特性。

      上传镜像

      例如,如果我们想在 docker.io 上分享我们新建的 Nginx 容器镜像,这很容易。首先登录码头:

      [root@localhost nginx]# tree 
      .
      ├── Dockerfile
      └── files
          └── nginx-1.20.1.tar.gz

      [root@localhost nginx]# cat Dockerfile 
      FROM docker.io/library/centos

      ENV PATH /usr/local/nginx/sbin:$PATH
      ADD files/nginx-1.20.1.tar.gz /usr/src
      RUN useradd -r -M -s /sbin/nologin nginx && 
          yum -y install pcre-devel openssl openssl-devel gd-devel gcc gcc-c++ make && 
          mkdir -p /var/log/nginx && 
          cd /usr/src/nginx-1.20.1 && 
          ./configure 
          --prefix=/usr/local/nginx 
          --user=nginx 
          --group=nginx 
          --with-debug 
          --with-http_ssl_module 
          --with-http_realip_module 
          --with-http_image_filter_module 
          --with-http_gunzip_module 
          --with-http_gzip_static_module 
          --with-http_stub_status_module 
          --http-log-path=/var/log/nginx/access.log 
          --error-log-path=/var/log/nginx/error.log && 
        make && make install

      CMD ["nginx","-g","daemon off"]
      [root@localhost nginx]# podman build -t nginx .

      // 修改镜像名
       [root@localhost ~]# podman tag docker.io/library/nginx:latest docker.io/1314444/test:latest

      // 登录并上传镜像
      [root@localhost ~]# podman login docker.io // 需要告诉其要登录到docker仓库
      [root@localhost ~]# podman login docker.io
      Username: 1314444  #账户
      Password: ********  #密码
      Login Succeeded!

      [root@localhost nginx]# podman push docker.io/1314444/test:latest  //上传镜像
      Getting image source signatures
      Copying blob 38c40d6c2c85 done
      Copying blob fee76a531659 done
      Copying blob c2adabaecedb done
      Copying config 7f3589c0b8 done
      Writing manifest to image destination
      Copying config 7f3589c0b8 done
      Writing manifest to image destination
      Storing signatures


      //请注意,我们将四层推送到我们的注册表,现在可供其他人共享。快速浏览一下:
      [root@localhost ~]# podman inspect 1314444/test:nginx
      //输出:
      [
          {
              "Id""7f3589c0b8849a9e1ff52ceb0fcea2390e2731db9d1a7358c2f5fad216a48263",
              "Digest""sha256:7822b5ba4c2eaabdd0ff3812277cfafa8a25527d1e234be028ed381a43ad5498",
              "RepoTags": [
                  "docker.io/1314444/test:nginx",
        ......

      总而言之,Podman 使查找、运行、构建和共享容器变得容易。

      配置别名

      如果习惯了使用 Docker 命令,可以直接给 Podman 配置一个别名来实现无缝转移。你只需要在 .bashrc 下加入以下行内容即可:

      [root@localhost ~]# echo "alias docker=podman" >> .bashrc
      source .bashrc
      [root@localhost ~]# alias
      alias cp='cp -i'
      alias docker='podman'
      .......

      用户操作

      在允许没有root特权的用户运行Podman之前,管理员必须安装或构建Podman并完成以下配置

      cgroup V2Linux内核功能允许用户限制普通用户容器可以使用的资源,如果使用cgroupV2启用了运行Podman的Linux发行版,则可能需要更改默认的OCI运行时。某些较旧的版本runc不适用于cgroupV2,必须切换到备用OCI运行时crun。

      [root@localhost ~]# yum -y install crun     //centos8系统自带

      [root@localhost ~]# vi /usr/share/containers/containers.conf 
          446 # Default OCI runtime
          447 
          448 runtime = "crun"      //取消注释并将runc改为crun

      [root@localhost ~]# podman run -d --name web -p 80:80 docker.io/library/nginx
      c8664d2e43c872e1e5219f82d41f63048ed3a5ed4fb6259c225a14d6c243677f

      [root@localhost ~]# podman inspect web | grep crun
              "OCIRuntime""crun",
                  "crun",

      安装slirp4netns和fuse-overlayfs

      在普通用户环境中使用Podman时,建议使用fuse-overlayfs而不是VFS文件系统,至少需要版本0.7.6。现在新版本默认就是了。

      [root@localhost ~]# yum -y install slirp4netns

      [root@localhost ~]# yum -y install fuse-overlayfs
      [root@localhost ~]# vi /etc/containers/storage.conf
      77 mount_program = "/usr/bin/fuse-overlayfs"     //取消注释

      / etc / subuid和/ etc / subgid配置

      Podman要求运行它的用户在/ etc / subuid和/ etc / subgid文件中列出一系列UID,shadow-utils或newuid包提供这些文件

      [root@localhost ~]# yum -y install shadow-utils

      可以在/ etc / subuid和/ etc / subgid查看,每个用户的值必须唯一且没有任何重叠。

      [root@localhost ~]# useradd zz
      [root@localhost ~]# cat /etc/subuid
      zz:100000:65536
      [root@localhost ~]# cat /etc/subgid
      zz:100000:65536

      // 启动非特权ping 
      [root@localhost ~]# sysctl -w "net.ipv4.ping_group_range=0 200000" //大于100000这个就表示tom可以操作podman
      net.ipv4.ping_group_range = 0 200000

      这个文件的格式是 USERNAME:UID:RANGE中/etc/passwd或输出中列出的用户名getpwent。

      • 为用户分配的初始 UID。
      • 为用户分配的 UID 范围的大小。

      该usermod程序可用于为用户分配 UID 和 GID,而不是直接更新文件。

      [root@localhost ~]# usermod --add-subuids 200000-201000 --add-subgids 200000-201000 hh
      grep hh /etc/subuid /etc/subgid
      /etc/subuid:hh:200000:1001
      /etc/subgid:hh:200000:1001

      用户配置文件

      三个主要的配置文件是container.conf 、storage.conf 和r egistries.conf 。用户可以根据需要修改这些文件。

      container.conf

      // 用户配置文件
      [root@localhost ~]# cat /usr/share/containers/containers.conf
      [root@localhost ~]# cat /etc/containers/containers.conf
      [root@localhost ~]# cat ~/.config/containers/containers.conf  //优先级最高

      如果它们以该顺序存在。每个文件都可以覆盖特定字段的前一个文件。

      配置storage.conf文件

      1./etc/containers/storage.conf
      2.$HOME/.config/containers/storage.conf

      在普通用户中**/etc/containers/storage.conf** 的一些字段将被忽略

      [root@localhost ~]#  vi /etc/containers/storage.conf
      [storage]


      > 推荐下自己做的 Spring Cloud 的实战项目:
      >


      # Default Storage Driver, Must be set for proper operation.
      driver = "overlay"  #此处改为overlay
      .......
      mount_program = "/usr/bin/fuse-overlayfs"  #取消注释

      [root@localhost ~]# sysctl user.max_user_namespaces=15000  #如果版本为8以下,则需要做以下操作:

      在普通用户中这些字段默认

      graphroot="$HOME/.local/share/containers/storage"
      runroot="$XDG_RUNTIME_DIR/containers"

      registries.conf

      配置按此顺序读入,这些文件不是默认创建的,可以从/usr/share/containers 或复制文件/etc/containers并进行修改。

      1./etc/containers/registries.conf
      2./etc/containers/registries.d/*
      3.HOME/.config/containers/registries.conf

      授权文件

      此文件里面写了docker账号的密码,以加密方式显示

      [root@localhost ~]# podman login
      Username: 1314444
      Password: 
      Login Succeeded!
      [root@localhost ~]# cat /run/user/0/containers/auth.json 
      {
              "auths": {
                      "registry.fedoraproject.org": {
                              "auth""MTMxNDQ0NDpIMjAxNy0xOA=="
                      }
              }
      }

      普通用户是无法看见root用户的镜像的

      //root用户
      [root@localhost ~]# podman images
      REPOSITORY                  TAG      IMAGE ID       CREATED       SIZE
      docker.io/library/httpd     latest   ea28e1b82f31   11 days ago   146 MB

      //普通用户
      [root@localhost ~]# su - zz
      [zz@localhost ~]$ podman images
      REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

      • 容器与root用户一起运行,则root容器中的用户实际上就是主机上的用户。
      • UID GID是在/etc/subuid和/etc/subgid等中用户映射中指定的第一个UID GID。
      • 如果普通用户的身份从主机目录挂载到容器中,并在该目录中以根用户身份创建文件,则会看到它实际上是你的用户在主机上拥有的。

      使用卷

      [root@localhost ~]# su - zz
      [zz@localhost ~]$ pwd
      /home/zz
      [zz@localhost ~]$ mkdir /home/zz/data

      [zz@localhost ~]$ podman run -it -v "$(pwd)"/data:/data docker.io/library/busybox /bin/sh
      Trying to pull docker.io/library/busybox:latest...
      Getting image source signatures
      Copying blob 3cb635b06aa2 done  
      Copying config ffe9d497c3 done  
      Writing manifest to image destination
      Storing signatures
      # ls
      bin   data  dev   etc   home  proc  root  run   sys   tmp   usr   var
      # cd data/
      /data # ls
      /data # touch 123
      /data # ls -l
      total 0
      -rw-r--r--    1 root     root             0 Dec 13 00:17 123

      在主机上查看

      [zz@localhost ~]$ ll data/
      总用量 0
      -rw-r--r-- 1 zz zz 0 12月 13 00:17 123

      //写入文件
      [zz@localhost ~]$ echo "hell world" >> 123
      [zz@localhost ~]$ cat 123
      hell world

      容器里查看

      /data # cat 123
      hell world

      //我们可以发现在容器里面的文件的属主和属组都属于root,那么如何才能让其属于tom用户呢?下面告诉你答案
      /data # ls -l
      total 4
      -rw-rw-r--    1 root     root            12 Dec 13 00:20 123

      //只要在运行容器的时候加上一个--userns=keep-id即可。
      [zz@localhost ~]$ podman run -it --name test -v "$(pwd)"/data:/data --userns=keep-id docker.io/library/busybox /bin/sh
      ~ $ cd data/
      /data $ ls -l
      total 4
      -rw-r--r--    1 zz       zz              11 Dec 13 00:21 123

      使用普通用户映射容器端口时会报“ permission denied”的错误

      [zz@localhost ~]$ podman run  -d -p 80:80 httpd
      Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied

      普通用户可以映射>= 1024的端口

      [zz@localhost ~]$ podman run  -d -p 1024:80 httpd
      58613a6bdc70d4d4f9f624583f795a62a610596d166f0873bdff8fb26aa15092
      [zz@localhost ~]$ ss -anlt
      State       Recv-Q      Send-Q           Local Address:Port           Peer Address:Port      Process      
      LISTEN      0           128                    0.0.0.0:22                  0.0.0.0:*                      
      LISTEN      0           128                          *:1024                      *:*                      
      LISTEN      0           128                       [::]:22                     [::]:* 

      配置echo ‘net.ipv4.ip_unprivileged_port_start=80’ >> /etc/sysctl.conf后可以映射大于等于80的端口

      [root@localhost ~]# echo  'net.ipv4.ip_unprivileged_port_start=80'  >> /etc/sysctl.conf
      [root@localhost ~]# sysctl -p
      net.ipv4.ip_unprivileged_port_start = 80

      [zz@localhost ~]$ podman run -d -p 80:80 httpd
      1215455a0c300d78e7bf6afaefc9873f818c6b0f26affeee4e2bc17954e72d8e
      [zz@localhost ~]$ ss -anlt
      State       Recv-Q      Send-Q           Local Address:Port           Peer Address:Port      Process      
      LISTEN      0           128                    0.0.0.0:22                  0.0.0.0:*                      
      LISTEN      0           128                          *:1024                      *:*                      
      LISTEN      0           128                          *:80                        *:*                      
      LISTEN      0           128                       [::]:22                     [::]:*  

    • 支付系统就该这么设计,稳的一批!!

      快乐分享,Java干货及时送达👇

      支付永远是一个公司的核心领域,因为这是一个有交易属性公司的命脉。那么,支付系统到底长什么样,又是怎么运行交互的呢?抛开带有支付牌照的金融公司的支付架构,下述链路和系统组成基本上符合绝大多数支付场景。其实整体可以看成是交易核心+支付核心 两个大系统。交易系统关联了业务场景和底层支付,而支付系统完成了调用支付工具到对账清算等一系列相关操作。下面我们就来一起看下各个系统的核心组成和交互。

      1. 支付系统总览

      核心系统交互

      业务图谱

      2. 核心系统解析

      交易核心

      交易核心把公司的业务系统和底层支付关联起来,让业务系统专注于业务,不比关心底层支付。

      交易核心

      基础交易类型抽象

      多表聚合 & 订单关联

      支付核心

      支付核心主要负责将多种支付类型进行抽象,变成充值提现退款转账四种支付形态。同时,还要负责集成多种支付工具,对支付指令进行编排等等。

      支付核心总览

      支付行为编排

      其目的,是实现插件式开发支付规则可配置的 灵活开发方式。

      异常处理

      异常处理包括了 重复支付、部分支付、金额不一致、其他异常等异常场景。

      渠道网关

      资金核算

      3. 服务治理

      平台统一上下文

      通过确定系统边界、业务建模拆分之后,整个支付平台被拆分几十个服务,而如何保障在服务间流转业务信息不被丢失,是我们需要考虑的问题。平台统一上下文的要素信息(唯一业务标识码),在整个支付平台链路中全程传递,被用来解决这个问题。

      数据一致性治理

      大型的支付公司,内部都有非常严格和完备的数据一致性方案,比如采用业务侵入性非常大的分布式事务等,以牺牲开发效率来提升数据的稳定,是非常有必要的。而业务公司,如果不采用分布式事务又有哪些应对策略呢?

      CAS 校验

      幂等 & 异常补偿

      对账

      准实时对账

      DB 拆分

      异步化

      支付是整个交易链路的核心环节,那么,怎么兼顾支付系统的稳定性和执行效率呢?是异步化。

      消息异步化

      外部支付调用异步化

      在外部支付中,经常需要服务方与第三方支付交互,获取预支付凭证,如上图所示。

      这种同步调用的情况下,由于需要跨外部网络,响应的 RT 会非常长,可能会出现跨秒的情况。由于是同步调用,会阻塞整个支付链路。一旦 RT 很长且 QPS 比较大的情况下,服务会整体 hold 住,甚至会出现拒绝服务的情况。

      因此,可以拆分获取凭证的操作,通过独立网关渠道前置服务,将获取的方式异步化,从前置网关获取内部凭证,然后由前置网关去异步调用第三方。

      异步并行化

      资金核算异步化

      热点账户账务单独处理

      记账事务切分

      4. 生产实践

      性能压测

      构建压测模型,模拟现实真实场景;压测数据进影子库,正常业务无侵入;单机性能和集权链路都不能忽视;识别系统稳定性和容量配比。。。

      稳定性治理

      核心链路分离

      服务依赖降级

      来源:www.cnblogs.com/wintersun/

    • Spring Boot 优雅实现多租户架构,so easy!

      快乐分享,Java干货及时送达👇

      一、概述

      1.什么是多租户架构?

      多租户架构是指在一个应用中支持多个租户(Tenant)同时访问,每个租户拥有独立的资源和数据,并且彼此之间完全隔离。通俗来说,多租户就是把一个应用按照客户的需求“分割”成多个独立的实例,每个实例互不干扰。

      2. 多租户架构的优势

      • 更好地满足不同租户的个性化需求。
      • 可以降低运维成本,减少硬件、网络等基础设施的投入。
      • 节约开发成本,通过复用代码,快速上线新的租户实例。
      • 增强了系统的可扩展性和可伸缩性,支持水平扩展,每个租户的数据和资源均可管理和控制。

      3. 实现多租户架构的技术选择

      对于实现多租户架构技术不是最重要的最重要的是正确的架构思路。但是选择正确的技术可以更快地实现多租户架构。

      二、设计思路

      1. 架构选型

      基于Java开发多租户应用推荐使用Spring Boot和Spring Cloud。Spring Boot能快速搭建应用并提供许多成熟的插件。Spring Cloud则提供了许多实现微服务架构的工具和组件。

      1.1 Spring Boot

      使用Spring Boot可以简化项目的搭建过程自动配置许多常见的第三方库和组件,减少了开发人员的工作量。

      @RestController
      public class TenantController {

          @GetMapping("/hello")
          public String hello(@RequestHeader("tenant-id") String tenantId) {
              return "Hello, " + tenantId;
          }
      }

      1.2 Spring Cloud

      在架构多租户的系统时Spring Cloud会更加有用。Spring Cloud提供了一些成熟的解决方案,如Eureka、Zookeeper、Consul等,以实现服务发现、负载均衡等微服务功能。

      2. 数据库设计

      在多租户环境中数据库必须为每个租户分别存储数据并确保数据隔离。我们通常使用以下两种方式实现:

      • 多个租户共享相同的数据库,每个表中都包含tenant_id这一列,用于区分不同租户的数据。
      • 为每个租户创建单独的数据库,每个数据库内的表结构相同,但数据相互隔离。

      3. 应用多租户部署

      为了实现多租户在应用部署时我们需要考虑以下两个问题。

      3.1 应用隔离

      在多租户环境中不同租户需要访问不同的资源,因此需要进行应用隔离。可以通过构建独立的容器或虚拟机、使用命名空间等方式实现。Docker就是一种非常流行的隔离容器技术。

      3.2 应用配置

      由于每个租户都有自己的配置需求因此需要为每个租户分别设置应用配置信息,例如端口号、SSL证书等等。这些配置可以存储在数据库中,也可以存储在云配置中心中。

      4. 租户管理

      在多租户系统中需要能够管理不同租户的数据和资源,同时需要为每个租户分配相应的权限。解决方案通常包括以下两部分。

      4.1 租户信息维护

      租户信息的维护包括添加、修改、删除、查询等操作,要求能够根据租户名称或租户ID快速查找对应的租户信息。

      CREATE TABLE tenant (
          id BIGINT AUTO_INCREMENT PRIMARY KEY,
          name VARCHAR(50)
       NOT NULL UNIQUE,
          description VARCHAR(255),
          created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
          updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
      )
      ;

      4.2 租户权限控制

      在多租户应用中必须为每个租户分别设置对系统资源的访问权限。例如,A租户和B租户不能访问彼此的数据。

      @EnableGlobalMethodSecurity(prePostEnabled = true)
      @Configuration
      public class SecurityConfig extends WebSecurityConfigurerAdapter {

          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http.authorizeRequests()
                      .antMatchers("/api/tenant/**").hasRole("ADMIN")
                      .anyRequest().authenticated()
                      .and()
                      .formLogin();
          }

          @Autowired
          public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(userDetailsService())
                      .passwordEncoder(new BCryptPasswordEncoder())
                      .and()
                      .inMemoryAuthentication()
                      .withUser("admin")
                      .password(new BCryptPasswordEncoder().encode("123456"))
                      .roles("ADMIN");
          }
      }

      三、技术实现

      1. Spring Boot中的多租户实现

      在Spring Boot中可以通过多数据源和动态路由来实现多租户机制。

      1.1 多数据源实现

      多数据源是指为不同的租户配置不同的数据源,使得每个租户都可以访问自己的独立数据。具体实现方法如下:

      @Configuration
      public class DataSourceConfig {
          @Bean(name = "dataSourceA")
          @ConfigurationProperties(prefix = "spring.datasource.a")
          public DataSource dataSourceA() {
              return DataSourceBuilder.create().build();
          }

          @Bean(name = "dataSourceB")
          @ConfigurationProperties(prefix = "spring.datasource.b")
          public DataSource dataSourceB() {
              return DataSourceBuilder.create().build();
          }

          @Bean(name = "dataSourceC")
          @ConfigurationProperties(prefix = "spring.datasource.c")
          public DataSource dataSourceC() {
              return DataSourceBuilder.create().build();
          }
      }

      以上代码是配置了三个数据源分别对应三个租户。然后在使用时,可以使用注解标记需要连接的数据源。

      @Service
      public class ProductService {
          @Autowired
          @Qualifier("dataSourceA")
          private DataSource dataSource;

          // ...
      }

      1.2 动态路由实现

      动态路由是指根据请求的URL或参数动态地切换到对应租户的数据源。具体实现如下:

      public class DynamicDataSource extends AbstractRoutingDataSource {
          @Override
          protected Object determineCurrentLookupKey() {
              return TenantContextHolder.getTenantId();
          }
      }

      @Configuration
      public class DataSourceConfig {
          @Bean(name = "dataSource")
          @ConfigurationProperties(prefix = "spring.datasource")
          public DataSource dataSource() {
              return DataSourceBuilder.create().type(DynamicDataSource.class).build();
          }
      }

      以上是动态路由的核心代码DynamicDataSource继承自AbstractRoutingDataSource,通过determineCurrentLookupKey()方法动态获得租户ID,然后切换到对应的数据源。关注公众号:码猿技术专栏,回复关键词:1111 获取阿里内部Java性能调优手册!

      2. Spring Cloud中的多租户实现

      在Spring Cloud中可以通过服务注册与发现、配置中心、负载均衡等方式实现多租户机制。

      2.1 服务注册与发现

      使用Spring Cloud中的Eureka实现服务注册与发现。每个租户的服务都在注册中心以不同的应用名称进行注册,客户端可以通过服务名称来访问对应租户的服务。

      2.2 配置中心

      使用Spring Cloud Config作为配置中心。配置文件以租户ID进行区分,客户端通过读取对应租户的配置文件来获取配置信息。

      2.3 负载均衡

      使用Spring Cloud Ribbon作为负载均衡器。根据请求的URL或参数选择对应租户的服务实例进行请求转发。

      2.4 API

      在API网关层面实现多租户机制根据请求的URL或参数判断所属租户,并转发到对应租户的服务实例。

      四、 应用场景

      1. 私有云环境

      私有云环境指的是由企业自行搭建的云环境,不对外提供服务,主要应用于企业内部的数据存储、管理、共享和安全控制。相较于公有云,私有云的优点在于可以更好地保护企业核心数据,同时也能够满足企业对于数据安全性和可控性的要求。

      2. 公有云环境

      公有云环境指的是由云服务商搭建并对外提供服务的云环境,用户可以根据需要购买相应的云服务,如云存储、云计算、云数据库等。相较于私有云,公有云的优点在于具有成本低廉、弹性伸缩、全球化部署等特点,能够更好地满足企业快速发展的需求。

      3. 企业级应用

      企业级应用是指面向企业客户的应用程序,主要包括ERP、CRM、OA等一系列应用系统。这类应用的特点在于功能强大、流程复杂、数据量大,需要满足企业的高效率、高可靠性、高安全性和易维护性等要求。在云计算环境下,企业可以将这些应用部署在私有云或公有云上,减少了硬件设备的投入和维护成本,提高了管理效率。

      五、实现步骤

      1. 搭建Spring Boot和Spring Cloud环境

      首先需要在Maven项目中引入以下依赖:


      dependency>
          groupId>org.springframework.bootgroupId>
          artifactId>spring-boot-starter-webartifactId>
      dependency>


      dependency>
          groupId>org.springframework.cloudgroupId>
          artifactId>spring-cloud-dependenciesartifactId>
          version>2020.0.3version>
          type>pomtype>
          scope>importscope>
      dependency>

      然后需要在application.yml中配置相应的参数,如下所示:

      spring:
        datasource:
          url: jdbc:mysql://localhost:3306/appdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
          username: root
          password: 123456

      mybatis:
        type-aliases-package: com.example.demo.model
        mapper-locations: classpath:mapper/*.xml

      server:
        port: 8080

      eureka:
        client:
          serviceUrl:
            defaultZone: http://localhost:8761/eureka/

      management:
        endpoints:
          web:
            exposure:
              include: "*"

      其中datasource.url为数据库连接的URL,username和password为数据库连接的账号和密码;server.port为Spring Boot应用启动的端口;eureka.client.serviceUrl.defaultZone为Eureka服务注册中心的URL。

      Java指南:java-family.cn

      2. 修改数据库设计

      接下来需要对数据库进行相应的修改,以支持多租户部署。具体来说,我们需要在数据库中添加一个与租户相关的字段,以便在应用中区分不同的租户。

      3. 实现应用多租户部署

      接着需要在代码中实现应用的多租户部署功能。具体来说,我们需要为每个租户实例化对应的Spring Bean,并根据租户ID将请求路由到相应的Bean中去处理。

      以下是一个简单的实现示例:

      @Configuration
      public class MultiTenantConfig {
       
          // 提供对应租户的数据源
          @Bean
          public DataSource dataSource(TenantRegistry tenantRegistry) {
              return new TenantAwareDataSource(tenantRegistry);
          }
       
          // 多租户Session工厂
          @Bean(name = "sqlSessionFactory")
          public SqlSessionFactory sqlSessionFactory(DataSource dataSource)
                  throws Exception 
      {
              SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
              sessionFactory.setDataSource(dataSource);
              return sessionFactory.getObject();
          }
       
          // 动态切换租户
          @Bean
          public MultiTenantInterceptor multiTenantInterceptor(TenantResolver tenantResolver) {
              MultiTenantInterceptor interceptor = new MultiTenantInterceptor();
              interceptor.setTenantResolver(tenantResolver);
              return interceptor;
          }
       
          // 注册拦截器
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(multiTenantInterceptor());
          }
       
          // 注册租户信息
          @Bean
          public TenantRegistry tenantRegistry() {
              return new TenantRegistryImpl();
          }
           
          // 解析租户ID
          @Bean
          public TenantResolver tenantResolver() {
              return new HeaderTenantResolver();
          }
       
      }

      其中MultiTenantConfig是多租户部署的核心配置类,它提供了对应租户数据源、多租户Session工厂、动态切换租户等功能。

      4. 实现租户管理

      最后需要实现一个租户管理的功能,以便在系统中管理不同的租户。具体来说,我们可以使用Spring Cloud的服务注册与发现组件Eureka来注册每个租户的实例,并在管理界面中进行相应的操作。另外,我们还需要为每个租户提供一个独立的数据库,以保证数据隔离性。

      六、小结回顾

      本文详细介绍了如何使用Spring Boot和Spring Cloud实现一个支持多租户部署的应用。主要包括搭建Spring Boot和Spring Cloud环境、修改数据库设计、实现应用多租户部署、实现租户管理等方面。

      应用场景主要包括SaaS应用、多租户云服务等。优劣势主要体现在提升了应用的可扩展性和可维护性,但也增加了部署和管理的复杂度。未来的改进方向可以考虑进一步提升多租户管理的自动化程度,减少人工干预和错误率。

      来源:blog.csdn.net/u010349629/article

    • 40亿个QQ号,限制1G内存,如何去重?

      快乐分享,Java干货及时送达👇

      40亿个QQ号,限制1G内存,如何去重?

      40亿个unsigned int,如果直接用内存存储的话,需要:

      4*4000000000 /1024/1024/1024 = 14.9G ,考虑到其中有一些重复的话,那1G的空间也基本上是不够用的。

      想要实现这个功能,可以借助位图。

      使用位图的话,一个数字只需要占用1个bit,那么40亿个数字也就是:

      4000000000 * 1 /8 /1024/1024 = 476M

      相比于之前的14.9G来说,大大的节省了很多空间。

      比如要把我的QQ号”907607222″放到Bitmap中,就需要找到第907607222这个位置,然后把他设置成1就可以了。

      图片

      这样,把40亿个数字都放到Bitmap之后,所有位置上是1的表示存在,不为1的表示不存在,相同的QQ号只需要设置一次1就可以了,那么,最终就把所有是1的数字遍历出来就行了。

      什么是BitMap?有什么用?

      位图(BitMap),基本思想就是用一个bit来标记元素,bit是计算机中最小的单位,也就是我们常说的计算机中的0和1,这种就是用一个位来表示的。

      所谓位图,其实就是一个bit数组,即每一个位置都是一个bit,其中的取值可以是0或者1

      图片

      像上面的这个位图,可以用来表示1,4,6:

      图片

      如果不用位图的话,我们想要记录1,4,6 这三个整型的话,就需要用三个unsigned int,已知每个unsigned int占4个字节,那么就是3*4 = 12个字节,一个字节有8 bit,那么就是 12*8 = 96 个bit。

      所以,位图最大的好处就是节省空间。

      位图有很多种用途,特别适合用在去重、排序等场景中,著名的布隆过滤器就是基于位图实现的。

      但是位图也有着一定的限制,那就是他只能表示0和1,无法存储其他的数字。所以他只适合这种能表示ture or false的场景。

      什么是布隆过滤器,实现原理是什么?

      布隆过滤器是一种数据结构,用于快速检索一个元素是否可能存在于一个集合(bit 数组)中。

      它的基本原理是利用多个哈希函数,将一个元素映射成多个位,然后将这些位设置为 1。当查询一个元素时,如果这些位都被设置为 1,则认为元素可能存在于集合中,否则肯定不存在

      所以,布隆过滤器可以准确的判断一个元素是否一定不存在,但是因为哈希冲突的存在,所以他没办法判断一个元素一定存在。只能判断可能存在。

      图片

      所以,布隆过滤器是存在误判的可能的,也就是当一个不存在的Hero元素,经过hash1、hash2和hash3之后,刚好和其他的值的哈希结果冲突了。那么就会被误判为存在,但是其实他并不存在。

      图片

      想要降低这种误判的概率,主要的办法就是降低哈希冲突的概率及引入更多的哈希算法。

      下面是布隆过滤器的工作过程:

      1、初始化布隆过滤器

      在初始化布隆过滤器时,需要指定集合的大小和误判率。布隆过滤器内部包含一个bit数组和多个哈希函数,每个哈希函数都会生成一个索引值。

      2、添加元素到布隆过滤器

      要将一个元素添加到布隆过滤器中,首先需要将该元素通过多个哈希函数生成多个索引值,然后将这些索引值对应的位设置为 1。如果这些索引值已经被设置为 1,则不需要再次设置。

      3、查询元素是否存在于布隆过滤器中

      要查询一个元素是否存在于布隆过滤器中,需要将该元素通过多个哈希函数生成多个索引值,并判断这些索引值对应的位是否都被设置为 1。如果这些位都被设置为 1,则认为元素可能存在于集合中,否则肯定不存在。

      布隆过滤器的主要优点是可以快速判断一个元素是否属于某个集合,并且可以在空间和时间上实现较高的效率。但是,它也存在一些缺点,例如:

      1. 布隆过滤器在判断元素是否存在时,有一定的误判率。、
      2. 布隆过滤器删除元素比较困难,因为删除一个元素需要将其对应的多个位设置为 0,但这些位可能被其他元素共享。

      应用场景

      布隆过滤器因为他的效率非常高,所以被广泛的使用,比较典型的场景有以下几个:

      1、网页爬虫: 爬虫程序可以使用布隆过滤器来过滤掉已经爬取过的网页,避免重复爬取和浪费资源。

      2、缓存系统: 缓存系统可以使用布隆过滤器来判断一个查询是否可能存在于缓存中,从而减少查询缓存的次数,提高查询效率。布隆过滤器也经常用来解决缓存穿透的问题。

      3、分布式系统: 在分布式系统中,可以使用布隆过滤器来判断一个元素是否存在于分布式缓存中,避免在所有节点上进行查询,减少网络负载。

      4、垃圾邮件过滤: 布隆过滤器可以用于判断一个邮件地址是否在垃圾邮件列表中,从而过滤掉垃圾邮件。

      5、黑名单过滤: 布隆过滤器可以用于判断一个IP地址或手机号码是否在黑名单中,从而阻止恶意请求。

      如何使用

      Java中可以使用第三方库来实现布隆过滤器,常见的有Google Guava库和Apache Commons库以及Redis。

      如Guava:

      import com.google.common.hash.BloomFilter;
      import com.google.common.hash.Funnels;
      public class BloomFilterExample {
          public static void main(String[] args) {
              // 创建布隆过滤器,预计插入100个元素,误判率为0.01
              BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 100, 0.01);
              // 插入元素
              bloomFilter.put("Lynn");
              bloomFilter.put("666");
              bloomFilter.put("八股文");
              // 判断元素是否存在
              System.out.println(bloomFilter.mightContain("Lynn")); // true
              System.out.println(bloomFilter.mightContain("张三"));  // false
          }
      }

      Apache Commons:

      import org.apache.commons.lang3.StringUtils;
      import org.apache.commons.collections4.BloomFilter;
      import org.apache.commons.collections4.functors.HashFunctionIdentity;
      public class BloomFilterExample {
          public static void main(String[] args) {
              // 创建布隆过滤器,预计插入100个元素,误判率为0.01
              BloomFilter bloomFilter = new BloomFilter(HashFunctionIdentity.hashFunction(StringUtils::hashCode), 100, 0.01);
              // 插入元素
              bloomFilter.put("Lynn");
              bloomFilter.put("666");
              bloomFilter.put("八股文");
              // 判断元素是否存在
              System.out.println(bloomFilter.mightContain("Lynn")); // true
              System.out.println(bloomFilter.mightContain("张三"));  // false
          }
      }

      Redis中可以通过Bloom模块来使用,使用Redisson可以:

      Config config = new Config();
      config.useSingleServer().setAddress("redis://127.0.0.1:6379");
      RedissonClient redisson = Redisson.create(config);
      RBloomFilter bloomFilter = redisson.getBloomFilter("myfilter");
      bloomFilter.tryInit(100, 0.01);
      bloomFilter.add("Lynn");
      bloomFilter.add("666");
      bloomFilter.add("八股文");
      System.out.println(bloomFilter.contains("Lynn"));
      System.out.println(bloomFilter.contains("张三"));
      redisson.shutdown();

      首先创建一个RedissonClient对象,然后通过该对象获取一个RBloomFilter对象,使用tryInit方法来初始化布隆过滤器,指定了最多能添加的元素数量为100,误判率为0.01。

      然后,使用add方法将元素”犬小哈”、”666″和”八股文”添加到布隆过滤器中,使用contains方法来检查元素是否存在于布隆过滤器中。

      或者Jedis也可以:

      Jedis jedis = new Jedis("localhost");
      jedis.bfCreate("myfilter", 100, 0.01);
      jedis.bfAdd("myfilter""Lynn");
      jedis.bfAdd("myfilter""666");
      jedis.bfAdd("myfilter""八股文");
      System.out.println(jedis.bfExists("myfilter""Lynn"));
      System.out.println(jedis.bfExists("myfilter""张三"));
      jedis.close();

      来源:blog.csdn.net/songmulin/article/details/130814507

    • 简单几行代码,优雅的实现 Spring Boot 项目鉴权!

      快乐分享,Java干货及时送达👇

      1. 技术选型

      最近在做登录、授权的功能,一开始考虑到的是spring boot + spring security,但spring security太重,而我们是轻量级的项目,所以,spring security不适合我们。

      而后考虑spring boot + shiro,但shiro自带的aop会影响spring boot的aop,所以,shiro也不适合我们。

      后来浏览github时,发现Sa-Token这个框架,如下是Sa-Token的功能图:

      2. Sa-Token概述

      2.1 简单介绍

      Sa-Token是一个轻量级Java权限认证框架。

      主要解决的问题如下:

      • 登录认证
      • 权限认证
      • 单点登录
      • OAuth2.0
      • 分布式Session会话
      • 微服务网关鉴权等一系列权限相关问题。

      2.2 登录认证

      设计思路

      对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:

      • 如果校验通过,则:正常返回数据。
      • 如果校验未通过,则:抛出异常,告知其需要先进行登录。

      那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:

      • 用户提交name + password参数,调用登录接口。
      • 登录成功,返回这个用户的Token会话凭证。
      • 用户后续的每次请求,都携带上这个Token。
      • 服务器根据Token判断此会话是否登录成功。

      所谓登录认证,指的就是服务器校验账号密码,为用户颁发Token会话凭证的过程,这个Token也是我们后续通过接口校验的关键所在。

      登录与注销

      根据以上思路,我们需要一个会话登录的函数:

      // 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
      StpUtil.login(Object id);     

      只此一句代码,便可以使会话登录成功,实际上Sa-Token在背后做了大量的工作,包括但不限于:

      • 检查此账号是否已被封禁
      • 检查此账号是否之前已有登录
      • 为账号生成 Token 凭证与 Session 会话
      • 通知全局侦听器,xx 账号登录成功
      • 将 Token 注入到请求上下文
      • 等等其它工作……

      你暂时不需要完整的了解整个登录过程,你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过Cookie 上下文返回给了前端。

      所以一般情况下,我们的登录接口代码,会大致类似如下:

      // 会话登录接口 
      @RequestMapping("doLogin")
      public SaResult doLogin(String name, String pwd) {
          // 第一步:比对前端提交的账号名称、密码
          if("zhang".equals(name) && "123456".equals(pwd)) {
              // 第二步:根据账号id,进行登录 
              StpUtil.login(10001);
              return SaResult.ok("登录成功");
          }
          return SaResult.error("登录失败");
      }

      如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:此处仅仅做了会话登录,但并没有主动向前端返回Token信息。是因为不需要吗?严格来讲是需要的,只不过StpUtil.login(id)方法利用了Cookie自动注入的特性,省略了你手写返回Token的代码。

      你需要了解Cookie最基本的两点:

      • Cookie可以从后端控制往浏览器中写入Token值。
      • Cookie会在每次请求时自动提交Token值。

      因此,在Cookie功能的加持下,我们可以仅靠StpUtil.login(id) 一句代码就完成登录认证。

      除了登录方法,我们还需要:

      // 当前会话注销登录
      StpUtil.logout();

      // 获取当前会话是否已经登录,返回true=已登录,false=未登录
      StpUtil.isLogin();

      // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
      StpUtil.checkLogin();

      异常NotLoginException代表当前会话暂未登录,可能的原因有很多:

      • 前端没有提交 Token
      • 前端提交的 Token 是无效的
      • 前端提交的 Token 已经过期
      • …… 等等

      会话查询

      // 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
      StpUtil.getLoginId();

      // 类似查询API还有:
      StpUtil.getLoginIdAsString();    // 获取当前会话账号id, 并转化为`String`类型
      StpUtil.getLoginIdAsInt();       // 获取当前会话账号id, 并转化为`int`类型
      StpUtil.getLoginIdAsLong();      // 获取当前会话账号id, 并转化为`long`类型

      // ---------- 指定未登录情形下返回的默认值 ----------

      // 获取当前会话账号id, 如果未登录,则返回null 
      StpUtil.getLoginIdDefaultNull();

      // 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
      StpUtil.getLoginId(T defaultValue);

      Token查询

      // 获取当前会话的token值
      StpUtil.getTokenValue();

      // 获取当前`StpLogic`的token名称
      StpUtil.getTokenName();

      // 获取指定token对应的账号id,如果未登录,则返回 null
      StpUtil.getLoginIdByToken(String tokenValue);

      // 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
      StpUtil.getTokenTimeout();

      // 获取当前会话的token信息参数
      StpUtil.getTokenInfo();

      有关TokenInfo参数详解,如下代码所示:

      {
          "code"200,
          "msg""ok",
          "data": {
              "tokenName""satoken",           // token名称
              "tokenValue""e67b99f1-3d7a-4a8d-bb2f-e888a0805633",      // token值
              "isLogin"true,                  // 此token是否已经登录
              "loginId""10001",               // 此token对应的LoginId,未登录时为null
              "loginType""login",              // 账号类型标识
              "tokenTimeout"2591977,          // token剩余有效期 (单位: 秒)
              "sessionTimeout"2591977,        // User-Session剩余有效时间 (单位: 秒)
              "tokenSessionTimeout": -2,        // Token-Session剩余有效时间 (单位: 秒)
              "tokenActivityTimeout": -1,       // token剩余无操作有效时间 (单位: 秒)
              "loginDevice""default-device"   // 登录设备类型 
          },
      }

      小测试,加深理解

      新建LoginController,复制以下代码

      /**
       * 登录测试 
       */

      @RestController
      @RequestMapping("/acc/")
      public class LoginController {

          // 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
          @RequestMapping("doLogin")
          public SaResult doLogin(String name, String pwd) {
              // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 
              if("zhang".equals(name) && "123456".equals(pwd)) {
                  StpUtil.login(10001);
                  return SaResult.ok("登录成功");
              }
              return SaResult.error("登录失败");
          }

          // 查询登录状态  ---- http://localhost:8081/acc/isLogin
          @RequestMapping("isLogin")
          public SaResult isLogin() {
              return SaResult.ok("是否登录:" + StpUtil.isLogin());
          }

          // 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo
          @RequestMapping("tokenInfo")
          public SaResult tokenInfo() {
              return SaResult.data(StpUtil.getTokenInfo());
          }

          // 测试注销  ---- http://localhost:8081/acc/logout
          @RequestMapping("logout")
          public SaResult logout() {
              StpUtil.logout();
              return SaResult.ok();
          }

      }

      2.3 权限认证

      设计思路

      所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限:

      • 有,就让你通过。
      • 没有?那么禁止访问!

      深入到底层数据中,就是每个账号都会拥有一个权限码集合,框架来校验这个集合中是否包含指定的权限码。

      例如:当前账号拥有权限码集合 [user-add, user-delete, user-get],这时候我来校验权限 user-update,则其结果就是:验证失败,禁止访问。

      所以现在问题的核心就是:

      • 如何获取一个账号所拥有的的权限码集合?
      • 本次操作需要验证的权限码是哪个?

      获取当前账号权限码集合

      因为每个项目的需求不同,其权限设计也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中, 所以Sa-Token将此操作以接口的方式暴露给你,以方便你根据自己的业务逻辑进行重写。

      你需要做的就是新建一个类,实现StpInterface接口,例如以下代码:

      /**
       * 自定义权限验证接口扩展 
       */

      @Component    // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展 
      public class StpInterfaceImpl implements StpInterface {

          /**
           * 返回一个账号所拥有的权限码集合 
           */

          @Override
          public List getPermissionList(Object loginId, String loginType) {
              // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
              List list = new ArrayList();    
              list.add("101");
              list.add("user-add");
              list.add("user-delete");
              list.add("user-update");
              list.add("user-get");
              list.add("article-get");
              return list;
          }

          /**
           * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
           */

          @Override
          public List getRoleList(Object loginId, String loginType) {
              // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
              List list = new ArrayList();    
              list.add("admin");
              list.add("super-admin");
              return list;
          }

      }

      参数解释:

      • loginId:账号id,即你在调用StpUtil.login(id)时写入的标识值。
      • loginType:账号体系标识

      权限认证

      然后就可以用以下api来鉴权了

      // 获取:当前账号所拥有的权限集合
      StpUtil.getPermissionList();

      // 判断:当前账号是否含有指定权限, 返回true或false
      StpUtil.hasPermission("user-update");        

      // 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException 
      StpUtil.checkPermission("user-update");        

      // 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
      StpUtil.checkPermissionAnd("user-update""user-delete");        

      // 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
      StpUtil.checkPermissionOr("user-update""user-delete");    

      扩展:NotPermissionException对象可通过getLoginType()方法获取具体是哪个StpLogic抛出的异常

      角色认证

      在Sa-Token中,角色和权限可以独立验证

      // 获取:当前账号所拥有的角色集合
      StpUtil.getRoleList();

      // 判断:当前账号是否拥有指定角色, 返回true或false
      StpUtil.hasRole("super-admin");        

      // 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
      StpUtil.checkRole("super-admin");        

      // 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
      StpUtil.checkRoleAnd("super-admin""shop-admin");        

      // 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] 
      StpUtil.checkRoleOr("super-admin""shop-admin");        

      扩展:NotRoleException对象可通过getLoginType()方法获取具体是哪个StpLogic抛出的异常

      拦截全局异常

      有同学要问,鉴权失败,抛出异常,然后呢?要把异常显示给用户看吗?当然不可以!

      你可以创建一个全局异常拦截器,统一返回给前端的格式,参考:

      @RestControllerAdvice
      public class GlobalExceptionHandler {
          // 全局异常拦截 
          @ExceptionHandler
          public SaResult handlerException(Exception e) {
              e.printStackTrace(); 
              return SaResult.error(e.getMessage());
          }
      }

      权限通配符

      Sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有user*的权限时,user-add、user-delete、user-update都将匹配通过

      // 当拥有 user* 权限时
      StpUtil.hasPermission("user-add");        // true
      StpUtil.hasPermission("user-update");     // true
      StpUtil.hasPermission("art-add");         // false

      // 当拥有 *-delete 权限时
      StpUtil.hasPermission("user-add");        // false
      StpUtil.hasPermission("user-delete");     // true
      StpUtil.hasPermission("art-delete");      // true

      // 当拥有 *.js 权限时
      StpUtil.hasPermission("index.js");        // true
      StpUtil.hasPermission("index.css");       // false
      StpUtil.hasPermission("index.html");      // false

      上帝权限:当一个账号拥有 * 权限时,他可以验证通过任何权限码 (角色认证同理)

      如何把权限精确到按钮级?

      权限精确到按钮级的意思就是指:权限范围可以控制到页面上的每一个按钮是否显示。

      思路:如此精确的范围控制只依赖后端已经难以完成,此时需要前端进行一定的逻辑判断。

      如果是前后端分离项目,则:

      • 在登录时,把当前账号拥有的所有权限码一次性返回给前端。
      • 前端将权限码集合保存在localStorage或其它全局状态管理对象中。
      • 在需要权限控制的按钮上,使用 js 进行逻辑判断,例如在Vue框架中我们可以使用如是写法:

      其中:arr是当前用户拥有的权限码数组,user:delete是显示按钮需要拥有的权限码,删除按钮是用户拥有权限码才可以看到的内容。

      注意:以上写法只为提供一个参考示例,不同框架有不同写法,大家可根据项目技术栈灵活封装进行调用。

      前端有了鉴权后端还需要鉴权吗?

      需要!

      前端的鉴权只是一个辅助功能,对于专业人员这些限制都是可以轻松绕过的,为保证服务器安全,无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验!

      3. 功能一览

      上述只提供了登录认证和权限认证的两个功能,Sa-Token还有如下诸多功能:

      • 登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录
      • 权限认证 —— 权限认证、角色认证、会话二级认证
      • Session会话 —— 全端共享Session、单端独享Session、自定义Session
      • 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线
      • 账号封禁 —— 指定天数封禁、永久封禁、设定解封时间
      • 持久层扩展 —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
      • 分布式会话 —— 提供jwt集成、共享数据中心两种分布式会话方案
      • 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证
      • 单点登录 —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定
      • OAuth2.0认证 —— 基于RFC-6749标准编写,OAuth2.0标准流程的授权认证,支持openid模式
      • 二级认证 —— 在已登录的基础上再次认证,保证安全性
      • Basic认证 —— 一行代码接入Http Basic认证
      • 独立Redis —— 将权限缓存与业务缓存分离
      • 临时Token验证 —— 解决短时间的Token授权问题
      • 模拟他人账号 —— 实时操作任意用户状态数据
      • 临时身份切换 —— 将会话身份临时切换为其它账号
      • 前后台分离 —— APP、小程序等不支持Cookie的终端
      • 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
      • 多账号认证体系 —— 比如一个商城项目的user表和admin表分开鉴权
      • 花式token生成 —— 内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀
      • 注解式鉴权 —— 优雅的将鉴权与业务代码分离
      • 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配restful模式
      • 自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签
      • 会话治理 —— 提供方便灵活的会话查询接口
      • 记住我模式 —— 适配[记住我]模式,重启浏览器免验证
      • 密码加密 —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密
      • 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作
      • 开箱即用 —— 提供SpringMVC、WebFlux等常见web框架starter集成包,真正的开箱即用

      4. Sa-Token使用

      4.1 引入Sa-Token依赖

      Maven依赖


      dependency>
          groupId>cn.dev33groupId>
          artifactId>sa-token-spring-boot-starterartifactId>
          version>1.30.0version>
      dependency>

      Gradle依赖

      implementation 'cn.dev33:sa-token-spring-boot-starter:1.30.0'

      4.2 Sa-Token源码

      源码下载

      • Gitee地址:https://gitee.com/dromara/sa-token
      • GitHub地址:https://github.com/dromara/sa-token

      源码目录

      ── sa-token
          ├── sa-token-core                         // [核心] Sa-Token 核心模块
          ├── sa-token-starter                      // [整合] Sa-Token 与其它框架整合
              ├── sa-token-servlet                      // [整合] Sa-Token 整合 Servlet容器实现类包
              ├── sa-token-spring-boot-starter          // [整合] Sa-Token 整合 SpringBoot 快速集成 
              ├── sa-token-reactor-spring-boot-starter  // [整合] Sa-Token 整合 Reactor 响应式编程 快速集成 
              ├── sa-token-solon-plugin                 // [整合] Sa-Token 整合 Solon 快速集成 
              ├── sa-token-jfinal-plugin                // [整合] Sa-Token 整合 JFinal 快速集成 
              ├── sa-token-jboot-plugin                 // [整合] Sa-Token 整合 jboot 快速集成 
          ├── sa-token-plugin                       // [插件] Sa-Token 插件合集
              ├── sa-token-dao-redis                    // [插件] Sa-Token 整合 Redis (使用jdk默认序列化方式)
              ├── sa-token-dao-redis-jackson            // [插件] Sa-Token 整合 Redis (使用jackson序列化方式)
              ├── sa-token-spring-aop                   // [插件] Sa-Token 整合 SpringAOP 注解鉴权
              ├── sa-token-temp-jwt                     // [插件] Sa-Token 整合 jwt 临时令牌鉴权 
              ├── sa-token-quick-login                  // [插件] Sa-Token 快速注入登录页插件 
              ├── sa-token-alone-redis                  // [插件] Sa-Token 独立Redis插件,实现[权限缓存与业务缓存分离]
              ├── sa-token-sso                          // [插件] Sa-Token 整合 SSO 单点登录
              ├── sa-token-oauth2                       // [插件] Sa-Token 实现 OAuth2.0 模块 
              ├── sa-token-dialect-thymeleaf            // [插件] Sa-Token 标签方言(Thymeleaf版)
              ├── sa-token-jwt                          // [插件] Sa-Token 整合 jwt 登录认证
          ├── sa-token-demo                         // [示例] Sa-Token 示例合集
              ├── sa-token-demo-springboot              // [示例] Sa-Token 整合 SpringBoot 
              ├── sa-token-demo-springboot-redis        // [示例] Sa-Token 整合 SpringBoot 
              ├── sa-token-demo-webflux                 // [示例] Sa-Token 整合 WebFlux 
              ├── sa-token-demo-jwt                     // [示例] Sa-Token 集成 jwt 
              ├── sa-token-demo-solon                   // [示例] Sa-Token 集成 Solon 
              ├── sa-token-demo-quick-login             // [示例] Sa-Token 集成 quick-login 模块 
              ├── sa-token-demo-alone-redis             // [示例] Sa-Token 集成 alone-redis 模块
              ├── sa-token-demo-thymeleaf               // [示例] Sa-Token 集成 Thymeleaf 标签方言
              ├── sa-token-demo-jwt                     // [示例] Sa-Token 集成 jwt 登录认证 
              ├── sa-token-demo-sso-server              // [示例] Sa-Token 集成 SSO单点登录-Server认证中心
              ├── sa-token-demo-sso1-client             // [示例] Sa-Token 集成 SSO单点登录-模式一 应用端 
              ├── sa-token-demo-sso2-client             // [示例] Sa-Token 集成 SSO单点登录-模式二 应用端
              ├── sa-token-demo-sso3-client             // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端
              ├── sa-token-demo-sso3-client-nosdk       // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (不使用sdk,纯手动对接)
              ├── sa-token-demo-sso-server-h5           // [示例] Sa-Token 集成 SSO单点登录-Server认证中心 (前后端分离)
              ├── sa-token-demo-sso-client-h5           // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离)
              ├── sa-token-demo-oauth2-server           // [示例] Sa-Token 集成 OAuth2.0 (服务端)
              ├── sa-token-demo-oauth2-client           // [示例] Sa-Token 集成 OAuth2.0 (客户端)
              ├── sa-token-demo-websocket               // [示例] Sa-Token 集成 Web-Socket 鉴权示例
              ├── sa-token-demo-websocket-spring        // [示例] Sa-Token 集成 Web-Socket(Spring封装版) 鉴权示例
          ├── sa-token-test                         // [测试] Sa-Token 单元测试合集
              ├── sa-token-core-test                    // [测试] Sa-Token Core核心包单元测试
              ├── sa-token-springboot-test              // [测试] Sa-Token SpringBoot 整合测试
              ├── sa-token-springboot-integrate-test    // [测试] Sa-Token SpringBoot 整合客户端测试
              ├── sa-token-jwt-test                     // [测试] Sa-Token jwt 整合测试
          ├── sa-token-doc                          // [文档] Sa-Token 开发文档 
          ├──pom.xml                                // [依赖] 顶级pom文件 

      源码运行示例

      • 下载代码(学习测试用master分支)。
      • 从根目录导入项目。
      • 选择相应的示例添加为Maven项目,打开XxxApplication.java运行。

      4.3 SpringBoot集成Sa-Token示例

      创建项目

      在IDE中新建一个SpringBoot项目,例如:sa-token-demo-springboot

      添加依赖

      在pom.xml中添加依赖:


      dependency>
          groupId>cn.dev33groupId>
          artifactId>sa-token-spring-boot-starterartifactId>
          version>1.30.0version>
      dependency>

      设置配置文件

      你可以零配置启动项目 ,但同时你也可以在 application.yml 中增加如下配置,定制性使用框架:

      server:
          ## 端口
          port: 8081

      ## Sa-Token配置
      sa-token: 
          ## token 名称 (同时也是cookie名称)
          token-name: satoken
          ## token 有效期,单位s 默认30天, -1代表永不过期 
          timeout: 2592000
          ## token 临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
          activity-timeout: -1
          ## 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) 
          is-concurrent: true
          ## 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) 
          is-share: false
          ## token风格
          token-style: uuid
          ## 是否输出操作日志 
          is-log: false

      创建启动类

      在项目中新建包com.pj,在此包内新建主类 SaTokenDemoApplication.java,复制以下代码:

      @SpringBootApplication
      public class SaTokenDemoApplication {
          public static void main(String[] args) throws JsonProcessingException {
              SpringApplication.run(SaTokenDemoApplication.classargs);
              System.out.println("启动成功:Sa-Token配置如下:" + SaManager.getConfig());
          }
      }

      创建测试Controller

      @RestController
      @RequestMapping("/user/")
      public class UserController {

          // 测试登录,浏览器访问:http://localhost:8081/user/doLogin?username=zhang&password=123456
          @RequestMapping("doLogin")
          public String doLogin(String username, String password) {
              // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 
              if("zhang".equals(username) && "123456".equals(password)) {
                  StpUtil.login(10001);
                  return "登录成功";
              }
              return "登录失败";
          }

          // 查询登录状态,浏览器访问:http://localhost:8081/user/isLogin
          @RequestMapping("isLogin")
          public String isLogin() {
              return "当前会话是否登录:" + StpUtil.isLogin();
          }

      }

      运行

      启动代码,从浏览器依次访问上述测试接口:

      4.4 Spring WebFlux集成Sa-Token示例

      Reactor是一种非阻塞的响应式模型,以WebFlux 为例来展示Sa-Token与Reactor响应式模型框架相整合的示例, 你可以用同样方式去对接其它 Reactor模型框架(Netty、ShenYu、SpringCloud Gateway等)

      整合示例在官方仓库的/sa-token-demo/sa-token-demo-webflux文件夹下,如遇到难点可结合源码进行测试学习

      WebFlux 用于微服务网关架构中,如果您的应用基于单体架构且非Reactor 模型

      创建项目

      在IDE中新建一个SpringBoot项目,例如:sa-token-demo-webflux

      添加依赖

      在pom.xml中添加依赖:


      dependency>
          groupId>cn.dev33groupId>
          artifactId>sa-token-reactor-spring-boot-starterartifactId>
          version>1.30.0version>
      dependency>

      创建启动类

      在项目中新建包com.pj,在此包内新建主类 SaTokenDemoApplication.java,输入以下代码:

      @SpringBootApplication
      public class SaTokenDemoApplication {
          public static void main(String[] args) throws JsonProcessingException {
              SpringApplication.run(SaTokenDemoApplication.classargs);
              System.out.println("启动成功:Sa-Token配置如下:" + SaManager.getConfig());
          }
      }

      创建全局过滤器

      新建SaTokenConfigure.java,注册Sa-Token的全局过滤器

      /**
       * [Sa-Token 权限认证] 全局配置类 
       */

      @Configuration
      public class SaTokenConfigure {
          /**
           * 注册 [Sa-Token全局过滤器] 
           */

          @Bean
          public SaReactorFilter getSaReactorFilter() {
              return new SaReactorFilter()
                      // 指定 [拦截路由]
                      .addInclude("/**")
                      // 指定 [放行路由]
                      .addExclude("/favicon.ico")
                      // 指定[认证函数]: 每次请求执行 
                      .setAuth(obj -> {
                          System.out.println("---------- sa全局认证");
                          // SaRouter.match("/test/test", () -> StpUtil.checkLogin());
                      })
                      // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 
                      .setError(e -> {
                          System.out.println("---------- sa全局异常 ");
                          return SaResult.error(e.getMessage());
                      })
                      ;
          }
      }

      你只需要按照此格式复制代码即可。

      创建测试Controller

      @RestController
      @RequestMapping("/user/")
      public class UserController {

          // 测试登录,浏览器访问:http://localhost:8081/user/doLogin?username=zhang&password=123456
          @RequestMapping("doLogin")
          public String doLogin(String username, String password) {
              // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 
              if("zhang".equals(username) && "123456".equals(password)) {
                  StpUtil.login(10001);
                  return "登录成功";
              }
              return "登录失败";
          }

          // 查询登录状态,浏览器访问:http://localhost:8081/user/isLogin
          @RequestMapping("isLogin")
          public String isLogin() {
              return "当前会话是否登录:" + StpUtil.isLogin();
          }

      }

      运行

      启动代码,从浏览器依次访问上述测试接口:

      5. 总结

      Sa-Token框架是一个轻量级的登录、鉴权框架,有利于我们开发。

      来源:blog.csdn.net/lvoelife/article/details/126533792

    • SpringBoot + 规则引擎 URule,真的很强!

      快乐分享,Java干货及时送达👇

      没有规则,不成方圆;

      一、背景

      前段时间,在做项目重构的时候,遇到很多地方需要做很多的条件判断。当然可以用很多的if-else判断去解决,但是当时也不清楚怎么回事,就想玩点别的。于是乎,就去调研了规则引擎。

      当然,市面上有很多成熟的规则引擎,功能很多,性能很好。但是,就是想玩点不一样的(大家做技术选型别这样,这个是反面教材)。最终一款URule的规则引擎吸引了我,主要还是采用浏览器可直接配置,不需要过多安装,可视化规则也做的不错。经过一系列调研,后面就把它接入了项目中,顺便记录下调研的结果。

      二、介绍

      规则引擎其实是一种组件,它可以嵌入到程序当中。将程序复杂的判断规则从业务代码中剥离出来,使得程序只需要关心自己的业务,而不需要去进行复杂的逻辑判断;简单的理解是规则接受一组输入的数据,通过预定好的规则配置,再输出一组结果。

      当然,市面上有很多成熟的规则引擎,如:Drools、Aviator、EasyRules等等。但是URule,它可以运行在Windows、Linux、Unix等各种类型的操作系统之上,采用纯浏览器的编辑模式,不需要安装工具,直接在浏览器上编辑规则和测试规则。

      当然这款规则引擎有开源和pro版本的区别,至于pro版是啥,懂的都懂,下面放个表格,了解下具体的区别

      特性 PRO版 开源版
      向导式决策集
      脚本式决策集
      决策树
      决策流
      决策表
      交叉决策表
      复杂评分卡
      文件名、项目名重构
      参数名、变量常量名重构
      Excel决策表导入
      规则集模版保存与加载
      中文项目名和文件名支持
      服务器推送知识包到客户端功能的支持
      知识包优化与压缩的支持
      客户端服务器模式下大知识包的推拉支持
      规则集中执行组的支持
      规则流中所有节点向导式条件与动作配置的支持
      循环规则多循环单元支持
      循环规则中无条件执行的支持
      导入项目自动重命名功能
      规则树构建优化
      对象查找索引支持
      规则树中短路计算的支持
      规则条件冗余计算缓存支持
      基于方案的批量场景测试功能
      知识包调用监控
      更为完善的文件读写权限控制
      知识包版本控制
      SpringBean及Java类的热部署
      技术支持

      三、安装使用

      实际使用时,有四种使用URule Pro的方式,分别是嵌入式模式、本地模式、分布式计算模式以及独立服务模式。

      但是我们这里不考虑URule Pro,咱自己整个开源版,在开源版集成springboot的基础上做一个二次开发,搜了一圈,其实就有解决方案。大致的项目模块如下:

      图片

      自己创建个空数据库,只需要在edas-rule-server服务中修改下数据库的配置,然后启动服务即可。第一次启动完成,数据库中会创建表。

      properties
      复制代码spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
      spring.datasource.driver-class-name=com.mysql.jdbc.Driver
      spring.datasource.url=jdbc:mysql://localhost:3306/urule-data?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
      spring.datasource.username=root
      spring.datasource.password=mysql

      上面说过,它是纯用浏览器进行编辑,配置规则的,只需要打开浏览器,输入地址:http://localhost:8090/urule/frame,看到这个界面,就说明启动成功了。

      图片

      四、基础概念

      3.1整体介绍

      先说下URule它的构成部分,主要是两部分:1、设计器部分 2、规则执行引擎。设计器部分主要是库文件和规则文件构成。下面看下整体的结构图

      图片

      3.2库文件

      如上图介绍的,库文件有4种,包括变量库,参数库,常量库和动作库。其实类似于Java开发的系统中的实体对象,枚举,常量以及方法。

      上面说过,规则都是可视化配置的。在配置规则的过程中,就需要引入各种已经定义好的库文件,再结合业务需求,从而配置出符合业务场景的业务规则,所以哪里都有库文件的身影。

      3.2.1变量库文件

      在业务开发中,我们会创建很多Getter和Setter的Java类,比如PO、VO、BO、DTO、POJO等等,其实这些类new对象后主要起到的作用就是数据的载体,用来传输数据。

      在URule中,变量库就是用来映射这些对象,然后可以在规则中使用,最终完成业务和规则的互动。最后上一张图,用来创建变量库

      图片

      对了,上面废话了这么多可视化配置,这才是第一次展示配置界面,惭愧惭愧。

      上图一目了然,在“库”这个菜单底下右键,然后点击添加变量库即可,最后定义自己喜欢的变量库名,当然名字只支持中文或者英文,其他字符不可用。

      图片

      创建完变量库后,就可以对变量库进行编辑,可以认为就是给POJO添加属性

      图片

      也不弯弯绕绕讲什么术语,就个人理解。图左边是创建类,其中名称是它的别名,配置规则用它代替这个类。图右边是类的属性,我这里随便写了几个,估计看了懂得都懂。

      最后在业务系统中创建对应的类,注意全限定名和配置变量库的类路径一致。

      package com.cicada;

      import com.bstek.urule.model.Label;
      import lombok.Data;

      /**
       * @author 往事如风
       * @version 1.0
       * @date 2023/3/3 15:38
       * @description
       */
      @Data
      public class Stu {

          @Label("姓名")
          private String name;

          @Label("年龄")
          private int age;

          @Label("班级")
          private String classes;
      }

      最后说下这个@Label注解,这个是由URule提供的注解,主要是描述字段的属性,跟变量库的标题一栏一致就行。听官方介绍可以通过这个注解,实现POJO属性和变量库属性映射。就是POJO写好,然后对应规则的变量库就不需要重新写,可以直接生成。反正就有这个功能,这里就直接一笔带过了。

      3.2.2常量库文件

      说到常量库,这个就可以认为是我们Java系统中的常量,枚举。比如性别,要定义枚举吧;比如对接的机构,也可以定义一个枚举吧。

      当然,类似于变量库,常量库也可以实现和系统中的枚举相互映射,这样做的好处可以避免我们手动输入,防止输入错误。创建常量库也比较简单,直接在“库”这个菜单下右键,“添加常量库”。

      创建好常量库文件后,也会出现如下页面:

      图片

      3.2.3参数库文件

      参数库,就是URule规则中的临时变量,变量的类型和数量不固定。可以认为类似于Map,实际上存储参数库的也就是个Map。

      同样的套路,直接在“库”这个菜单下右键,“添加参数库”。

      可以看到,参数库已经少了左边分类这一项,直接添加参数,选择类型就是干,相对简单了很多。“名称”这列我这里用了英文,就是Map中的key,而“标题”这列就是在配置规则时候显示用的,中文看着比较直观。

      当然还需要注意的点是,定义的名称要保证唯一,因为Map中的key是唯一的,不然就会存在覆盖的情况。

      3.2.4动作库文件

      动作库可以对配置在spring中的bean方法进行映射,然后可以在规则中直接调用这批方法。惯用套路,还是在“库”菜单下右键,点击“添加动作库”。

      图片

      然后我在系统中添加了一个类Action,然后在类上标记@Component注解,将该类交给spring的bean容器管理。该类中添加一些方法,在方法上标记@ExposeAction注解,该注解是URule定义的,说明被标记的方法都会被动作库读取到。

      package com.bstek.urule.cicada;

      import com.bstek.urule.action.ActionId;
      import com.bstek.urule.model.ExposeAction;
      import org.springframework.stereotype.Component;

      import java.text.SimpleDateFormat;
      import java.util.Date;

      /**
       * @author 往事如风
       * @version 1.0
       * @date 2023/3/10 13:59
       * @description
       */
      @Component("action")
      public class Action {

          @ActionId("Hello")
          public String hello(){
              return "hello";
          }

          @ExposeAction(value="方法1")
          public boolean evalTest(String username){
              if(username==null){
                  return false;
              }else if(username.equals("张三")){
                  return true;
              }
              return false;
          }

          @ExposeAction(value="测试Int")
          public int testInt(int a,int b){
              return a+b;
          }

          @ExposeAction(value="打印内容")
          public void printContent(String username, Date birthday){
              SimpleDateFormat sd=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
              if(birthday!=null){
                  System.out.println(username+"今年已经"+sd.format(birthday)+"岁了!");
              }else{
                  System.out.println("Hello "+username+"");
              }
          }
          
          @ExposeAction(value="打印Stu")
          public void printUser(Stu m){
              System.out.println("Hello "+m.getName()+", is age:"+m.getAge());
          }
      }

      最后在动作库页面上添加bean,“Bean Id”一列输入对应的spring bean的名称,这里输入action。然后点击操作列中的小手按钮,就会弹出刚在Action类中标记了ExposeAction注解的方法。选择一个指定的方法添加进来,最后看到方法对应的参数也会被自动加载进去。

      最后,变量库、参数库、动作库、常量库这些库文件定义好后,各种规则文件配置的时候就可以导入他们。但是一旦这些库文件被某个规则文件使用,就不要随意修改库文件了。

      我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取!

      3.3规则集

      说到规则集,顾名思义,就是配置规则了。前面定义的库文件就需要导入到规则集中去配置使用。它是使用频率最高的一个业务规则实现方式。

      规则集说的是规则的集合,由三个部分规则组成:如果、那么、否则。

      在规则集的定义的方式上,URule由向导式和脚本式两种;

      • 向导式规则集:就是在页面上通过鼠标点点点,高度的可视化配置,不是开发都能懂,这也是这个规则引擎的亮点所在。
      • 脚本式规则集:听名字就知道了,这玩意要写脚本的。拉高配置门槛,需要懂点编码的人来编写。

      3.3.1向导式规则集

      还是一样,首先新建。这次是在“决策集”菜单上右键,点击“添加向导式决策集”,这样就创建好一个规则集了。

      图片

      在配置规则前,可以先导入前面定义好的库文件。我这里导入变量库文件,页面上点击“变量库”,然后选择指定的变量库文件即可。如图所示;

      图片

      最后,可以愉快的配置规则了,向导式没什么好讲的,都是可视化界面,点点点即可。下面是我配置的一个简单的规则集;

      图片

      可以看到由三部分组成:如果、那么、否则;

      1. 如果:配置规则的条件;
      2. 那么:配置满足条件后执行的动作,一般配置变量赋值比较多
      3. 否则:配置不满足条件执行的动作

      最后,附上添加完规则后,通过代码去执行规则;

      package com.cicada;

      import cn.hutool.core.bean.BeanUtil;
      import com.Result;
      import com.bstek.urule.Utils;
      import com.bstek.urule.runtime.KnowledgePackage;
      import com.bstek.urule.runtime.KnowledgeSession;
      import com.bstek.urule.runtime.KnowledgeSessionFactory;
      import com.bstek.urule.runtime.service.KnowledgeService;
      import com.cicada.req.StuReq;
      import org.springframework.web.bind.annotation.PostMapping;
      import org.springframework.web.bind.annotation.RequestBody;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;

      import java.io.IOException;

      /**
       * @author 往事如风
       * @version 1.0
       * @date 2023/3/10 16:47
       * @description
       */
      @RestController
      @RequestMapping("/rule")
      public class RuleDataController {

          @PostMapping("/stu")
          public Result rule(@RequestBody StuReq stuReq) throws IOException {
              KnowledgeService knowledgeService = (KnowledgeService) Utils.getApplicationContext().getBean(KnowledgeService.BEAN_ID);
              KnowledgePackage knowledgePackage = knowledgeService.getKnowledge("xxx/xxx");
              KnowledgeSession knowledgeSession = KnowledgeSessionFactory.newKnowledgeSession(knowledgePackage);
              Stu stu = BeanUtil.copyProperties(stuReq, Stu.class);
              knowledgeSession.insert(stu);
              knowledgeSession.fireRules();
              return Result.success(stu.getTeacher());
          }
      }

      请求接口,最终参数符合配置的条件,返回“那么”中配置的输出结果。

      3.3.2脚本式规则集

      脚本式的规则集,各种原理都是和向导式一模一样,无非就是拉高门槛,用写脚本的方式去实现配置的规则。这里不做过多的介绍了。

      3.4决策表

      再聊下决策表,其实它就是规则集的另一种展示形式,比较相对规则集,我更喜欢用决策表去配置规则,应为它呈现的更加直观,更便于理解。但是本质和规则集没啥区别。

      也不展开过多的赘述,这里我就放一张配置过的决策表;

      图片

      3.5其他

      当然,还有其他的概念和功能,这里也不一一介绍了,因为上面说的已经是最常用的了,想了解的可以自行去了解。其他功能包括:交叉决策表、评分卡、复杂评分卡、决策树、规则流;当然,其中有些是Pro版的功能。

      我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取!

      四、运用场景

      最近在开发一期大版本的需求,其中就有个场景,具体如下;参与购买订单的用户都会有自己的一个职级,也可以说是角色。每个用户都会有三个职位:普通用户、会员、精英会员。

      然后,每个月初都会对用户进行一次晋升处理,普通用户达到要求,就会晋升为会员,会员达到要求就会晋升为精英会员。

      当然,普通用户晋升会员,会员晋升精英会员,都会有不同的规则;

      1. 普通用户->会员:3个月内帮注册人数达到3人;3个月内自己和底下团队的人,下单金额超过1万;个人的订单继续率超过80%。
      2. 会员->精英会员:3个月内帮注册人数达到6人;3个月内自己和底下团队的人,下单金额超过5万;个人的订单继续率超过90%。
      3. 不能跨级晋升,普通用户最多只能到会员,达到会员了才能晋升到精英会员。

      当然,这只是做过简化的一部分需求,我做过稍许的改动,真实的需求场景并没有这么简单。

      下面,我对这个需求做一个规则的配置,这里用一个决策表进行配置;在配置规则前,我添加一个变量库文件和常量库;

      图片

      图片

      最后,添加一个决策表,并进行规则配置;

      图片

      可以看到,表格一共五列,其中前四列是规则,最后一列是满足规则后输出的信息。这样看着就很清晰,即使并不是技术人员,也可以轻松看懂其中的规则。

      五、总结

      规则引擎对于我们的系统而言可用可不用,它可以锦上添花,帮助我们剥离出业务中需要进行大量判断的场景。但是,这种规则的剥离,需要我们开发人员对需求进行理解,在理解的基础上进行抽象概念的具化。这,也是整个编程的必经之路

      六、参考源码

      • https://gitee.com/cicadasmile/butte-java-note
      • https://gitee.com/cicadasmile/butte-flyer-parent

      来源:juejin.cn/post/7210194936276680759

      
      

    • 不愧是腾讯,问的贼细

      最近有位小伙伴被腾讯的面试题问懵了,大家来看看是怎么考察的,主要是后端的。
      • Java集合主要是hashmap实现原理。
      • 多线程问AQS源码、并发工具类源码、锁的实现原理、阻塞队列源码、线程池实现原理。
      • Jvm问内存结构和垃圾回收机制加Jvm优化参数配置。
      • Springi问ioc和aop原理,bean的生命周期。
      • Redis问数据类型、线程模型、持久化机制、主从复制原理、高可用原理、redis cluster,分布式锁、消息中间件、hash一致性算法。
      • MQ问可靠性、幂等性、可用性、持久化机制、以及优缺点和使用场景。
      • ZK问使用场景和分布式锁实现。
      • Dubbo问底层通信原理,负载均衡方式和集群容错方式和代理方式和spi机制。
      • 如何保证分布式幂等性、redis和mysql数据—致性、防止redis并发写,缓存雪崩和缓存穿透、限流、降级、熔断。
      •   项目框架,Spring、Mybatis 等框架实现原理是否熟悉?
      •   消息中间件,负载均衡、RPC 框架等技术是否有接触过?MySQL 分库分表是否做过?
      •   分布式架构设计,Redis 分布式锁是否有涉及?

        其实很多时候,面试官问的问题会和自己准备的“题库”中的问题不太一样,即使做了复盘,下次面试还是不知道该从何处下手。
      为此我专门整理一份《Java进阶学习+面试宝典》分享给大家,内容涵盖:计算机基础、Java、JVM、spring、算法、微服务、分布式、大厂面经、技术脑图等等…共1700+页 质量非常高!!!
      不管最近要不要去面试,建议大家都保存一份!!学完之后不论是 厂内晋升 还是 跳槽涨薪 都不在话下!
      内容如下:
      • 大厂面试题真题解析(38页)
      • JVM(183页)
      • 多线程(221页)
      • Mysql(216页)
      • Spring(338页)
      • Spring Boot(41页)
      • 经典面试题(35页)
      • Spring Cloud(50页)
      • Dubbo(55页)
      • Mybtis+Redis(27页)
      • Linux+网络(66页)
      • MQ+Kafka+Zookeeper(40页)
      • Netty(21页)
      • 大数据+hadoop(31页)
      • 算法(38页)
      • 设计模式+项目+高并发(41页)

      注:篇幅有限,资料已整理成文档,扫码领取!
      注意!限今天

      备注暗号:30
      一、算法(大厂必备)2023版
      含:红黑树,B+树,贪心算法,哈希分治法,七大查找算法,动态规划,一致性算法,数据结构等…
      二、设计模式(工欲善其事必先利其器)2023版
      (含:单例模式,工厂模式,抽象工厂模式,建造者模式,原型模式,适配器模式,装饰器模式,代理模式等23种设计模式…
      注:篇幅有限,资料已整理成文档,扫码领取!
      注意!仅限今天

      备注暗号:30
      三、Netty + MQ + kafka(底层原理+面试题)2023版
      (含:Netty常用场景,高性能设计,架构设计,经典面试题等…)
      四、zookeeper + Dubbo(技术干货+面试题)2023版
      (含:zookeeper集群,应用场景,分布式锁,Dubbo核心功能,集群配置,负载均衡,常见面试题等…)
      五、Mybtis + Redis (底层原理+面试题)2023版
      (含:mybtis缓存,运用原理,分页,Redis事务,主从架构,缓存,穿透,穿击,降级面试题等…)
      六、Http协议 + Linux (底层原理+面试题)2023版
      (含:TCP/IP协议详细笔记,网络层架构,三四次握手,Linux概述,磁盘,目录,文件,安全,经典面试题等…)
      注:篇幅有限,资料已整理成文档,扫码领取!
      注意!仅限今天

      备注暗号:30
      七、Mysql (底层原理 + 面试题)2023版
      (含:数据库基础,数据类型,引擎,索引,事务,锁,视图,sql语句,优化,mysql锁,面试题等…)
      八、Spring +SpringMVC +SpringBoot(底层原理 + 技术干货)
      (含:spring原理,周期,ioc原理,MVC事务,AOP原理,Boot配置,安全,监视器,面试题等…)
      九、经典面试题 + SpringCloud(大厂常见面试题)2023版
      (含:面试必考21问,SpringCloud熔断,cap原理,设计目标优缺点,版本关系等…)
      注:篇幅有限,资料已整理成文档,扫码领取!
      注意!仅限今天

      备注暗号:30
      十、Java基础 + JVM(技术干货+底层原理)2023版
      (含:Java基础,异常,NIO,HashMap,Tomcat,JVM堆栈,内存模型,调优,GC,老年代,新生代,垃圾回收,面试题等…)
      十一、多线程 (底层原理 + 技术干货)2023版
      (含:多线程基本概念,线程安全,线程出,volatile,ThreadLocal,使用场景,并发量,阻塞列队,面试题等…)
      十二、实战项目(Github爆火)2023版
      该项目是一款标准且已上线的“网约车”应用。符合我国交通部对网约车监管的技术要求。通过了交通部对网约车线上和线下能力认定。项目原型曾在杭州上线运行。
      项目中核心功能包括:账户系统,订单系统,支付系统,地图引擎,派单引擎,消息系统等 网约车核心解决方案。
      项目中完全采用微服务架构设计,应用了成熟的接口安全设计方案,采用分布式锁保证了分布式环境中的数据同步,用分布式事务解决了分布式环境中的数据一致性等。
      前置技能:Git,Maven,Spring Boot,Spring Cloud,Redis,MySql ,RabbitMQ,ActiveMQ等。
      项目架构图:
      注:篇幅有限,资料已整理成文档,扫码领取!
      注意!限今天

      备注暗号:30

    • 星标3.5k,一款国产的轻量级开源在线项目任务管理工具

      快乐分享,Java干货及时送达👇

      今天给大家推荐一个轻量级的开源在线项目任务管理工具:DooTask

      图片

      DooTask 提供各类文档协作工具、在线思维导图、在线流程图、项目管理、任务分发、即时IM,文件管理等工具。

      高效便捷的团队沟通工具

      针对项目和任务建立群组,工作问题可及时沟通,促进团队快速协作,提高团队工作效率。

      图片

      强大易用的协同创作云文档

      汇集文档、电子表格、思维笔记等多种在线工具,汇聚企业知识资源于一处,支持多人实时协同编辑,让团队协作更便捷。

      图片

      便捷易用的项目管理模板

      模版满足多种团队协作场景,同时支持自定义模版,满足团队个性化场景管理需求,可直观的查看项目的进展情况,团队协作更方便。

      图片

      清晰直观的任务日历

      通过灵活的任务日历,轻松安排每一天的日程,把任务拆解到每天,让工作目标更清晰,时间分配更合理。

      图片

      支持多平台应用

      多平台应用支持,打开客户端即可跟进项目任务进度, 同时让你在工作中每一个步骤都能拥有更高效愉悦的体验。

      图片

      感兴趣的小伙伴赶紧来试试吧:

      官方网站:

      • https://www.dootask.com/

      开源项目:

      • https://github.com/kuaifan/dootask

    • 阿里一面:MySQL 单表数据最大不要超过多少行?为什么?

      点击关注公众号,Java干货及时送达👇

      来源:https://my.oschina.net/u/4090830/blog/5559454

      1 背景

      作为在后端圈开车的多年老司机,是不是经常听到过,“mysql 单表最好不要超过 2000w”,“单表超过 2000w 就要考虑数据迁移了”,“你这个表数据都马上要到 2000w 了,难怪查询速度慢

      这些名言民语就和 “群里只讨论技术,不开车,开车速度不要超过 120 码,否则自动踢群”,只听过,没试过,哈哈。

      下面我们就把车速踩到底,干到 180 码试试…….

      2 实验

      某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” style=”color: rgb(58, 58, 58);” data-linktype=”2″>实验一把看看…

      某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” data-linktype=”2″>建一张表:

       某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">CREATE TABLE person(
        id int NOT NULL AUTO_INCREMENT PRIMARY KEY comment '主键',
        person_id tinyint not null comment '用户id',
        person_name VARCHAR(200) comment '用户名称',
        gmt_create datetime comment '创建时间',
        gmt_modified datetime comment '修改时间'
      ) comment '人员信息表';

      插入一条数据:

       某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">insert into person values(1,1,'user_1', NOW(), now());

      利用 mysql 伪列 rownum 设置伪列起始点为 1

       某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">select (@i:=@i+1) as rownum, person_name 
      from person, (select @i:=100) as init;

      set @i=1;

      某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” style=”color: rgb(58, 58, 58);” data-linktype=”2″>运行下面的 sql,连续执行 20 次,就是 2 的 20 次方约等于 100w 的数据;执行 23 次就是 2 的 23 次方约等于 800w , 如此下去即可实现千万测试数据的插入,如果不想翻倍翻倍的增加数据,而是想少量,少量的增加,有个技巧,就是在 SQL 的后面增加 where 条件,如 id > 某一个值去控制增加的数据量即可。

       某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">insert into person(id, person_id, person_name, gmt_create, gmt_modified)
      select @i:=@i+1,
        left(rand()*10,10) as person_id,
        concat('user_',@i%2048),
        date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND),
       date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)
      from person;

      某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” style=”color: rgb(58, 58, 58);” data-linktype=”2″>此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。

       某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">SET GLOBAL tmp_table_size =512*1024*1024; (512M)
      SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);

      某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” style=”color: rgb(58, 58, 58);” data-linktype=”2″>先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。

      看到这组数据似乎好像真的和标题对应,当数据达到 2000w 以后,查询时长急剧上升;难道这就是铁律吗?那下面我们就来看看这个建议值 2kw 是怎么来的?

      3 单表数量限制

      首先我们先想想数据库单表行数最大多大?

      CREATE TABLE person(
        id int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY comment '主键',
        person_id tinyint not null comment '用户id',
        person_name VARCHAR(200) comment '用户名称',
        gmt_create datetime comment '创建时间',
        gmt_modified datetime comment '修改时间'
      ) comment '人员信息表';

      看看上面的建表 sql,id 是主键,本身就是唯一的,也就是说主键的大小可以限制表的上限,如果主键声明 int 大小,也就是 32 位,那么支持 2^32-1 ~~21 亿;如果是 bigint,那就是 2^62-1 ?(36893488147419103232),难以想象这个的多大了,一般还没有到这个限制之前,可能数据库已经爆满了!!

      有人统计过,如果建表的时候,自增字段选择无符号的 bigint , 那么自增长最大值是 18446744073709551615,按照一秒新增一条记录的速度,大约什么时候能用完?

      4 表空间

      下面我们再来看看索引的结构,对了,我们下面讲内容都是基于 Innodb 引擎的,大家都知道 Innodb 的索引内部用的是 B+ 树

      这张表数据,在硬盘上存储也是类似如此的,它实际是放在一个叫 person.ibd (innodb data)的文件中,也叫做表空间;虽然数据表中,他们看起来是一条连着一条,但是实际上在文件中它被分成很多小份的数据页,而且每一份都是 16K。大概就像下面这样,当然这只是我们抽象出来的,在表空间中还有段、区、组等很多概念,但是我们需要跳出来看。

      5 页的数据结构

      因为每个页只有 16K 的大小,但是如果数据很多,那一页肯定就放不下这些数据,那数据肯定就会被分到其他的页中,所以为了把这些页关联起来,肯定就会有记录前后页地址,方便找到对应页;同时每页都是唯一的,那就会需要有一个唯一标志来标记页,就是页号;页中会记录数据所以会存在读写操作,读写操作会存在中断或者其他异常导致数据不全等,那就会需要有校验机制,所以里面还有会校验码,而读操作最重要的就是效率问题,如果按照记录一个个进行遍历,那肯定是很费劲的,所以这里面还会为数据生成对应的页目录(Page Directory); 所以实际页的内部结构像是下面这样的。

      从图中可以看出,一个 InnoDB 数据页的存储空间大致被划分成了 7 个部分,有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。

      在页的 7 个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到 User Records 部分。

      但是在一开始生成页的时候,其实并没有 User Records 这个部分,每当我们插入一条记录,都会从 Free Space 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分,当 Free Space 部分的空间全部被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。这个过程的图示如下。

      那下面就来说说,数据的查找过程,假如我们需要查找一条记录,我们可以把表空间中的每一页都加载到内存中,然后对记录挨个判断是不是我们想要的,在数据量小的时候,没啥问题,内存也可以撑;但是现实就是这么残酷,不会给你这个局面;为了解决这问题,mysql 中就有了索引的概念;大家都知道索引能够加快数据的查询,那到底是怎么个回事呢?下面我就来看看。

      6 索引的数据结构

      在 mysql 中索引的数据结构和刚刚描述的页几乎是一模一样的,而且大小也是 16K, 但是在索引页中记录的是页 (数据页,索引页) 的最小主键 id 和页号,以及在索引页中增加了层级的信息,从 0 开始往上算,所以页与页之间就有了上下层级的概念。

      看到这个图之后,是不是有点似曾相似的感觉,是不是像一棵二叉树啊,对,没错!它就是一棵树,只不过我们在这里只是简单画了三个节点,2 层结构的而已,如果数据多了,可能就会扩展到 3 层的树,这个就是我们常说的 B+ 树,最下面那一层的 page level =0, 也就是叶子节点,其余都是非叶子节点。

      看上图中,我们是单拿一个节点来看,首先它是一个非叶子节点(索引页),在它的内容区中有 id 和 页号地址两部分,这个 id 是对应页中记录的最小记录 id 值,页号地址是指向对应页的指针;而数据页与此几乎大同小异,区别在于数据页记录的是真实的行数据而不是页地址,而且 id 的也是顺序的。

      7 单表建议值

      下面我们就以 3 层,2 分叉(实际中是 M 分叉)的图例来说明一下查找一个行数据的过程。

      比如说我们需要查找一个 id=6 的行数据,因为在非叶子节点中存放的是页号和该页最小的 id,所以我们从顶层开始对比,首先看页号 10 中的目录,有 [id=1, 页号 = 20],[id=5, 页号 = 30], 说明左侧节点最小 id 为 1,右侧节点最小 id 是 5;6>5, 那按照二分法查找的规则,肯定就往右侧节点继续查找,找到页号 30 的节点后,发现这个节点还有子节点(非叶子节点),那就继续比对,同理,6>5&&6

      从上述的过程中发现,我们为了查找 id=6 的数据,总共查询了三个页,如果三个页都在磁盘中(未提前加载至内存),那么最多需要经历三次的磁盘 IO。需要注意的是,图中的页号只是个示例,实际情况下并不是连续的,在磁盘中存储也不一定是顺序的。

      至此,我们大概已经了解了表的数据是怎么个结构了,也大概知道查询数据是个怎么的过程了,这样我们也就能大概估算这样的结构能存放多少数据了。

      从上面的图解我们知道 B+ 数的叶子节点才是存在数据的,而非叶子节点是用来存放索引数据的。

      所以,同样一个 16K 的页,非叶子节点里的每条数据都指向新的页,而新的页有两种可能

      • 如果是叶子节点,那么里面就是一行行的数据
      • 如果是非叶子节点的话,那么就会继续指向新的页

      假设

      • 非叶子节点内指向其他页的数量为 x
      • 叶子节点内能容纳的数据行数为 y
      • B+ 数的层数为 z

      如下图中所示
      Total =x^(z-1) *y 也就是说总数会等于 x 的 z-1 次方 与 Y 的乘积。

      X =?

      在文章的开头已经介绍了页的结构,索引也也不例外,都会有 File Header (38 byte)、Page Header (56 Byte)、Infimum + Supermum(26 byte)、File Trailer(8byte), 再加上页目录,大概 1k 左右,我们就当做它就是 1K, 那整个页的大小是 16K, 剩下 15k 用于存数据,在索引页中主要记录的是主键与页号,主键我们假设是 Bigint (8 byte), 而页号也是固定的(4Byte), 那么索引页中的一条数据也就是 12byte; 所以 x=15*1024/12≈1280 行。

      Y=?

      叶子节点和非叶子节点的结构是一样的,同理,能放数据的空间也是 15k;但是叶子节点中存放的是真正的行数据,这个影响的因素就会多很多,比如,字段的类型,字段的数量;每行数据占用空间越大,页中所放的行数量就会越少;这边我们暂时按一条行数据 1k 来算,那一页就能存下 15 条,Y≈15。

      算到这边了,是不是心里已经有谱了啊
      根据上述的公式,Total =x^(z-1) y,已知 x=1280,y=15
      假设 B+ 树是两层,那就是 Z =2, Total = (1280 ^1 )
      15 = 19200
      假设 B+ 树是三层,那就是 Z =3, Total = (1280 ^2) *15 = 24576000 (约 2.45kw)

      哎呀,妈呀!这不是正好就是文章开头说的最大行数建议值 2000w 嘛!对的,一般 B+ 数的层级最多也就是 3 层,你试想一下,如果是 4 层,除了查询的时候磁盘 IO 次数会增加,而且这个 Total 值会是多少,大概应该是 3 百多亿吧,也不太合理,所以,3 层应该是比较合理的一个值。

      到这里难道就完了?


      我们刚刚在说 Y 的值时候假设的是 1K ,那比如我实际当行的数据占用空间不是 1K , 而是 5K, 那么单个数据页最多只能放下 3 条数据
      同样,还是按照 Z=3 的值来计算,那 Total = (1280 ^2) *3 = 4915200 (近 500w)

      所以,在保持相同的层级(相似查询性能)的情况下,在行数据大小不同的情况下,其实这个最大建议值也是不同的,而且影响查询性能的还有很多其他因素,比如,数据库版本,服务器配置,sql 的编写等等,MySQL 为了提高性能,会将表的索引装载到内存中。在 InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降,所以增加硬件配置(比如把内存当磁盘使),可能会带来立竿见影的性能提升哈。

      8 总结

      • Mysql 的表数据是以页的形式存放的,页在磁盘中不一定是连续的。
      • 页的空间是 16K, 并不是所有的空间都是用来存放数据的,会有一些固定的信息,如,页头,页尾,页码,校验码等等。
      • 在 B+ 树中,叶子节点和非叶子节点的数据结构是一样的,区别在于,叶子节点存放的是实际的行数据,而非叶子节点存放的是主键和页号。
      • 索引结构不会影响单表最大行数,2kw 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能。

      参考资料:

      • https://www.jianshu.com/p/cf5d381ef637
      • https://www.modb.pro/db/139052
      • 《MYSQL 内核:INNODB 存储引擎 卷 1》

      
      

      文章来源于互联网:阿里一面:MySQL 单表数据最大不要超过多少行?为什么?

    • 告别卡顿困扰:IDEA 性能优化设置

      在我们日常使用IDEA进行开发时,可能会遇到许多卡顿的瞬间,明明我们的机器配置也不低啊?为什么就会一直卡顿呢?

      原来这是因为IDEA软件在我们安装的时候就设置了默认的内存使用上限(通常很小),这就是造成我们使用IDEA时卡顿的根本原因。

      比如我这台电脑,明明是16GB的运行内存,但是IDEA默认给我分配的使用上限是1GB,当我运行大量代码时自然而然的就会产生卡顿。

      我们可以通过显示内存使用情况来查看当前项目占用的内存大小。

      可以看到当前我的程序占用了690MB的内存,而上限是1024MB(在性能突发时完全承受不住)。我们可以通过进入IDEA的设置来更改这些配置。

      具体步骤:(三步)

      1.选择顶部导航栏中的Help,然后点击Edit Custom VM Options(自定义虚拟机内存)。

      2.接下来我们将会看到这个界面:

      -Xmx1024m    // 最大内存上限为:1024MB(1GB)
      -Xms256m     // 初始内存分配大小为:256MB
      -XX:ReservedCodeCacheSize=128m    //代码缓冲区大小:128MB
      -XX:+UseG1GC
      

      我们对其进行适当的修改(具体根据个人电脑配置),并保存文件:

      -Xmx4096m
      -Xms4096m
      -XX:ReservedCodeCacheSize=256m
      -XX:+UseG1GC
      

      3. 缓存清理(使新配置生效):

      选择 File ---> Invalidate Caches(清理无效的缓存)

      保持默认的选项,选择确定清理缓存并重启IDEA (结束)。

      好了,最后我们来看一下重启之后的IDEA效果~没错,很流畅

      另外说一句,JetBrains系列的产品都可以通过这种设置来解决卡顿的问题。没错,包括PyCharm、Android Studio、WebStorm等,其余参数说明如下:

      参数说明:

      -server:一定要作为第一个参数,在多个CPU时性能佳
      -Xms:初始Heap大小,使用的最小内存,cpu性能高时此值应设的大一些
      -Xmx:java heap最大值,使用的最大内存
      -XX:PermSize:设定内存的永久保存区域
      -XX:MaxPermSize:设定最大内存的永久保存区域
      -XX:MaxNewSize:
      +XX:AggressiveHeap 使 Xms 失去意义。
      -Xss:每个线程的Stack大小
      -verbose:gc 现实垃圾收集信息
      -Xloggc:gc.log 指定垃圾收集日志文件
      -Xmn:young generation的heap大小,一般设置为Xmx的3、4分之一
      -XX:+UseParNewGC :缩短minor收集的时间
      -XX:+UseConcMarkSweepGC :缩短major收集的时间
      

      提示:此选项在Heap Size 比较大而且Major收集时间较长的情况下使用更合适。

      作者:ThinkStu

      来源:blog.csdn.net/qq_35760825/article/

      details/123325533