<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0"><channel><title>xiaoming728</title><link>https://xiaoming728.com</link><atom:link href="https://xiaoming728.com/rss.xml" rel="self" type="application/rss+xml"/><description>个人博客</description><generator>Halo v2.20.12</generator><language>zh-cn</language><image><url>https://xiaoming728.oss-cn-hangzhou.aliyuncs.com/xiaoming728/a41b1afcff08f9a63c0448de101878d7.jpg</url><title>xiaoming728</title><link>https://xiaoming728.com</link></image><lastBuildDate>Sat, 18 Apr 2026 05:36:24 GMT</lastBuildDate><item><title><![CDATA[Claude Code 公益站分享]]></title><link>https://xiaoming728.com/archives/claude-code-gong-yi-zhan-fen-xiang</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Claude%20Code%20%E5%85%AC%E7%9B%8A%E7%AB%99%E5%88%86%E4%BA%AB&amp;url=/archives/claude-code-gong-yi-zhan-fen-xiang" width="1" height="1" alt="" style="opacity:0;">
<p style=""><strong>公益站分享</strong></p>
<p style=""><strong>注册就有 100$ 额度，用我的邀请链接 再额外赠送 50$</strong><br><a href="https://anyrouter.top/register?aff=PKVb" target="_blank">https://anyrouter.top/register?aff=PKVb</a></p>
<p style=""><strong>这个公益站稳定一些， 100$ 额度，用我的邀请链接 再额外赠送 25$</strong></p>
<p style=""><a href="https://api.codemirror.codes/register?aff=xaXt" target="_self">https://api.codemirror.codes/register?aff=xaXt</a></p>
<p style=""></p>
<p style=""><strong>付费站分享</strong></p>
<p style=""><a href="https://www.88code.org/register?ref=KPABKZ" target="_blank">https://www.88code.org/register?ref=KPABKZ</a></p>
<p style=""><a href="https://privnode.com/register?aff=PT4s" target="_blank">https://privnode.com/register?aff=PT4s</a></p>]]></description><guid isPermaLink="false">/archives/claude-code-gong-yi-zhan-fen-xiang</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fanthropic-claude-code.webp&amp;size=m" type="image/jpeg" length="0"/><pubDate>Tue, 14 Oct 2025 07:45:50 GMT</pubDate></item><item><title><![CDATA[Docker 部署 MariaDB 指南]]></title><link>https://xiaoming728.com/archives/docker-bu-shu-mariadb-zhi-nan</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%20%E9%83%A8%E7%BD%B2%20MariaDB%20%E6%8C%87%E5%8D%97&amp;url=/archives/docker-bu-shu-mariadb-zhi-nan" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">本文档基于常见的 Docker 部署实践（拉取镜像 -&gt; 运行一次获取配置 -&gt; 挂载配置与数据目录）并扩展为适用于生产环境的 MariaDB 部署与调优建议。</p>
</blockquote>
<blockquote>
 <p style="">参考文章：Docker 环境安装 Mysql。</p>
</blockquote>
<hr>
<h2 style="" id="%E7%9B%AE%E5%BD%95">目录</h2>
<ol>
 <li>
  <p style="">目标与适用场景</p></li>
 <li>
  <p style="">前提条件</p></li>
 <li>
  <p style="">拉取镜像</p></li>
 <li>
  <p style="">初次运行（生成默认配置 &amp; 初始化数据目录）</p></li>
 <li>
  <p style="">拷贝并自定义 <code>my.cnf</code></p></li>
 <li>
  <p style="">使用 Docker Run 持久化部署（数据与配置卷）</p></li>
 <li>
  <p style="">推荐的 <code>docker-compose.yml</code> 示例</p></li>
 <li>
  <p style="">性能与大表（千万/亿级）调优建议</p></li>
 <li>
  <p style="">备份 / 恢复方案</p></li>
 <li>
  <p style="">安全性与网络配置</p></li>
 <li>
  <p style="">监控与日志</p></li>
 <li>
  <p style="">常见问题与排查要点</p></li>
</ol>
<hr>
<h2 style="" id="1.-%E7%9B%AE%E6%A0%87%E4%B8%8E%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF">1. 目标与适用场景</h2>
<p style="">本文档适用于希望用 <strong>Docker</strong> 快速部署 <strong>MariaDB</strong> 的开发/测试及小到中型生产环境。重点考虑：</p>
<ul>
 <li>
  <p style="">数据目录持久化</p></li>
 <li>
  <p style="">可定制配置（字符集、innodb 调优）</p></li>
 <li>
  <p style="">针对单表大数据量（千万/亿行）的查询性能优化建议</p></li>
</ul>
<blockquote>
 <p style="">若需要线性扩展或分布式能力（例如 sharding / HTAP），请考虑 TiDB / Vitess 等分布式方案。</p>
</blockquote>
<div style="overflow-x: auto; overflow-y: hidden;">
 <table style="width: 992px">
  <colgroup>
   <col style="width: 100px">
   <col style="width: 198px">
   <col style="width: 694px">
  </colgroup>
  <tbody>
   <tr style="height: 60px;">
    <th colspan="1" rowspan="1" colwidth="100">
     <p style="">MySQL 版本</p></th>
    <th colspan="1" rowspan="1" colwidth="198">
     <p style="">MariaDB 对应大致版本</p></th>
    <th colspan="1" rowspan="1" colwidth="694">
     <p style="">说明</p></th>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" rowspan="1" colwidth="100">
     <p style="">MySQL 5.5</p></td>
    <td colspan="1" rowspan="1" colwidth="198">
     <p style="">MariaDB 5.5</p></td>
    <td colspan="1" rowspan="1" colwidth="694">
     <p style="">基本兼容，社区广泛使用</p></td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" rowspan="1" colwidth="100">
     <p style="">MySQL 5.6</p></td>
    <td colspan="1" rowspan="1" colwidth="198">
     <p style="">MariaDB 10.0</p></td>
    <td colspan="1" rowspan="1" colwidth="694">
     <p style="">功能增强，InnoDB 改进</p></td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" rowspan="1" colwidth="100">
     <p style="">MySQL 5.7</p></td>
    <td colspan="1" rowspan="1" colwidth="198">
     <p style="">MariaDB 10.2</p></td>
    <td colspan="1" rowspan="1" colwidth="694">
     <p style="">JSON 支持、虚拟列等</p></td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" rowspan="1" colwidth="100">
     <p style="">MySQL 8.0</p></td>
    <td colspan="1" rowspan="1" colwidth="198">
     <p style="">MariaDB 10.3~10.11</p></td>
    <td colspan="1" rowspan="1" colwidth="694">
     <p style="">MariaDB 10.3+ 引入窗口函数、CTE、JSON 函数等，功能与 MySQL 8.0 接近，但不是完全一致</p></td>
   </tr>
  </tbody>
 </table>
</div>
<h2 style="" id="2.-%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6">2. 前提条件</h2>
<ul>
 <li>
  <p style="">已安装 Docker Engine（建议 20.x+）和 docker-compose（v2+）</p></li>
 <li>
  <p style="">主机有稳定磁盘（SSD 推荐）和足够内存（建议至少 8GB）</p></li>
 <li>
  <p style="">对 MariaDB 基本配置（<code>my.cnf</code>）和 SQL 执行计划（<code>EXPLAIN</code>）有基本认识</p></li>
</ul>
<h2 style="" id="3.-%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">3. 拉取镜像</h2>
<p style="">以 MariaDB 官方镜像为例：</p>
<pre><code class="language-bash"># 列出可用镜像
docker search mariadb

# 拉取指定版本（示例：10.11）
docker pull mariadb:10.11
</code></pre>
<p style="">选择固定版本以保证可重复部署与回滚能力。</p>
<h2 style="" id="4.-%E5%88%9D%E6%AC%A1%E8%BF%90%E8%A1%8C%EF%BC%88%E7%94%9F%E6%88%90%E9%BB%98%E8%AE%A4%E9%85%8D%E7%BD%AE-%26-%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E6%8D%AE%E7%9B%AE%E5%BD%95%EF%BC%89">4. 初次运行（生成默认配置 &amp; 初始化数据目录）</h2>
<p style="">建议第一步先用临时容器初始化数据目录并导出默认配置作为模版：</p>
<pre><code class="language-bash">docker run --name mariadb-temp -e MYSQL_ROOT_PASSWORD='StrongRootPass123!' -d mariadb:10.11

# 等待容器初始化数据库（查看日志）
docker logs -f mariadb-temp

# 将容器内的 /etc/mysql 或 /etc/mysql/mariadb.conf.d 拷贝到主机
mkdir -p /opt/docker/mariadb/{data,conf}
docker cp mariadb-temp:/etc/mysql /opt/docker/mariadb/conf

# 停止并删除临时容器
docker stop mariadb-temp &amp;&amp; docker rm mariadb-temp

# 将容器内的数据目录拷贝到宿主机（如果已有数据）
# docker cp mariadb-temp:/var/lib/mysql /opt/docker/mariadb/data
</code></pre>
<blockquote>
 <p style="">目的：获取官方默认配置，便于以后在主机上编辑 <code>my.cnf</code> 并通过挂载生效。</p>
</blockquote>
<h2 style="" id="5.-%E6%8B%B7%E8%B4%9D%E5%B9%B6%E8%87%AA%E5%AE%9A%E4%B9%89-my.cnf">5. 拷贝并自定义 <code>my.cnf</code></h2>
<p style="">把刚才拷贝出的配置目录下的 <code>my.cnf</code>（或 <code>mariadb.conf.d</code> 下文件）做为基础，调整为适合大表与查询密集场景的配置。下面给出推荐项（示例，仅供参考，请结合实际硬件调整）：</p>
<pre><code class="language-ini">[mysqld]
user=mysql
bind-address=0.0.0.0
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
skip-name-resolve = 1

# InnoDB
innodb_buffer_pool_size = 6G         # 根据内存分配，通常 = 60%-80% 可用内存
innodb_buffer_pool_instances = 4
innodb_file_per_table = 1
innodb_log_file_size = 1G
innodb_flush_log_at_trx_commit = 2  # 权衡性能与持久性
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000

# 临时表/排序/并发
tmp_table_size = 256M
max_heap_table_size = 256M
sort_buffer_size = 4M
join_buffer_size = 8M

# 连接
max_connections = 500
table_open_cache = 4000
open_files_limit = 65535

# 日志与慢查询
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_error = /var/log/mysql/error.log

# 其他
query_cache_type = 0
query_cache_size = 0
</code></pre>
<p style=""><strong>说明</strong>：</p>
<ul>
 <li>
  <p style=""><code>innodb_buffer_pool_size</code> 对查询性能决定性影响最大；单表很大时尽量将热点数据放到 buffer pool。</p></li>
 <li>
  <p style=""><code>innodb_file_per_table=1</code> 便于表级别管理与压缩。</p></li>
 <li>
  <p style=""><code>innodb_flush_log_at_trx_commit=2</code> 可以显著提升写性能，但在断电时可能会丢失最后一秒的数据（权衡可接受时使用）。</p></li>
</ul>
<h2 style="" id="6.-%E4%BD%BF%E7%94%A8-docker-run-%E6%8C%81%E4%B9%85%E5%8C%96%E9%83%A8%E7%BD%B2%EF%BC%88%E6%95%B0%E6%8D%AE%E4%B8%8E%E9%85%8D%E7%BD%AE%E5%8D%B7%EF%BC%89">6. 使用 Docker Run 持久化部署（数据与配置卷）</h2>
<p style="">下面给出一个生产可用的 <code>docker run</code> 示例，包含主机目录挂载与重启策略：</p>
<pre><code class="language-bash">docker run -d \
  --name mariadb \
  -p 3306:3306 \
  -v /opt/docker/mariadb/data:/var/lib/mysql \
  -v /opt/docker/mariadb/conf:/etc/mysql \
  -v /opt/docker/mariadb/logs:/var/log/mysql \
  -e MYSQL_ROOT_PASSWORD='StrongRootPass123!' \
  -e TZ='Asia/Shanghai' \
  --restart=always \
  mariadb:10.11 \
  --character-set-server=utf8mb4 \
  --collation-server=utf8mb4_general_ci
</code></pre>
<p style=""><strong>注意</strong>：第一次使用宿主机空目录作为 <code>/var/lib/mysql</code> 时，容器会自动初始化数据；若之前有残留数据请确保权限（UID/GID）一致。</p>
<h2 style="" id="7.-%E6%8E%A8%E8%8D%90%E7%9A%84-docker-compose.yml-%E7%A4%BA%E4%BE%8B">7. 推荐的 <code>docker-compose.yml</code> 示例</h2>
<p style="">生产或多容器场景建议使用 docker-compose（便于扩展、备份/恢复）：</p>
<pre><code class="language-yaml">version: '3.8'
services:
  mariadb:
    image: mariadb:10.11
    container_name: mariadb
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=StrongRootPass123!
      - TZ=Asia/Shanghai
    volumes:
      - /opt/docker/mariadb/data:/var/lib/mysql
      - /opt/docker/mariadb/conf:/etc/mysql
      - /opt/docker/mariadb/logs:/var/log/mysql
    restart: always
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 8G
</code></pre>
<h2 style="" id="8.-%E6%80%A7%E8%83%BD%E4%B8%8E%E5%A4%A7%E8%A1%A8%EF%BC%88%E5%8D%83%E4%B8%87%2F%E4%BA%BF%E7%BA%A7%EF%BC%89%E8%B0%83%E4%BC%98%E5%BB%BA%E8%AE%AE">8. 性能与大表（千万/亿级）调优建议</h2>
<p style="">针对单表很大、以查询为主的场景，除了上面的 <code>my.cnf</code> 调整，还应考虑如下策略：</p>
<h3 style="" id="8.1-%E7%B4%A2%E5%BC%95%E7%AD%96%E7%95%A5">8.1 索引策略</h3>
<ul>
 <li>
  <p style=""><strong>合理建索引</strong>：覆盖索引优先，避免全表扫描。使用联合索引时注意索引前缀原则（leftmost）。</p></li>
 <li>
  <p style=""><strong>避免冗余索引</strong>：过多索引会影响写入和占用空间。定期审查索引使用情况（<code>EXPLAIN</code>、<code>performance_schema</code>）。</p></li>
 <li>
  <p style=""><strong>分区（Partitioning）</strong>：对历史数据或按时间范围查询频繁的表使用 RANGE 或 LIST 分区，减小单次扫描数据量。</p></li>
</ul>
<h3 style="" id="8.2-%E8%A1%A8%E8%AE%BE%E8%AE%A1">8.2 表设计</h3>
<ul>
 <li>
  <p style=""><strong>垂直拆表/水平拆分</strong>：当单表列非常多时考虑垂直拆表；当单表行数影响性能时考虑水平拆分（sharding）。</p></li>
 <li>
  <p style=""><strong>归档冷数据</strong>：将不常访问的历史数据迁移到归档库或冷存储（例如单独实例或对象存储 + MyRocks）。</p></li>
</ul>
<h3 style="" id="8.3-%E6%9F%A5%E8%AF%A2%E4%BC%98%E5%8C%96">8.3 查询优化</h3>
<ul>
 <li>
  <p style="">用 <code>EXPLAIN</code> 分析慢查询，避免 <code>SELECT *</code>、过多子查询和不走索引的 <code>JOIN</code>。</p></li>
 <li>
  <p style="">对大范围 <code>ORDER BY</code> 或 <code>GROUP BY</code> 使用合适索引或预聚合表。</p></li>
 <li>
  <p style="">利用批量分页（<code>WHERE id &gt; last_id LIMIT N</code>）替代 <code>OFFSET</code>分页。</p></li>
</ul>
<h3 style="" id="8.4-%E7%A1%AC%E4%BB%B6%E4%B8%8E%E7%B3%BB%E7%BB%9F%E5%8F%82%E6%95%B0">8.4 硬件与系统参数</h3>
<ul>
 <li>
  <p style="">SSD（NVMe 更佳）可显著提升 IO 性能。</p></li>
 <li>
  <p style="">提高系统 <code>vm.swappiness=1</code>，避免频繁 swap。</p></li>
 <li>
  <p style="">为 MariaDB 容器预留足够内存与 CPU，避免与 host 上其他容器争抢资源。</p></li>
</ul>
<h3 style="" id="8.5-%E4%BD%BF%E7%94%A8%E6%9F%A5%E8%AF%A2%E7%BC%93%E5%AD%98-%2F-%E7%BC%93%E5%AD%98%E5%B1%82">8.5 使用查询缓存 / 缓存层</h3>
<ul>
 <li>
  <p style="">MariaDB 的 query cache 在高并发写场景中并不推荐启用（容易锁竞争）。推荐使用 Redis / Varnish 等外部缓存层缓存热点数据或结果。</p></li>
</ul>
<h2 style="" id="9.-%E5%A4%87%E4%BB%BD-%2F-%E6%81%A2%E5%A4%8D%E6%96%B9%E6%A1%88">9. 备份 / 恢复方案</h2>
<p style=""><strong>9.1 逻辑备份（mysqldump）</strong></p>
<pre><code class="language-bash">docker exec -i mariadb mysqldump -u root -p'password' --single-transaction --routines --events --databases mydb &gt; mydb.sql
</code></pre>
<p style=""><strong>9.2 物理备份（xtrabackup / mariabackup）</strong></p>
<ul>
 <li>
  <p style="">使用 Percona XtraBackup 或 MariaDB 官方的 <code>mariabackup</code> 做热备份，适合大数据量快速恢复。</p></li>
 <li>
  <p style="">示例（容器内运行 mariabackup）：</p></li>
</ul>
<pre><code class="language-bash"># 在宿主机安装 mariabackup 或将备份工具打包到容器
docker exec mariadb mariabackup --backup --target-dir=/backupdir --user=root --password='password'
</code></pre>
<p style=""><strong>9.3 备份存储与策略</strong></p>
<ul>
 <li>
  <p style="">备份文件建议上传到对象存储（S3/MinIO）并设置生命周期策略。</p></li>
 <li>
  <p style="">建议保留：每日备份 7 天、周备份 4 周、月备份 12 个月。</p></li>
</ul>
<h2 style="" id="10.-%E5%AE%89%E5%85%A8%E6%80%A7%E4%B8%8E%E7%BD%91%E7%BB%9C%E9%85%8D%E7%BD%AE">10. 安全性与网络配置</h2>
<ul>
 <li>
  <p style="">不要把数据库端口暴露到公网，使用私有网络或 VPN。若必须暴露，请用防火墙（安全组）限制访问来源。</p></li>
 <li>
  <p style="">强制使用强密码并关闭匿名账户。</p></li>
 <li>
  <p style="">定期更新镜像并修补安全漏洞。</p></li>
 <li>
  <p style="">启用 TLS 加密（<code>--ssl-ca</code>、<code>--ssl-cert</code>、<code>--ssl-key</code>）以保护传输层数据。</p></li>
</ul>
<h2 style="" id="11.-%E7%9B%91%E6%8E%A7%E4%B8%8E%E6%97%A5%E5%BF%97">11. 监控与日志</h2>
<ul>
 <li>
  <p style="">建议接入 Prometheus + Grafana 或使用 Percona Monitoring and Management（PMM）来监控：</p>
  <ul>
   <li>
    <p style="">QPS / TPS、慢查询数、Innodb Buffer Pool 命中率、磁盘 IO、连接数等。</p></li>
  </ul></li>
 <li>
  <p style="">打开慢查询日志并定期分析（<code>pt-query-digest</code>）。</p></li>
</ul>
<h2 style="" id="12.-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E6%8E%92%E6%9F%A5%E8%A6%81%E7%82%B9">12. 常见问题与排查要点</h2>
<ul>
 <li>
  <p style="">容器无法启动：检查 <code>/var/lib/mysql</code> 目录权限（UID/GID）、日志文件（<code>/var/log/mysql/error.log</code>）。</p></li>
 <li>
  <p style="">性能下降：排查慢查询、Buffer Pool 命中率、IO 阻塞、swap 使用。</p></li>
 <li>
  <p style="">数据目录损坏：从最近备份恢复或用 <code>--innodb_force_recovery</code> 参数尝试导出数据（谨慎使用）。</p></li>
</ul>
<hr>
<h2 style="" id="%E9%99%84%E5%BD%95%EF%BC%9A%E5%BF%AB%E9%80%9F%E6%93%8D%E4%BD%9C%E5%91%BD%E4%BB%A4%E6%B1%87%E6%80%BB">附录：快速操作命令汇总</h2>
<pre><code class="language-bash"># 查看容器日志
docker logs -f mariadb

# 进入容器
docker exec -it mariadb bash

# 备份示例
docker exec -i mariadb mysqldump -u root -p'password' --single-transaction mydb &gt; mydb.sql

# 恢复示例
cat mydb.sql | docker exec -i mariadb mysql -u root -p'password'
</code></pre>
<hr>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/docker-bu-shu-mariadb-zhi-nan</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmariadb.png&amp;size=m" type="image/jpeg" length="0"/><pubDate>Thu, 18 Sep 2025 07:59:59 GMT</pubDate></item><item><title><![CDATA[阿里云 ICP 代备案管理审核时间]]></title><link>https://xiaoming728.com/archives/a-li-yun-icp-dai-bei-an-guan-li-shen-he-shi-jian</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E9%98%BF%E9%87%8C%E4%BA%91%20ICP%20%E4%BB%A3%E5%A4%87%E6%A1%88%E7%AE%A1%E7%90%86%E5%AE%A1%E6%A0%B8%E6%97%B6%E9%97%B4&amp;url=/archives/a-li-yun-icp-dai-bei-an-guan-li-shen-he-shi-jian" width="1" height="1" alt="" style="opacity:0;">
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-hizu.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>]]></description><guid isPermaLink="false">/archives/a-li-yun-icp-dai-bei-an-guan-li-shen-he-shi-jian</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1729837448367-9760bfab80e0%3Fixid%3DM3w1NDIyMzN8MHwxfHNlYXJjaHw3fHxBbGliYWJhfGVufDB8fHx8MTc1MzA3NzA0Mnww%26ixlib%3Drb-4.1.0&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Mon, 21 Jul 2025 05:51:20 GMT</pubDate></item><item><title><![CDATA[CloudFront 配置与分析]]></title><link>https://xiaoming728.com/archives/cloudfront-pei-zhi-yu-fen-xi</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=CloudFront%20%E9%85%8D%E7%BD%AE%E4%B8%8E%E5%88%86%E6%9E%90&amp;url=/archives/cloudfront-pei-zhi-yu-fen-xi" width="1" height="1" alt="" style="opacity:0;">
<p style="">作为云产品开发，玩好云产品、理解云产品的底层逻辑，也是重要功课之一。</p>
<p style="">本系列对 Amazon CloudFront 产品做一下基础配置体验与使用分析。</p>
<h3 style="" id="%E5%A4%AA%E9%95%BF%E4%B8%8D%E7%9C%8B">太长不看</h3>
<ul>
 <li>
  <p style="">CloudFront 是什么</p></li>
 <li>
  <p style="">CDN 原理与解决的问题</p></li>
 <li>
  <p style="">CloudFront 基础配置流程</p></li>
 <li>
  <p style="">tcpdump 抓包与分析</p></li>
 <li>
  <p style="">结语</p></li>
</ul>
<h3 style="" id="01%2Fcloudfront-%E6%98%AF%E4%BB%80%E4%B9%88">01/CloudFront 是什么</h3>
<p style="">这里摘录一段 Amazon CloudFront 官网说明。CloudFront 是一项网络服务，它可以加快向用户分发静态和动态网页内容的速度，例如 html、css、js、image 文件。它通过一个遍布全球的数据中心网络（称为边缘节点）来传递您的内容。</p>
<p style="">当用户请求您通过 CloudFront 提供的内容时，请求会被路由到提供最低延迟（时间延迟）的边缘节点，以便以最佳性能交付内容。</p>
<p style="">CloudFront 直译过来是 “云前端”，它是一个 CDN 产品。</p>
<h3 style="" id="02%2Fcdn-%E5%8E%9F%E7%90%86%E4%B8%8E%E8%A7%A3%E5%86%B3%E7%9A%84%E9%97%AE%E9%A2%98">02/CDN 原理与解决的问题</h3>
<p style=""><strong>原理</strong></p>
<p style="">关于 CDN 原理，下述有一张比较形象的图用于解释（外部引用）</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-rbqi.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">从上述流程图可知：</p>
<ul>
 <li>
  <p style="">用户发起请求：用户向 http://www.test.com/1.jpg 请求资源。</p></li>
 <li>
  <p style="">DNS 解析：浏览器向 DNS 服务器发起域名解析，域名解析发现配置 CNAME 到 CDN 的调度域名，DNS 递归查询到 CDN 调度系统。</p></li>
 <li>
  <p style="">调度决策：调度服务会将最佳接入节点（边缘节点）IP 返回。</p></li>
 <li>
  <p style="">缓存判断：浏览器向 IP 发起请 1.jpg 访问请求，此时进入 CDN 接入节点，接入节点会检查其缓存是否有用户请求内容。</p></li>
 <li>
  <p style="">缓存命中：如果内容存在于缓存中，接入节点直接将内容返回给用户。</p></li>
 <li>
  <p style="">缓存未命中：如果内容不在缓存中，接入节点向源服务器发起请求，获取内容（其中可能会经过多层中间源节点，这里涉及请求收敛策略，进一步降低源站压力）。</p></li>
 <li>
  <p style="">缓存并传输内容：接入节点将从源服务器获取的内容存储在缓存中，并将内容传输给用户。</p></li>
 <li>
  <p style="">浏览器渲染：用户设备接收到内容，浏览器开始渲染页面。</p></li>
</ul>
<p style=""><strong>解决的问题</strong></p>
<p style="">概要的说，CDN 服务是为了降低服务提供方成本（资源成本、运营成本），提高用户体验。</p>
<p style="">因为其能通过大量就近接入节点，提供快速请求响应和资源缓存，并提供访问控制、边缘计算、安全等增值能力。</p>
<p style="">费用案例：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xstg.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-gtxa.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-bozp.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">以 100G 流量为例月消费，优惠后大约为 96.89 人民币，这通常比带宽费用要实惠，且比非 CDN 方案投入去运营服务器和带宽人工、技术成本要低，只需做一些配置接入即可。</p>
<h3 style="" id="03%2Fcloudfront-%E5%9F%BA%E7%A1%80%E9%85%8D%E7%BD%AE%E6%B5%81%E7%A8%8B">03/CloudFront 基础配置流程</h3>
<p style="">创建分配</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xqnf.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">源站配置</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-tjad.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">缓存配置</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-lcno.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-dsbr.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">函数、WAF、备用域名等（都按默认不配置）</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-prxh.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ittx.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">配置完毕后，分配了一个域名 d37z7ecg72nt7t.cloudfront.net（本篇直接使用该域名，后续篇章再配置自定义域名）</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-pipw.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<h3 style="" id="04%2Ftcpdump-%E6%8A%93%E5%8C%85%E4%B8%8E%E5%88%86%E6%9E%90">04/tcpdump 抓包与分析</h3>
<p style="">登录源站 sg.lukachen.work 所在服务器，抓包并写入 test.pcap 文件（把网卡出入的包全抓了，用 wireshark 再做过滤重组。注意！抓包会消耗大量 CPU 和硬盘资源，如果在现网服务器，需在负载低峰或经过较为合理的过滤参数与评估决策后执行）</p>
<pre><code>tcpdump -i eth0 -w test.pcap</code></pre>
<p style="">本地浏览器访问资源（也可以用 curl），发现响应 404，翻车了。。。排查原因</p>
<pre><code>curl https://d37z7ecg72nt7t.cloudfront.net/1.txt</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-osgm.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">登录服务器，终止抓包，并将抓包文件发送到本地 wireshark 分析</p>
<pre><code>sz test.pcap -y</code></pre>
<p style="">wireshark 包定位，1.txt 关键字过滤并使用 Follow TCP Stream 进行 TCP 包重组</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-dlot.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">重组后可看到，请求头，通过分析根因是请求头中的 Host，我服务器并没有配</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ipix.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">去配置上，重载 nginx 配置</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-wyas.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">再请求，通啦！</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-broh.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">重复上述抓包动作，再看一下相关头部信息</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-llji.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<h3 style="" id="05%2F%E7%BB%93%E8%AF%AD">05/结语</h3>
<p style="">好了，上述就是本次开篇的内容，并涉猎了 tcpdump 服务端抓包与 wireshark 重组分析。</p>
<p style="">CDN 是现代互联网基础设施的重要组成部分，尤其对于需要全球分发内容的大型网站和服务来说，CDN 是提高性能和用户满意度的关键技术。</p>
<p style="">在本章中，我们已经探讨了 CloudFront 基本概念、工作原理和基础配置。</p>
<p style="">在接后续章节中，我们将更深入了解 CloudFront 每个配置项的用法和抓包分析，进一步探讨如何针对不同的业务需求进行优化，并通过测试案例展示。</p>
<p style="">PS</p>
<p style="">加速原理图引用 https://cloud.tencent.com/document/product/228/2939</p>
<blockquote>
 <p style="">https://blog.csdn.net/weixin_48332026/article/details/143205598</p>
 <p style="">作者：LukaChen</p>
 <p style="">本文链接：CloudFront 配置与分析：开篇 - LukaChen Blog</p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/cloudfront-pei-zhi-yu-fen-xi</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FAWS-Blog-images-2.png&amp;size=m" type="image/jpeg" length="0"/><category>AWS</category><pubDate>Thu, 19 Jun 2025 06:02:58 GMT</pubDate></item><item><title><![CDATA[Docker 启停 Pm2 项目]]></title><link>https://xiaoming728.com/archives/docker-qi-ting-pm2</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%20%E5%90%AF%E5%81%9C%20Pm2%20%E9%A1%B9%E7%9B%AE&amp;url=/archives/docker-qi-ting-pm2" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E4%BD%BF%E7%94%A8-git-%E6%8B%89%E5%8F%96%E9%A1%B9%E7%9B%AE%E4%BB%A3%E7%A0%81%E5%88%B0%E6%9C%AC%E5%9C%B0">使用 GIt 拉取项目代码到本地</h3>
<pre><code>yum install -y git</code></pre>
<pre><code>git clone https://xxxxx.git</code></pre>
<pre><code>git branch -r</code></pre>
<h4 style="" id="%E4%BA%8C%E9%80%89%E4%B8%80">二选一</h4>
<pre><code>git checkout 分支名</code></pre>
<pre><code>git checkout -b 本地分支名 origin/远程分支名</code></pre>
<h3 style="" id="dockerfile">Dockerfile</h3>
<pre><code># 使用 Node.js 官方镜像作为基础镜像
FROM node:18.20.3 as build-stage
# 设置npm镜像源
RUN npm config set registry https://registry.npmmirror.com
# 创建工作目录并设置工作目录
RUN mkdir -p /app
WORKDIR /app
# 复制项目文件和目录到容器中
COPY . /app
# 全局安装 pm2 和 pnpm
RUN npm install pm2 -g
RUN npm install pnpm -g
# 安装依赖项并构建应用程序
RUN pnpm install &amp;&amp; \
    pnpm build
# 清理node_modules目录
RUN rm -rf ./node_modules
# 指定容器启动时执行的命令
CMD ["pm2-runtime", "ecosystem.config.cjs"]</code></pre>
<h3 style="" id="%E5%90%AF%E5%8A%A8%E8%84%9A%E6%9C%AC">启动脚本</h3>
<pre><code># start.sh 启动脚本
#!/bin/bash
# 切换到项目源码目录
cd /opt/system/xxxx
# 定义镜像和容器名称
IMAGE_NAME="xxxx"
CONTAINER_NAME="xxxx"
# 检查镜像是否已经存在，如果不存在则构建镜像
if [[ "$(docker images -q $IMAGE_NAME 2&gt; /dev/null)" == "" ]]; then
  echo "构建 Docker 镜像 $IMAGE_NAME ..."
  docker build -t $IMAGE_NAME .
fi
# 检查容器是否已经在运行
RUNNING=$(docker inspect --format="{{.State.Running}}" $CONTAINER_NAME 2&gt;/dev/null)
if [ "$RUNNING" == "true" ]; then
  echo "容器 $CONTAINER_NAME 已经在运行."
else
  # 如果容器不存在或者停止了，启动容器
  echo "启动 Docker 容器 $CONTAINER_NAME ..."
  docker run -d --name $CONTAINER_NAME -p 8400:8400 $IMAGE_NAME
fi</code></pre>
<h3 style="" id="%E5%81%9C%E6%AD%A2%E8%84%9A%E6%9C%AC">停止脚本</h3>
<pre><code># stop.sh 停止的shell脚本
# !/bin/bash
# 定义镜像和容器名称
IMAGE_NAME="xxxx"
CONTAINER_NAME="xxxx"
# 检查容器是否已经运行
RUNNING=$(docker inspect --format="{{.State.Running}}" $CONTAINER_NAME 2&gt;/dev/null)
if [ "$RUNNING" == "true" ]; then
  # 如果容器正在运行，先停止容器
  echo "停止 Docker 容器 $CONTAINER_NAME ..."
  docker stop $CONTAINER_NAME
fi
# 检查并删除容器
EXIST=$(docker ps -a --filter "name=$CONTAINER_NAME" --format "{{.Names}}")
if [ ! -z "$EXIST" ]; then
  # 如果容器存在，删除容器
  echo "删除 Docker 容器 $CONTAINER_NAME ..."
  docker rm $CONTAINER_NAME
else
  echo "容器 $CONTAINER_NAME 不存在."
fi
# 检查并删除镜像
EXIST_IMAGE=$(docker images -q $IMAGE_NAME 2&gt;/dev/null)
if [ ! -z "$EXIST_IMAGE" ]; then
  # 如果镜像存在，删除镜像
  echo "删除 Docker 镜像 $IMAGE_NAME ..."
  docker rmi $IMAGE_NAME
else
  echo "镜像 $IMAGE_NAME 不存在."
fi</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/docker-qi-ting-pm2</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><pubDate>Mon, 9 Jun 2025 02:02:53 GMT</pubDate></item><item><title><![CDATA[Mysql 定时备份 Docker 全库数据]]></title><link>https://xiaoming728.com/archives/mysql-ding-shi-bei-fen-quan-ku-shu-ju</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Mysql%20%E5%AE%9A%E6%97%B6%E5%A4%87%E4%BB%BD%20Docker%20%E5%85%A8%E5%BA%93%E6%95%B0%E6%8D%AE&amp;url=/archives/mysql-ding-shi-bei-fen-quan-ku-shu-ju" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E5%8D%95%E5%BA%93-linux-shell-%E8%84%9A%E6%9C%AC">单库 Linux Shell 脚本</h3>
<pre><code class="language-shell">#!/bin/bash

# 配置参数
CONTAINER_NAME="mysql"
USER="root"
PASSWORD="admin@mysql"
DATABASE="xxx"
BACKUP_DIR="/etc/bak"
DATE=$(date +"%Y%m%d%H%M%S")
BACKUP_FILE="$BACKUP_DIR/$DATABASE-$DATE.sql"

# 创建备份目录（如果不存在）
mkdir -p $BACKUP_DIR

# 备份数据库
docker exec $CONTAINER_NAME mysqldump -u $USER -p$PASSWORD $DATABASE &gt; $BACKUP_FILE

# 检查备份是否成功
if [ $? -eq 0 ]; then
  echo "数据库备份成功: $BACKUP_FILE"
else
  echo "数据库备份失败"
  exit 1
fi

# 可选：删除超过7天的备份文件
find $BACKUP_DIR -type f -name "*.sql" -mtime +7 -exec rm {} \;

# 设置文件权限
chmod 600 $BACKUP_FILE</code></pre>
<h3 style="" id="%E5%85%A8%E5%BA%93-linux-shell-%E8%84%9A%E6%9C%AC">全库 Linux Shell 脚本</h3>
<pre><code class="language-shell">#!/bin/bash

# 配置参数
CONTAINER_NAME="mysql"
USER="root"
PASSWORD="admin@mysql"
BACKUP_DIR="/etc/bak"
DATE=$(date +"%Y%m%d%H%M%S")
BACKUP_FILE="$BACKUP_DIR/all-databases-$DATE.sql"

# 创建备份目录（如果不存在）
mkdir -p "$BACKUP_DIR"

# 全库备份
docker exec "$CONTAINER_NAME" mysqldump -u"$USER" -p"$PASSWORD" --all-databases &gt; "$BACKUP_FILE"

# 检查备份是否成功
if [ $? -eq 0 ]; then
  echo "全库备份成功: $BACKUP_FILE"
else
  echo "全库备份失败"
  exit 1
fi

# 可选：删除超过7天的备份文件
find "$BACKUP_DIR" -type f -name "*.sql" -mtime +7 -exec rm -f {} \;

# 设置文件权限
chmod 600 "$BACKUP_FILE"
</code></pre>
<h3 style="" id="linux-cron-%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E8%84%9A%E6%9C%AC">Linux Cron 定时任务脚本</h3>
<p style="">打开定时任务列表</p>
<pre><code>crontab -e</code></pre>
<p style="">配置执行脚本</p>
<pre><code># 每天凌晨 2 点执行
0 2 * * * /opt/mysql/mysqlbak.sh</code></pre>
<p style="">各字段含义</p>
<pre><code>*     *     *     *     *     命令
|     |     |     |     |
|     |     |     |     └───── 星期几 (0 - 7)（0和7都代表星期日）
|     |     |     └────────── 月份 (1 - 12)
|     |     └─────────────── 日期 (1 - 31)
|     └──────────────────── 小时 (0 - 23)
└───────────────────────── 分钟 (0 - 59)</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/mysql-ding-shi-bei-fen-quan-ku-shu-ju</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Fri, 23 May 2025 07:45:55 GMT</pubDate></item><item><title><![CDATA[小明剑魔视频制作]]></title><link>https://xiaoming728.com/archives/wei-ming-ming-wen-zhang</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%B0%8F%E6%98%8E%E5%89%91%E9%AD%94%E8%A7%86%E9%A2%91%E5%88%B6%E4%BD%9C&amp;url=/archives/wei-ming-ming-wen-zhang" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">https://baijiahao.baidu.com/s?id=1826770280113152236&amp;wfr=spider&amp;for=pc</p>
</blockquote>
<p style="">“回答我！”、“凝视我的双眼！”、“告诉我，这是为何？”——这些语录近期频频出现在你的视野中，它们正是当前网络热议的焦点，也是“小明剑魔”火爆全网，引发“全民爆改挑战”的根源所在。<br><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fnews%2F0110d5637dd9a384322c77a3e025b6ef.png&amp;size=m" alt="" width="100%" height="100%" style="display: inline-block"><br>
 接下来，我将为大家揭秘“小明剑魔”的制作秘诀。这次，我选择了“太乙真人”作为创作灵感，并进行了二次创作。现在，就让我们一起欣赏我的作品吧：</p>
<p style="line-height: 30px">是不是觉得既魔性又搞笑呢？接下来，我将详细分享我的制作流程，让你也能轻松尝试，打造属于自己的“小明剑魔”。<br>
 运用Deepseek生成语录文案</p>
<p style="line-height: 30px">首先，将原视频中的经典台词逐一提取，并妥善保存至文档中。接着，打开Deepseek平台：https://chat.deepseek.com/，向其咨询：“你知道小明剑魔吗？”在初步探索过程中，我惊喜地发现，Deepseek对我要进行二次创作的原型有着深入的了解，这让我对其可靠性倍感信心。<br><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fnews%2F34a00bc08b5a852613c77f77302a3d08.png&amp;size=m" alt="" width="100%" height="100%" style="display: inline-block"><br>
 这是从原视频中提取的经典台词，现在准备将其输入Deepseek平台进行进一步处理。请看，这就是我要投喂给deepseek的语录原文，内容如下，请务必全文背诵，以备后用。<br>
 这是要输入Deepseek平台的经典台词，现在准备就绪。请看，这就是即将被投入deepseek处理系统的语录原文，内容如下，请务必全文铭记，以备不时之需。<br><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fnews%2F1ca1b03e0eb9b90ce789c6ec5e772d85.png&amp;size=m" alt="" width="100%" height="100%" style="display: inline-block"><br>
 将语录提交给AI进行分析，同时明确我的需求：确保每一句台词都与语录一一对应，这样在后续克隆声音时，才能更好地与画面同步。特别是那些最经典、大声吼叫的英文台词，以及“回答我”这样的关键语句，必须保持原汁原味，不容更改。<br>
 若对生成结果感到不满，可利用deepseek进行进一步优化：<br>
 经过一系列的调整，当您对最终结果感到满意时，就意味着您成功地完成了二创角色的语录创作。此时，您可以进一步欣赏和对比优化前后的效果，感受创作带来的成就感。<br><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fnews%2F167c8aefb167dff786108740d1941a57.png&amp;size=m" alt="" width="100%" height="100%" style="display: inline-block"><br>
 Viggle换脸尝试</p>
<p style="line-height: 30px">在您完成二创角色的语录创作后，若希望进一步探索，您可以尝试进行Viggle换脸。这一步骤旨在为您的角色赋予更生动的形象，让其在视觉上更加鲜明。通过换脸，您可以为角色注入新的活力，使其更贴近您心中的设定。<br>
 1、您可以自行在网上寻找合适的图片，或者尝试使用AI技术来生成独特的二创形象。这一步骤将为您的角色增添更多的个性化元素，使其更符合您的创作需求。<br>
 2、访问<br>
 viggle<br>
 的在线创作平台：https://viggle.ai/create-mix<br>
 （可能需要使用魔法上网）。在界面左侧上传您的原始视频，右侧则上传您创建的独特二创形象。完成上传后，点击右下角即可生成融合了二创形象的视频。<br>
 这里生成的视频已经成功进行了换脸处理，但目前仍保留着原视频的声音和视频水印。接下来，我们将介绍如何去除这些水印。<br><strong>Noiz AI 声音克隆</strong></p>
<p style="line-height: 30px">要使用Noiz AI进行声音克隆，首先需要访问其官方网站：https://noiz.ai/landing。在页面上，找到并点击“立即开始”按钮，即可进入声音克隆的操作界面。<br>
 在进入声音克隆的操作界面后，你会看到右上角有一个“新建声音”的选项，点击它即可开始新的声音克隆旅程。<br>
 命名并上传你的声音，开启声音克隆之旅。<br>
 3-10秒的原视频音频，确认无误后即可进行后续操作。<br><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fnews%2Fd0e01e97229db630d9ef617d22cf924b.png&amp;size=m" alt="" width="100%" height="100%" style="display: inline-block"><br>
 声音克隆完成后，接下来需要在左侧选择“文本转语音”选项。<br><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fnews%2F8ef89298cf36f1c5818fca2d57292191.png&amp;size=m" alt="" width="100%" height="100%" style="display: inline-block"><br>
 在完成声音克隆后，将先前生成的文案粘贴至相应位置，并选取已克隆的音色，即可生成对应的语音。<br><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fnews%2Fbde02df0839ab6da88fca8557806bfc1.png&amp;size=m" alt="" width="100%" height="100%" style="display: inline-block"><br>
 将视频和音频素材导入剪映进行剪辑处理：<br>
 由于我们复制的声音缺少怒音效果，对于“回答我”以及几句英文怒斥的部分，我选择了直接使用原视频中的声音。同时，为了尽可能保持与原音频的节奏一致，我也对音频进行了调速处理。虽然这一步需要花费一些时间，但最终呈现的效果将更加贴切，更具真实感。<br>
 完成音频处理后，我们还需要进一步去除成品中的水印。为此，你可以使用腾讯智影平台（https://zenvideo.qq.com/），在其中选择智能抹除功能来轻松去除水印。<br><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fnews%2F565760ea71d2c425b2036ec1d8b7e783.png&amp;size=m" alt="" width="100%" height="100%" style="display: inline-block"><br>
 上传过程中，请确保你的网络连接稳定，以确保视频能够顺利上传。上传完成后，你可以在平台内查看并管理你的视频文件。<br>
 在处理视频时，若发现原有水印位置需要调整或增强，可以采取在相应位置添加水印框的方式。例如，对于viggle生成的视频，其左侧和右侧通常都会带有水印，因此，我们需要分别在这两个位置添加水印框，以确保水印的清晰可见。<br>
 完成水印调整后，即可下载并分享您精心制作的viggle视频，一个充满创意的小明剑魔二创视频就这样诞生了！</p>]]></description><guid isPermaLink="false">/archives/wei-ming-ming-wen-zhang</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1675897634504-bf03f1a2a66a%3Fixid%3DM3w1NDIyMzN8MHwxfHNlYXJjaHw1NHx8YWl8ZW58MHx8fHwxNzQ3NjQ3NDEyfDA%26ixlib%3Drb-4.1.0&amp;size=m" type="image/jpeg" length="0"/><category>AI</category><pubDate>Mon, 19 May 2025 09:36:30 GMT</pubDate></item><item><title><![CDATA[DeepWiki：GitHub 源码阅读神器]]></title><link>https://xiaoming728.com/archives/deepwiki-github-yuan-ma-yue-du-shen-qi</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=DeepWiki%EF%BC%9AGitHub%20%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%A5%9E%E5%99%A8&amp;url=/archives/deepwiki-github-yuan-ma-yue-du-shen-qi" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">引用：https://mp.weixin.qq.com/s/K5Fw2os5V5Bk-00pgSo4cQ</p>
</blockquote>
<p style=""><span fontsize="" color="">DeepWiki<strong><sup>[1]&nbsp;</sup></strong>：基于 GitHub Repo 源代码生成最新版可对话式文档，由&nbsp;Devin<strong><sup>[2]</sup></strong>&nbsp;驱动。</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="">开源项目免费使用，无需注册。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="">私有项目中使用需在&nbsp;</span><code>http://devin.ai</code><span fontsize="" color="">&nbsp;注册账号。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="">直接访问&nbsp;</span><code>https://deepwiki.com</code><span fontsize="" color="">，或将 GitHub 链接中的 github 替换为 deepwiki。</span></p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qceb.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ybys.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><span fontsize="" color="">如果只看以上介绍，会云里雾里的。下面我们先来聊聊相关背景，你就知道 DeepWiki 价值所在了。</span></p>
<h2 style="" id="%E8%83%8C%E6%99%AF"><span fontsize="" color=""><strong>背景</strong></span></h2>
<p style=""><span fontsize="" color="">Devin AI 是由&nbsp;Cognition Labs<strong><sup>[3]</sup></strong>&nbsp;开发的自主人工智能助手工具，标榜为 “AI 软件开发者”。曾号称全球首个全自动 AI 程序员，因执行成本高导致订阅价格也极高（$500/月），后来就淡出人们视野了。目前更主流的开发形式是&nbsp;</span><code>IDE + MCP</code><span fontsize="" color="">（如 Cursor、VSCode、Windsurf 等），半自动化的工具链调用让控制更精准，结果也变得更加可靠。</span></p>
<p style=""><span fontsize="" color="">Devin 这次带来的 DeepWiki 确实是阅读 GitHub 项目的好帮手，在正式开始介绍 DeepWiki 前，我们先来了解一下目前阅读开源项目的痛点：</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="">GitHub 主流开源项目介绍以英文 README.md 为主，支持多语言介绍的并不多，对于非母语的人来说，存在一定阅读障碍。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="">很多仓库可能连比较像样的 README 介绍都没，更别提专门的文档网站或 Blog 了。于开发者而言是灾难性的，需要自行查看源代码或在 issues 中搜寻一些描述。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="">如果仓库文件超多，上百个文件，或大几十万行代码，想要通过阅读源码来建立项目宏观认知会变得特别难。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="">阅读一个仓库的源代码或许不难，但面对 GitHub 这种世界级的开发者聚集地，每天都会诞生大量开源项目，纯靠人力阅读总结会被累死（面对海量代码，人会变得麻木）。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="">在项目文档中不会有功能与源码之间的映射关系说明，但这又是借鉴参考项目时的一个重点需求。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="">...</span></p></li>
</ul>
<p style=""><span fontsize="" color="">在源码阅读方面，其实 GitHub 本身就做了许多改进，如树状目录，函数依赖图谱等。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-lxqy.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><span fontsize="" color="">随着 GitHub Copilot 的升级，也被集成进 GitHub，通过交互式对话来进一步辅助源码阅读。点击具体代码行号或顶部固定按钮唤醒 AI 对话，可提问项目相关的任何问题。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-zdzz.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><span fontsize="" color="">但以上这些 GitHub 提供的能力远远不够，并不能帮助我们快速建立项目宏观层面的认知（系统架构图、依赖图等）。</span></p>
<h2 style="" id="deepwiki-%E7%AE%80%E4%BB%8B"><span fontsize="" color=""><strong>DeepWiki 简介</strong></span></h2>
<p style=""><span fontsize="" color="">关于 DeepWiki 的详细信息是由以下推文揭露的，我对其进行了梳理。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-pfbx.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><span fontsize="" color="">Cognition Labs 打造了&nbsp;<strong>DeepWiki</strong>，一个免费、可对话的 GitHub 仓库百科全书，致力于让每一个开发者都能轻松访问最新、结构化的项目文档。DeepWiki 由 Devin 技术驱动，专为开源项目免费开放，无需注册即可使用。只需将任何 GitHub 仓库链接中的&nbsp;</span><code>github</code><span fontsize="" color="">&nbsp;替换为&nbsp;</span><code>deepwiki</code><span fontsize="" color="">，即可直接访问对应的 DeepWiki 页面。如：</span><code>https://deepwiki.com/&lt;user&gt;/&lt;repo&gt;</code></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-zjdd.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><span fontsize="" color="">据 Cognition Labs 成员介绍，DeepWiki 在构建过程中，让大语言模型（LLM）全面扫描了完整的代码库。到目前为止，它已经索引了超过 30,000 个热门 GitHub 仓库，处理了超过 40 亿行代码，处理总量超过 1000 亿 tokens，仅索引过程的计算开销就超过了 30 万美元。索引一个仓库的平均成本大约为 12 美元，但团队还是决定让所有开源项目免费使用，无需任何注册门槛。</span></p>
<p style=""><span fontsize="" color="">从系统设计来看，模型在局部理解代码（如函数、模块）方面表现非常出色，但真正的挑战在于理解整个代码库的<strong>全局结构</strong>。DeepWiki 针对这一难题，采用了分层方法：先将代码库划分为一套套高层次系统（high-level systems），再为每一个系统生成对应的 Wiki 页面，帮助用户在整体上把握项目架构。</span></p>
<p style=""><span fontsize="" color="">它还利用了一个非常有趣的信号——<strong>提交历史（commit history）</strong>。通过分析哪些文件经常被一起修改，可以构建出文件之间的关联图（graph），从而揭示项目内部许多潜在且重要的结构模式。这一方法进一步增强了 DeepWiki 对代码库内部逻辑关系的理解与呈现。</span></p>
<p style=""><span fontsize="" color="">如果找不到你需要的仓库，团队也很乐意帮你索引任何公开 GitHub 仓库。对于私有仓库，只需注册 Devin 账号即可使用相同功能。此外，DeepWiki 支持分享 Wiki 页面和智能解答链接，方便团队成员始终保持信息同步。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-bkno.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-lkcv.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<h2 style="" id="%E6%A1%88%E4%BE%8B%E5%88%86%E4%BA%AB"><span fontsize="" color=""><strong>案例分享</strong></span></h2>
<p style=""><span fontsize="" color="">我简单看了两个项目，发现确实有趣。如基于 VSCode 源码生成的文档，可根据代码行号生成系统架构图（这在 GitHub 仓库里是完全没有的）。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-fnwv.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><span fontsize="" color="">比较精彩的是发现它收录了我的 Noi 项目，该项目我并没有上传核心源码，仅放了插件和部分配置代码。它也能根据 README 和插件代码推测出 Noi 的架构，不但准确而且是多维度的。访问链接：</span><code>https://deepwiki.com/lencx/Noi</code></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-sasy.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-edhw.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><span fontsize="" color="">在进一步测试中，DeepWiki 还可以使用中文来对话。它会通过源码检索来进行功能讲解，甚至是某个具体功能实现，但暂时无法检索 issues 或 pr。对话支持&nbsp;</span><code>Deep Research</code><span fontsize="" color="">。开启后，检索会慢很多，不开时会快速扫描文件给出结果。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-kpos.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xgku.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-etrb.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<h2 style="" id="%E7%BB%93%E8%AF%AD"><span fontsize="" color=""><strong>结语</strong></span></h2>
<p style=""><span fontsize="" color="">目前看来，在源码阅读上，DeepWiki 算是 GitHub Copilot 的很好补充。生成多维度的系统架构图、提供交互式对话这些都可以帮你快速建立起一个项目的框架感，因此它也绝对称得上“源码阅读神器”。</span></p>
<h1 style="text-align: center" id="%E9%A2%98%E5%A4%96%E8%AF%9D"><span fontsize="" color=""><strong>题外话</strong></span></h1>
<p style=""><span fontsize="" color="">网上看到一个关于&nbsp;LangChain<strong><sup>[4]</sup></strong>&nbsp;的段子，很有意思。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-duyk.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<h4 style="" id="langchain-%E6%AF%81%E4%BA%86%E6%88%91%E7%9A%84%E5%A9%9A%E5%A7%BB"><span fontsize="" color=""><strong>LangChain 毁了我的婚姻</strong></span></h4>
<p style=""><span fontsize="" color="">一开始我是多么天真啊。我不过想捣鼓个小玩意儿而已。“试试 LangChain，”网上劝诱道，“这玩意儿能轻松帮你串联各种模型、数据，搭出复杂 AI 应用。” 我寻思，也行啊。我老婆甚至还鼓励过我：“你不是一直想折腾点 AI 啥的吗？”殊不知，那竟是她最后一次对我展露的鼓励微笑。</span></p>
<p style=""><span fontsize="" color="">我选择从头开始搭，不用模板，不看教程，发誓要亲手串联每个 LLM、每个向量数据库、每个检索器。显然，我对自己下手可狠，也没放过身边那些爱我的人。几小时变成几天，我像个瘾君子一样死守着 Cursor，口中念念有词：“AgentExecutor... 我的宝贝 AgentExecutor…” 老婆好心端来咖啡，我居然像只护崽的猫一样嘶声叫道：“别打扰我神圣的提示工程大业！”</span></p>
<p style=""><span fontsize="" color="">那晚，她问我要不要一起看电影。我豪爽地答道：“当然，看，肯定看！等我修完这个模型幻觉问题先。” 这句话已经是三天前了。她一个人啃完了《指环王》三部曲，而我，却盯着终端里花花绿绿的 debug 输出狂喜不止，感受着一种说不清道不明的顿悟——或许只是疯了吧。</span></p>
<p style=""><span fontsize="" color="">她仍想拯救一下我们的关系。“出去走走吧，”她温柔地说，“聊聊我们俩的未来。”我头也不抬：“不行啊，RAG 系统检索结果不够准，我必须赶紧优化一下提示链。” 她幽幽地问：“你的心还能检索到吗？”</span></p>
<p style=""><span fontsize="" color="">接下来便是无尽的依赖更新灾难。我随手执行了个&nbsp;</span><code>pip install -U langchain</code><span fontsize="" color="">，然后砰！一切都炸裂了。我花了整整八小时解决版本兼容问题，一边狂翻文档，一边在 GitHub 怒开 issue。她走进房间，看着被几十个浏览器标签和终端窗口环绕的我，声音轻得像是怕吓坏了某种生物：“这就是……现在的你了吗？”</span></p>
<p style=""><span fontsize="" color="">那晚，她终于离开了，说她要“找个不会把聊天模型当成灵魂伴侣的人”。上周她寄来了离婚协议书。我刚要落笔签字，AI 编程助手居然像知音一样与我心灵相通，我还没开口，它便乖巧地把代码补全了。“谁还需要什么人类的情感？” 我心想，看着 Cursor 完美自动补全整个法律文件分析器，“尤其是当你的 AI 比老婆更懂你的时候。”</span></p>
<h3 style="" id="references"><span fontsize="" color="">References</span></h3>
<p style=""><span fontsize="" color="">[1] <strong>DeepWiki:</strong><em>https://deepwiki.com</em></span></p>
<p style=""><span fontsize="" color="">[2] <strong>Devin:</strong><em>https://devin.ai</em></span></p>
<p style=""><span fontsize="" color="">[3] <strong>Cognition Labs:</strong><em>https://cognition.ai</em></span></p>
<p style=""><span fontsize="" color="">[4] <strong>LangChain:</strong><em>https://github.com/langchain-ai</em></span></p>]]></description><guid isPermaLink="false">/archives/deepwiki-github-yuan-ma-yue-du-shen-qi</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1697577418970-95d99b5a55cf%3Fixid%3DM3w1NDIyMzN8MHwxfHNlYXJjaHw0OXx8QUl8ZW58MHx8fHwxNzQ2NjkxODQ1fDA%26ixlib%3Drb-4.1.0&amp;size=m" type="image/jpeg" length="0"/><category>AI</category><pubDate>Thu, 8 May 2025 08:10:12 GMT</pubDate></item><item><title><![CDATA[Docker 部署 GitLab-ee 并破解]]></title><link>https://xiaoming728.com/archives/docker-bu-shu-gitlab-ee-bing-po-jie</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%20%E9%83%A8%E7%BD%B2%20GitLab-ee%20%E5%B9%B6%E7%A0%B4%E8%A7%A3&amp;url=/archives/docker-bu-shu-gitlab-ee-bing-po-jie" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1.%E4%BD%BF%E7%94%A8-docker-compuse-%E5%AE%89%E8%A3%85%E5%AE%B9%E5%99%A8">1.使用 docker-compuse 安装容器</h3>
<pre><code>version: '3.7'
services:
  web:
    image: 'gitlab/gitlab-ee:latest'
    #restart: always
    hostname: 'gitlab.example.com'
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'https://gitlab.example.com'
    ports:
      - '22:22'
      - '80:80'
      - '443:443'
    volumes:
      - '/data/docker-compose/gitlab/config:/etc/gitlab'
      - '/data/docker-compose/gitlab/logs:/var/log/gitlab'
      - '/data/docker-compose/gitlab/data:/var/opt/gitlab'</code></pre>
<h3 style="" id="2.%E5%88%9B%E5%BB%BA-ruby-docker-%E9%95%9C%E5%83%8F">2.创建 ruby docker 镜像</h3>
<pre><code>docker run -it --rm ruby /bin/bash</code></pre>
<h3 style="" id="3.%E7%94%9F%E6%88%90%E8%AE%B8%E5%8F%AF%E8%AF%81">3.生成许可证</h3>
<pre><code>gem install gitlab-license</code></pre>
<p style="">注意：这里 cat 命令使用的时候先<strong>换行</strong></p>
<pre><code>cat &gt; license.rb</code></pre>
<p style="">添加内容, <strong>注意修改下面的开始和结束时间</strong></p>
<pre><code>require "openssl"
require "gitlab/license"
key_pair = OpenSSL::PKey::RSA.generate(2048)
File.open("license_key", "w") { |f| f.write(key_pair.to_pem) }
public_key = key_pair.public_key
File.open("license_key.pub", "w") { |f| f.write(public_key.to_pem) }
private_key = OpenSSL::PKey::RSA.new File.read("license_key")
Gitlab::License.encryption_key = private_key
license = Gitlab::License.new
license.licensee = {
  "Name" =&gt; "none",
  "Company" =&gt; "none",
  "Email" =&gt; "example@test.com",
}
license.starts_at = Date.new(2020, 1, 1) # 开始时间
license.expires_at = Date.new(2050, 1, 1) # 结束时间
license.notify_admins_at = Date.new(2049, 12, 1)
license.notify_users_at = Date.new(2049, 12, 1)
license.block_changes_at = Date.new(2050, 1, 1)
license.restrictions = {
  active_user_count: 10000,
}
puts "License:"
puts license
data = license.export
puts "Exported license:"
puts data
File.open("GitLabBV.gitlab-license", "w") { |f| f.write(data) }
public_key = OpenSSL::PKey::RSA.new File.read("license_key.pub")
Gitlab::License.encryption_key = public_key
data = File.read("GitLabBV.gitlab-license")
$license = Gitlab::License.import(data)
puts "Imported license:"
puts $license
unless $license
  raise "The license is invalid."
end
if $license.restricted?(:active_user_count)
  active_user_count = 10000
  if active_user_count &gt; $license.restrictions[:active_user_count]
    raise "The active user count exceeds the allowed amount!"
  end
end
if $license.notify_admins?
  puts "The license is due to expire on #{$license.expires_at}."
end
if $license.notify_users?
  puts "The license is due to expire on #{$license.expires_at}."
end
module Gitlab
  class GitAccess
    def check(cmd, changes = nil)
      if $license.block_changes?
        return build_status_object(false, "License expired")
      end
    end
  end
end
puts "This instance of GitLab Enterprise Edition is licensed to:"
$license.licensee.each do |key, value|
  puts "#{key}: #{value}"
end
if $license.expired?
  puts "The license expired on #{$license.expires_at}"
elsif $license.will_expire?
  puts "The license will expire on #{$license.expires_at}"
else
  puts "The license will never expire."
end</code></pre>
<p style="">结束 强推即可 <code>mac</code> 为 <code>command + c</code> 即可生产文件 license.rb</p>
<p style="">运行 <code>license.rb</code> 将会生成 <code>GitLabBV.gitlab-license</code> <code>license_key</code> <code>license_key.pub</code> 这三个文件。</p>
<pre><code>ruby license.rb</code></pre>
<p style="line-height: 1.74">记录 <code>GitLabBV.gitlab-license</code> <code>license_key.pub</code> 内容<span style="font-size: 19px; color: rgb(34, 34, 34)"><br></span>用 <code>license_key.pub</code> 文件替换 <code>/opt/gitlab/embedded/service/gitlab-rails/.license_encryption_key.pub</code> 中的内容</p>
<p style=""><code>GitLabBV.gitlab-license</code> 即是许可证，填入 ${address}/admin/license 地址（最新版本中该地址已移除）并重启。</p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/docker-bu-shu-gitlab-ee-bing-po-jie</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fopen-graph-gitlab.png&amp;size=m" type="image/jpeg" length="0"/><category>Git技术</category><pubDate>Mon, 10 Feb 2025 06:24:58 GMT</pubDate></item><item><title><![CDATA[GitLab 安装与破解]]></title><link>https://xiaoming728.com/archives/gitlab-an-zhuang-yu-po-jie</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=GitLab%20%E5%AE%89%E8%A3%85%E4%B8%8E%E7%A0%B4%E8%A7%A3&amp;url=/archives/gitlab-an-zhuang-yu-po-jie" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="1.yum%E5%AE%89%E8%A3%85gitlab"><strong>1.Yum安装GitLab</strong></h1>
<h3 style="" id="%E6%B7%BB%E5%8A%A0-gitlab-%E7%A4%BE%E5%8C%BA%E7%89%88-package">添加 GitLab 社区版 Package</h3>
<pre><code>curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash</code></pre>
<h3 style="" id="%E5%AE%89%E8%A3%85-gitlab-%E7%A4%BE%E5%8C%BA%E7%89%88">安装 GitLab 社区版</h3>
<pre><code>sudo yum install -y gitlab-ce</code></pre>
<p style="line-height: 1.74"></p>
<h3 style="" id="%E6%B7%BB%E5%8A%A0-gitlab-%E4%BC%81%E4%B8%9A%E7%89%88-package">添加 GitLab 企业版 Package</h3>
<pre><code>curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.rpm.sh | sudo bash</code></pre>
<h3 style="" id="%E5%AE%89%E8%A3%85-gitlab-%E4%BC%81%E4%B8%9A%E7%89%88">安装 GitLab 企业版</h3>
<pre><code>sudo yum install -y gitlab-ce</code></pre>
<h1 style="" id="2.%E9%85%8D%E7%BD%AEgitlab%E7%AB%99%E7%82%B9url%EF%BC%88%E5%8F%AF%E9%80%89%EF%BC%89"><strong>2.配置GitLab站点Url（可选）</strong></h1>
<p style="">GitLab默认的配置文件路径是 <code>/etc/gitlab/gitlab.rb</code></p>
<p style="line-height: 1.74">默认的站点Url配置项是：<code>external_url 'http://gitlab.example.com'</code></p>
<p style="line-height: 1.74">将GitLab站点Url修改为 <code>http(s)://xxx.com</code> 也可以用IP代替域名，这里根据自己需求来即可</p>
<h3 style="" id="%E4%BF%AE%E6%94%B9%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">修改配置文件</h3>
<pre><code>sudo vi /etc/gitlab/gitlab.rb</code></pre>
<h3 style="" id="%E9%85%8D%E7%BD%AE%E9%A6%96%E9%A1%B5%E5%9C%B0%E5%9D%80%EF%BC%88%E5%A4%A7%E7%BA%A6%E5%9C%A8%E7%AC%AC-15-%E8%A1%8C%EF%BC%89">配置首页地址（大约在第 15 行）</h3>
<pre><code>external_url 'http(s)://xxx.com'</code></pre>
<h1 style="" id="3.%E5%90%AF%E5%8A%A8%E5%B9%B6%E8%AE%BF%E9%97%AEgitlab"><strong>3.启动并访问GitLab</strong></h1>
<h3 style="" id="%E5%90%AF%E5%8A%A8gitlab">启动GitLab</h3>
<p style="line-height: 1.74">重新配置并启动</p>
<pre><code>sudo gitlab-ctl reconfigure</code></pre>
<p style="line-height: 1.74">完成后将会看到如下输出</p>
<pre><code>Running handlers complete
Chef Client finished, 432/613 resources updated in 03 minutes 43 seconds
gitlab Reconfigured!</code></pre>
<h3 style="" id="%E8%AE%BF%E9%97%AEgitlab">访问GitLab</h3>
<p style="line-height: 1.74">将设置的域名DNS解析到服务器IP，或者修改本地host将域名指向服务器IP。</p>
<h1 style="" id="4.%E7%A0%B4%E8%A7%A3-gitlab-ee%EF%BC%88%E5%8F%AF%E9%80%89%EF%BC%89">4.破解 GitLab-ee（可选）</h1>
<h3 style="" id="%E5%8F%AF%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5">可参考链接</h3>
<p style=""><hyperlink-inline-card target="_blank" href="https://linux.do/t/topic/30583" theme="inline">
  <a href="https://linux.do/t/topic/30583" target="_blank">https://linux.do/t/topic/30583</a>
 </hyperlink-inline-card></p>
<p style=""><hyperlink-inline-card target="_blank" href="https://blog.17lai.site/posts/29a820b3/" theme="inline">
  <a href="https://blog.17lai.site/posts/29a820b3/" target="_blank">https://blog.17lai.site/posts/29a820b3/</a>
 </hyperlink-inline-card></p>
<p style=""><hyperlink-inline-card target="_blank" href="https://github.com/nannanStrawberry314/license" theme="inline">
  <a href="https://github.com/nannanStrawberry314/license" target="_blank">https://github.com/nannanStrawberry314/license</a>
 </hyperlink-inline-card></p>
<p style=""><span style="font-size: 16px; color: rgb(31, 35, 40)"><hyperlink-inline-card target="_blank" href="https://github.com/qy527145/gitlabee_crack" theme="inline">
   <a href="https://github.com/qy527145/gitlabee_crack" target="_blank">https://github.com/qy527145/gitlabee_crack</a>
  </hyperlink-inline-card></span></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/gitlab-an-zhuang-yu-po-jie</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fopen-graph-gitlab.png&amp;size=m" type="image/jpeg" length="0"/><category>Git技术</category><pubDate>Mon, 10 Feb 2025 04:17:21 GMT</pubDate></item><item><title><![CDATA[Docker 镜像地址收集]]></title><link>https://xiaoming728.com/archives/docker-jing-xiang-di-zhi-shou-ji</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%20%E9%95%9C%E5%83%8F%E5%9C%B0%E5%9D%80%E6%94%B6%E9%9B%86&amp;url=/archives/docker-jing-xiang-di-zhi-shou-ji" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E4%BF%AE%E6%94%B9%E9%95%9C%E5%83%8F%E6%BA%90">修改镜像源</h3>
<blockquote>
 <p style="">最新地址可以参考：https://www.wangdu.site/course/2109.html</p>
</blockquote>
<pre><code>tee /etc/docker/daemon.json &lt;&lt;-'EOF'
{
  "registry-mirrors": [
    "https://docker.rainbond.cc",
    "https://docker.hpcloud.cloud",
    "https://docker.m.daocloud.io",
    "https://docker.unsee.tech",
    "https://docker.1panel.live",
    "http://mirrors.ustc.edu.cn",
    "https://docker.chenby.cn",
    "http://mirror.azure.cn",
    "https://dockerpull.org",
    "https://dockerhub.icu",
    "https://hub.rat.dev",
    "https://proxy.1panel.live",
    "https://docker.1panel.top",
    "https://docker.m.daocloud.io",
    "https://docker.1ms.run",
    "https://docker.ketches.cn"
  ]
}
EOF</code></pre>
<h3 style="" id="%E5%88%B7%E6%96%B0%E9%95%9C%E5%83%8F%E6%BA%90">刷新镜像源</h3>
<pre><code>systemctl daemon-reload</code></pre>
<h3 style="" id="%E9%87%8D%E5%90%AF-doker">重启 Doker</h3>
<pre><code>systemctl restart docker</code></pre>
<blockquote>
 <p style=""><span fontsize="" color="rgb(17, 24, 39)" style="color: rgb(17, 24, 39)"><mark data-color="#fef9c3" style="background-color: #fef9c3; color: inherit">注意：更换镜像源后，search 命令依旧不好使，想要测试直接 docker pull nginx</mark></span></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/docker-jing-xiang-di-zhi-shou-ji</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><pubDate>Wed, 8 Jan 2025 01:57:14 GMT</pubDate></item><item><title><![CDATA[亚马逊 Aws EC2 服务器配置 Root 访问]]></title><link>https://xiaoming728.com/archives/aws-pei-zhi-root-fang-wen</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E4%BA%9A%E9%A9%AC%E9%80%8A%20Aws%20EC2%20%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%85%8D%E7%BD%AE%20Root%20%E8%AE%BF%E9%97%AE&amp;url=/archives/aws-pei-zhi-root-fang-wen" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="1.%E5%88%9B%E5%BB%BA%E5%AE%8Cec2%E5%AE%9E%E4%BE%8B%EF%BC%8C%E4%B8%8B%E8%BD%BD%E5%AF%86%E9%92%A5%E5%AF%B9-%E4%BD%BF%E7%94%A8%E5%AF%86%E9%92%A5%E5%AF%B9%E7%99%BB%E5%BD%95-(ec2%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%9D%E5%A7%8B%E7%94%A8%E6%88%B7%E5%90%8D%E9%83%BD%E6%98%AFec2-user)">1.创建完ec2实例，下载密钥对 使用密钥对登录 (ec2服务器初始用户名都是ec2-user)</h2>
<pre><code>ssh -i "ec2-user.pem" ec2-user@ec2-xx-xx-xx-xx.xx-1.compute.amazonaws.com</code></pre>
<h2 style="" id="2.%E4%B8%BA-root-%E5%88%9B%E5%BB%BA%E5%AF%86%E7%A0%81">2.为 root 创建密码</h2>
<pre><code>sudo passwd root</code></pre>
<h2 style="" id="3.%E5%88%87%E6%8D%A2%E5%88%B0-root-%E8%BA%AB%E4%BB%BD">3.切换到 root 身份</h2>
<pre><code>su root</code></pre>
<h2 style="" id="4.%E7%94%A8root%E5%8E%BB%E4%BF%AE%E6%94%B9ssh%E6%96%87%E4%BB%B6">4.用root去修改ssh文件</h2>
<pre><code>vi /etc/ssh/sshd_config
PermitRootLogin no                         改为 PermitRootLogin yes
PasswordAuthentication no                  改为 PasswordAuthentication yes
UsePAM yes                                 改为 UsePAM no</code></pre>
<h2 style="" id="5.%E9%87%8D%E5%90%AFsshd%E6%9C%8D%E5%8A%A1">5.重启sshd服务</h2>
<pre><code>sudo /sbin/service sshd restart</code></pre>
<h2 style="" id="6.%E4%B8%BA%E9%BB%98%E8%AE%A4%E7%94%A8%E6%88%B7-ec2-user-%E6%B7%BB%E5%8A%A0%E7%99%BB%E5%BD%95%E5%AF%86%E7%A0%81">6.为默认用户 ec2-user 添加登录密码</h2>
<pre><code>passwd ec2-user</code></pre>
<blockquote>
 <p style="">原文链接：https://blog.csdn.net/qq_18980621/article/details/135755022</p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/aws-pei-zhi-root-fang-wen</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F1_rwAyCH8oA4BmpJojOu8nXA.png&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 3 Jan 2025 08:32:44 GMT</pubDate></item><item><title><![CDATA[CentOS7 使用 Aliyun DDNS 动态解析域名]]></title><link>https://xiaoming728.com/archives/centos-shi-yong-aliyun-ddns-dong-tai-jie-xi-yu-ming</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=CentOS7%20%E4%BD%BF%E7%94%A8%20Aliyun%20DDNS%20%E5%8A%A8%E6%80%81%E8%A7%A3%E6%9E%90%E5%9F%9F%E5%90%8D&amp;url=/archives/centos-shi-yong-aliyun-ddns-dong-tai-jie-xi-yu-ming" width="1" height="1" alt="" style="opacity:0;">
<p style="text-align: start; "><strong>相关链接</strong></p>
<blockquote>
 <p style=""><a href="https://blog.csdn.net/joeyoj/article/details/138279727"><span fontsize="" color="">CentOS 安装 PIP</span></a><span style="font-size: 16px; color: rgb(119, 119, 119)"> </span></p>
 <p style=""><a href="https://blog.csdn.net/qq_20349639/article/details/114113931"><span fontsize="" color="">CentOS使用 Aliyun DDNS 动态域名解析</span></a></p>
</blockquote>
<h3 style="text-align: start; " id="1.-%E9%98%BF%E9%87%8C%E4%BA%91%E8%AE%BE%E7%BD%AE"><strong>1. 阿里云设置</strong></h3>
<p style="text-align: start; ">首先，要确定一个准备用于外网访问的域名，并将此域名转入到阿里云的云解析服务来解析。如图所示，添加需要管理的域名。</p>
<p style="text-align: start; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Ftypora-xiaoming.oss-cn-hangzhou.aliyuncs.com%2Ftypora%2F7bdffae2ad7a79f51f8320a31075513a.png&amp;size=m" alt="img" width="100%" height="100%" style="display: inline-block"></p>
<p style="text-align: start; "></p>
<p style="text-align: start; ">阿里云云解析服务</p>
<p style="text-align: start; ">转入后，在解析设置中，设置一下A记录解析，解析的IP地址可以填当前的公网IP。如果不知道自己的公网IP，在CentOS系统下，可以输入使用以下命令获取当前的公网IP。</p>
<p style="text-align: start; "></p>
<pre><code>curl ifconfig.me</code></pre>
<p style="text-align: start; ">获取公网IP后，在阿里云云解析中设置完A记录解析。</p>
<p style="text-align: start; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Ftypora-xiaoming.oss-cn-hangzhou.aliyuncs.com%2Ftypora%2F5f39c2f526410e1e4bcac4c20930a1e9.png&amp;size=m" alt="img" width="100%" height="100%" style="display: inline-block"></p>
<p style="text-align: start; ">设置A记录解析</p>
<p style="text-align: start; ">在阿里云账户管理后台，点击右上角的账户头像，然后点击accesskeys，或者直接登陆<a href="https://ak-console.aliyun.com，获取阿里云的AccessKeyID和AccessKeySecret。">https://ak-console.aliyun.com，获取阿里云的<strong>AccessKeyID</strong>和<strong>AccessKeySecret</strong>。</a></p>
<h3 style="text-align: start; " id="2.-%E8%B7%AF%E7%94%B1%E5%99%A8%E8%AE%BE%E7%BD%AE"><strong>2. 路由器设置</strong></h3>
<p style="text-align: start; ">阿里云的设置完成后，需要对路由器设置端口映射，使外网对公网IP的端口访问能转发到内网服务器的相应端口。绝大部分的路由器都支持端口映射。</p>
<p style="text-align: start; ">常见的服务端口包括，用于WEB访问的80端口、SSH远程管理的22端口、Mysql数据库的3306端口、Transmission下载服务管理的9091端口和Plex媒体服务的32400端口等等。不用花生壳的好处就是没有端口数量限制，想设置多少就可以设置多少。</p>
<p style="text-align: start; ">当然，在设置端口映射之前，应确保服务器的内网IP已经设置为静态IP，而不是DHCP动态获取。</p>
<p style="text-align: start; ">例：对外33898.，映射为本机192.168.0.8的3389端口</p>
<p style="text-align: start; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Ftypora-xiaoming.oss-cn-hangzhou.aliyuncs.com%2Ftypora%2F2311e2031e86149b90a7d38465d4a2de.png&amp;size=m" alt="img" width="100%" height="100%" style="display: inline-block"></p>
<h3 style="text-align: start; " id="3.-%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%AE%BE%E7%BD%AE"><strong>3. 服务器设置</strong></h3>
<p style="text-align: start; ">服务器端安装好想要使用的各种服务后，别忘了在防火墙中开启相应的端口，在CentOS 7中，防火墙永久开启端口的命令是：</p>
<pre><code>firewall-cmd --add-port=80/tcp --permanent</code></pre>
<p style="text-align: start; ">开启之后别忘了重新载入防火墙的设置以使其生效，命令如下：</p>
<pre><code>firewall-cmd --reload</code></pre>
<h3 style="text-align: start; " id="4.-%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0%E5%9F%9F%E5%90%8D%E8%A7%A3%E6%9E%90%E7%A8%8B%E5%BA%8F"><strong>4. 自动更新域名解析程序</strong></h3>
<p style="text-align: start; ">准备工作都做好了，接下来就是通过程序检测公网IP，并在公网IP发生变化时，及时更新阿里云的域名解析。</p>
<p style="text-align: start; ">这个程序是用Python写的，先使用Python的包管理工具pip下载安装阿里云的Python SDK。如果没有安装pip，则先安装pip：</p>
<pre><code>yum install pip</code></pre>
<p style="text-align: start; ">安装失败参考: <a href="https://blog.csdn.net/joeyoj/article/details/138279727">CentOS 安装 PIP</a><span style="font-size: 16px; color: rgb(119, 119, 119)"> </span></p>
<p style="text-align: start; ">安装好pip后，安装阿里云的Python核心SDK以及云解析SDK：</p>
<pre><code>pip install aliyun-python-sdk-core
pip install aliyun-python-sdk-alidns</code></pre>
<p style="text-align: start; ">导入项目所需要的包，如果缺少则使用pip安装：</p>
<pre><code>import os
import json
from urllib2 import urlopen
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkalidns.request.v20150109 import DescribeDomainRecordsRequest
from aliyunsdkalidns.request.v20150109 import UpdateDomainRecordRequest</code></pre>
<p style="text-align: start; ">完整代码如下：</p>
<p style="text-align: start; "></p>
<pre><code>#!/usr/bin/env python
# coding= utf-8
​
import os
import json
from urllib2 import urlopen
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkalidns.request.v20150109 import DescribeDomainRecordsRequest
from aliyunsdkalidns.request.v20150109 import UpdateDomainRecordRequest
​
class DnsHandler:
 &nbsp; &nbsp;# 从阿里云开发者后台获取Access_Key_Id和Access_Key_Secret
 &nbsp; &nbsp;access_key_id = ""
 &nbsp; &nbsp;access_key_secret = ""
 &nbsp; &nbsp;# 填入自己的域名
 &nbsp; &nbsp;domain_name = ""
 &nbsp; &nbsp;# 填入二级域名的RR值
 &nbsp; &nbsp;rr_keyword = ""
 &nbsp; &nbsp;# 解析记录类型，一般为A记录
 &nbsp; &nbsp;record_type = "A"
 &nbsp; &nbsp;# 用于储存解析记录的文件名
 &nbsp; &nbsp;file_name = ".ip_addr"
 &nbsp; &nbsp;client = None
 &nbsp; &nbsp;record = None
 &nbsp; &nbsp;current_ip &nbsp;= ''
 &nbsp; &nbsp;region_id = ''
 &nbsp; &nbsp;# 初始化，获取client实例
 &nbsp; &nbsp;def __init__(self):
 &nbsp; &nbsp; &nbsp;  self.client = AcsClient(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  self.access_key_id,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  self.access_key_secret,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  self.region_id
 &nbsp; &nbsp; &nbsp;  )
 &nbsp; &nbsp; &nbsp;  self.record = self.get_record()
 &nbsp; &nbsp; &nbsp;  self.current_ip = self.get_current_ip()
 &nbsp; &nbsp;# 如果公网IP发生变化，则自动修改阿里云解析记录
 &nbsp; &nbsp;def reset(self):
 &nbsp; &nbsp; &nbsp; &nbsp;if self.current_ip &lt;&gt; self.get_record_value():
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  print self.update_record(self.current_ip)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  self.get_record()
 &nbsp; &nbsp;# 获取阿里云域名解析完整记录，并使用文件缓存
 &nbsp; &nbsp;def get_record(self):
 &nbsp; &nbsp; &nbsp; &nbsp;if os.path.isfile(self.file_name) :
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  file_handler = open(self.file_name, 'r')
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  r = file_handler.read()
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  file_handler.close()
 &nbsp; &nbsp; &nbsp; &nbsp;else :
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  request = DescribeDomainRecordsRequest.DescribeDomainRecordsRequest()
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  request.set_PageSize(10)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  request.set_action_name("DescribeDomainRecords")
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  request.set_DomainName(self.domain_name)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  request.set_RRKeyWord(self.rr_keyword)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  request.set_TypeKeyWord(self.record_type)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  r = self.client.do_action_with_exception(request)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  file_handler = open(self.file_name, 'w')
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  file_handler.write(r)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  file_handler.close()
 &nbsp; &nbsp; &nbsp; &nbsp;return json.loads(r)
 &nbsp; &nbsp;# 获取阿里云域名解析记录ID
 &nbsp; &nbsp;def get_record_id(self) :
 &nbsp; &nbsp; &nbsp; &nbsp;return self.record["DomainRecords"]["Record"][0]["RecordId"]
 &nbsp; &nbsp;# 获取当前域名解析记录
 &nbsp; &nbsp;def get_record_value(self) :
 &nbsp; &nbsp; &nbsp; &nbsp;return self.record["DomainRecords"]["Record"][0]["Value"]
 &nbsp; &nbsp;# 修改阿里云解析记录
 &nbsp; &nbsp;def update_record(self, value):
 &nbsp; &nbsp; &nbsp;  request = UpdateDomainRecordRequest.UpdateDomainRecordRequest()
 &nbsp; &nbsp; &nbsp;  request.set_action_name("UpdateDomainRecord")
 &nbsp; &nbsp; &nbsp;  request.set_RecordId(self.get_record_id())
 &nbsp; &nbsp; &nbsp;  request.set_Type(self.record_type)
 &nbsp; &nbsp; &nbsp;  request.set_RR(self.rr_keyword)
 &nbsp; &nbsp; &nbsp;  request.set_Value(value)
 &nbsp; &nbsp; &nbsp; &nbsp;return self.client.do_action_with_exception(request)
 &nbsp; &nbsp;# 获取当前公网IP
 &nbsp; &nbsp;def get_current_ip(self):
 &nbsp; &nbsp; &nbsp; &nbsp;return json.load(urlopen('http://jsonip.com'))['ip']
# 实例化类并启动更新程序
dns = DnsHandler()
dns.reset()</code></pre>
<p style="text-align: start; "><a href="http://将以上代码保存为dns.py">将以上代码保存为dns.py</a>文件，并赋予执行权限：</p>
<pre><code>chmod +x dns.py</code></pre>
<h3 style="text-align: start; " id="5.-%E8%AE%BE%E7%BD%AE%E5%AE%9A%E6%97%B6%E8%BF%90%E8%A1%8C"><strong>5. 设置定时运行</strong></h3>
<p style="text-align: start; ">CentOS内置有强大的计划任务工具Crontab，如果系统里没有则先使用yum安装：</p>
<pre><code>yum install crontabs</code></pre>
<p style="text-align: start; ">首先，设置执行用户的环境变量，比如，我们使用root用户来执行这一程序，则先在用户目录下建立.profile文件，或者在已有的.profile文件下加入如下一行，以使得可以使用VI来编辑cron文件：</p>
<pre><code>EDITOR=vi; export EDITOR</code></pre>
<p style="text-align: start; ">建立mycron文件，加入如下内容：</p>
<pre><code>*/10 * * * * /root/ddns/dns.py</code></pre>
<p style="text-align: start; ">这意味着每10分钟执行一次任务，即扫描公网IP，若与阿里云解析不一致，则修改阿里云解析。</p>
<p style="text-align: start; ">然后，提交crontab任务：</p>
<pre><code>crontab mycron</code></pre>
<p style="text-align: start; ">好了，大功告成，接下来，程序会每隔10分钟自动扫描公网IP，然后自动更新阿里云的解析，速度、稳定性和安全性都远胜于第三方的DDNS服务。</p>
<p style="text-align: start; "></p>]]></description><guid isPermaLink="false">/archives/centos-shi-yong-aliyun-ddns-dong-tai-jie-xi-yu-ming</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapp_DDNS.jpg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Tue, 3 Sep 2024 09:02:00 GMT</pubDate></item><item><title><![CDATA[Mac下 .bash_profile 和 .zshrc 和 .zprofile 有什么区别？]]></title><link>https://xiaoming728.com/archives/wei-ming-ming-wen-zhang</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Mac%E4%B8%8B%20.bash_profile%20%E5%92%8C%20.zshrc%20%E5%92%8C%20.zprofile%20%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F&amp;url=/archives/wei-ming-ming-wen-zhang" width="1" height="1" alt="" style="opacity:0;">
<p style="">在 macOS 中，<code>.bash_profile</code>、<code>.zshrc</code> 和 <code>.zprofile</code> 是不同的 shell 配置文件，它们在 shell 启动时的作用有所不同。下面是这些文件的区别：</p>
<h3 style="text-align: start; " id=".bash_profile"><code>.bash_profile</code></h3>
<ul>
 <li>
  <p style=""><strong>用于 Bash Shell</strong>：<code>bash</code> 是 macOS 之前的默认 shell，直到 macOS Catalina 版本（10.15）为止。</p></li>
 <li>
  <p style=""><strong>登录 Shell 时加载</strong>：当你以登录 shell 方式启动 Bash 时（例如通过终端应用启动），<code>.bash_profile</code> 会被读取和执行。</p></li>
 <li>
  <p style=""><strong>常用配置</strong>：常用来设置环境变量、别名、以及其他在登录时需要运行的命令。</p></li>
</ul>
<h3 style="text-align: start; " id=".zshrc"><code>.zshrc</code></h3>
<ul>
 <li>
  <p style=""><strong>用于 Zsh Shell</strong>：<code>zsh</code> 是 macOS Catalina 及其后续版本的默认 shell。</p></li>
 <li>
  <p style=""><strong>交互 Shell 时加载</strong>：每次启动交互式 shell 时，<code>.zshrc</code> 文件会被读取和执行。交互式 shell 是指能够接受用户输入的 shell（例如通过终端应用启动的 shell）。</p></li>
 <li>
  <p style=""><strong>常用配置</strong>：通常用于设置别名、函数、shell 选项和其他需要在每次启动交互式 shell 时运行的配置。</p></li>
</ul>
<h3 style="text-align: start; " id=".zprofile"><code>.zprofile</code></h3>
<ul>
 <li>
  <p style=""><strong>用于 Zsh Shell</strong>：<code>zsh</code> 的配置文件之一。</p></li>
 <li>
  <p style=""><strong>登录 Shell 时加载</strong>：当你以登录 shell 方式启动 Zsh 时，<code>.zprofile</code> 文件会被读取和执行。</p></li>
 <li>
  <p style=""><strong>常用配置</strong>：用来设置环境变量和其他在登录时需要运行的命令。类似于 <code>.bash_profile</code>，但专用于 Zsh。</p></li>
</ul>
<h3 style="text-align: start; " id="%E6%80%BB%E7%BB%93%E5%92%8C%E4%BD%BF%E7%94%A8%E5%BB%BA%E8%AE%AE">总结和使用建议</h3>
<ul>
 <li>
  <p style=""><strong>登录 Shell 和交互 Shell 的区别</strong>：</p>
  <ul>
   <li>
    <p style=""><strong>登录 Shell</strong>：通常通过登录会话启动，例如通过 SSH 连接或在终端应用中启动一个新的登录会话。</p></li>
   <li>
    <p style=""><strong>交互 Shell</strong>：通过终端应用直接启动的 shell。</p></li>
  </ul></li>
 <li>
  <p style=""><strong>配置文件加载顺序</strong>：</p>
  <ul>
   <li>
    <p style="">对于 Bash：</p>
    <ul>
     <li>
      <p style="">登录 shell：<code>.bash_profile</code> → <code>.bashrc</code>（如果在 <code>.bash_profile</code> 中显式调用）。</p></li>
     <li>
      <p style="">非登录 shell：<code>.bashrc</code>。</p></li>
    </ul></li>
   <li>
    <p style="">对于 Zsh：</p>
    <ul>
     <li>
      <p style="">登录 shell：<code>.zprofile</code> → <code>.zshrc</code>。</p></li>
     <li>
      <p style="">交互 shell：<code>.zshrc</code>。</p></li>
    </ul></li>
  </ul></li>
 <li>
  <p style=""><strong>使用建议</strong>：</p>
  <ul>
   <li>
    <p style="">如果你使用的是 Bash：</p>
    <ul>
     <li>
      <p style="">在 <code>.bash_profile</code> 中配置环境变量和启动脚本。</p></li>
     <li>
      <p style="">在 <code>.bashrc</code> 中配置别名和函数，并在 <code>.bash_profile</code> 中调用 <code>.bashrc</code>。</p></li>
    </ul></li>
   <li>
    <p style="">如果你使用的是 Zsh：</p>
    <ul>
     <li>
      <p style="">在 <code>.zprofile</code> 中配置环境变量和启动脚本。</p></li>
     <li>
      <p style="">在 <code>.zshrc</code> 中配置别名、函数和交互 shell 设置。</p></li>
    </ul></li>
  </ul></li>
</ul>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/wei-ming-ming-wen-zhang</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FmacOS.jpg&amp;size=m" type="image/jpeg" length="0"/><pubDate>Fri, 26 Jul 2024 08:07:00 GMT</pubDate></item><item><title><![CDATA[Nuxt.js 部署]]></title><link>https://xiaoming728.com/archives/nuxt.js-bu-shu</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Nuxt.js%20%E9%83%A8%E7%BD%B2&amp;url=/archives/nuxt.js-bu-shu" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E5%AE%89%E8%A3%85node">安装node</h3>
<p style=""><strong>CentOS7安装nodejs18和yarn</strong></p>
<p style=""><strong><span color="#dc2626" style="color: #dc2626">从nodejs18开始就不支持Centos7了</span></strong>，这是因为centos7的gilbc版本比较低，因此<span color="#dc2626" style="color: #dc2626">需要安装非官方构建的版本</span>。</p>
<p style=""><code>如果npm安装的包依赖于glibc，那得改用docker或者换操作系统了。</code></p>
<p style="">到非官方发布版本网站下载压缩包node-v18.19.0-linux-x64-glibc-217.tar.gz</p>
<blockquote>
 <p style=""><a href="https://unofficial-builds.nodejs.org/download/release/v18.19.0/">https://unofficial-builds.nodejs.org/download/release/v18.19.0/</a></p>
</blockquote>
<h3 style="" id="%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6%E5%88%B0%E6%9C%8D%E5%8A%A1%E5%99%A8%EF%BC%8C%E5%B9%B6%E8%A7%A3%E5%8E%8B%E5%88%B0%E6%96%87%E4%BB%B6%E5%A4%B9%2Fopt">上传文件到服务器，并解压到文件夹/opt</h3>
<pre><code>mv node-v18.19.0-linux-x64-glibc-217 node-v18</code></pre>
<h3 style="" id="%E8%BF%9B%E5%85%A5%2Fetc%2Fprofile.d%E6%96%87%E4%BB%B6%E5%A4%B9">进入/etc/profile.d文件夹</h3>
<pre><code>cd /etc/profile.d</code></pre>
<h3 style="" id="%E5%88%9B%E5%BB%BA%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E6%96%87%E4%BB%B6%EF%BC%8C%E7%84%B6%E5%90%8E%E4%BF%9D%E5%AD%98">创建环境变量文件，然后保存</h3>
<pre><code>vi nodejs.sh</code></pre>
<pre><code>export NODE_HOME=/opt/node-v18
export PATH=.:$NODE_HOME/bin:$PATH</code></pre>
<h3 style="" id="%E9%87%8D%E6%96%B0%E5%8A%A0%E8%BD%BD%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F">重新加载环境变量</h3>
<pre><code>source /etc/profile</code></pre>
<h3 style="" id="%E5%AE%89%E8%A3%85pm2">安装pm2</h3>
<pre><code>npm install pm2@latest -g</code></pre>
<h3 style="" id="%E5%AE%89%E8%A3%85git">安装git</h3>
<pre><code>sudo yum install git</code></pre>
<h3 style="" id="%E6%8B%89%E4%BB%A3%E7%A0%81">拉代码</h3>
<pre><code>git clone https://gitee.com/xxx.git</code></pre>
<h3 style="" id="%E5%88%87%E5%88%86%E6%94%AF">切分支</h3>
<pre><code>git branch -r</code></pre>
<pre><code>git checkout main</code></pre>
<h3 style="" id="%E6%9B%B4%E6%96%B0%E4%BB%A3%E7%A0%81">更新代码</h3>
<pre><code>git pull origin main</code></pre>
<h3 style="" id="%E9%83%A8%E7%BD%B2%E5%9C%A8%E4%BB%A3%E7%A0%81%E7%9B%AE%E5%BD%95%E4%B8%8B">部署在代码目录下</h3>
<pre><code>npm install</code></pre>
<pre><code>npm run build</code></pre>
<pre><code>pm2 start ecosystem.config.cjs</code></pre>
<h3 style="" id="%E5%A6%82%E6%9E%9C%E5%90%AF%E5%8A%A8%E5%A4%B1%E8%B4%A5%E5%85%88%E6%8A%8Apm2%E7%BB%99kill%E6%8E%89">如果启动失败先把pm2给kill掉</h3>
<pre><code>ps -ef|grep pm2</code></pre>
<h3 style="" id="%E5%86%8D%E9%87%8D%E6%96%B0%E5%90%AF%E5%8A%A8">再重新启动</h3>
<pre><code>pm2 start ecosystem.config.cjs</code></pre>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/nuxt.js-bu-shu</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fnuxt-js-logo.jpeg&amp;size=m" type="image/jpeg" length="0"/><pubDate>Thu, 11 Jul 2024 08:52:00 GMT</pubDate></item><item><title><![CDATA[Navicat 17 for mac 无限14天试用]]></title><link>https://xiaoming728.com/archives/navicat-17-wu-xian-shi-yong</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Navicat%2017%20for%20mac%20%E6%97%A0%E9%99%9014%E5%A4%A9%E8%AF%95%E7%94%A8&amp;url=/archives/navicat-17-wu-xian-shi-yong" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="1.%E5%AE%98%E7%BD%91%E4%B8%8B%E8%BD%BDnavicat-17">1.官网下载Navicat 17</h2>
<pre><code>https://www.navicat.com.cn/download/direct-download?product=navicat170_premium_cs.dmg&amp;location=1</code></pre>
<h2 style="" id="2.%E5%88%9B%E5%BB%BAnavicat.sh%E8%84%9A%E6%9C%AC">2.创建navicat.sh脚本</h2>
<pre><code>#!/bin/bash

set -e

file=$(defaults read /Applications/Navicat\ Premium.app/Contents/Info.plist)

regex="CFBundleShortVersionString = \"([^\.]+)"
[[ $file =~ $regex ]]

version=${BASH_REMATCH[1]}

echo "Detected Navicat Premium version $version"

case $version in
    "17")
        file=~/Library/Preferences/com.navicat.NavicatPremium.plist
        ;;
    "16")
        file=~/Library/Preferences/com.navicat.NavicatPremium.plist
        ;;
    *)
        echo "Version '$version' not handled"
        exit 1
       ;;
esac

echo -n "Reseting trial time..."

regex="([0-9A-Z]{32}) = "
[[ $(defaults read $file) =~ $regex ]]

hash=${BASH_REMATCH[1]}

if [ ! -z $hash ]; then
    defaults delete $file $hash
fi

regex="\.([0-9A-Z]{32})"
[[ $(ls -a ~/Library/Application\ Support/PremiumSoft\ CyberTech/Navicat\ CC/Navicat\ Premium/ | grep '^\.') =~ $regex ]]

hash2=${BASH_REMATCH[1]}

if [ ! -z $hash2 ]; then
    rm ~/Library/Application\ Support/PremiumSoft\ CyberTech/Navicat\ CC/Navicat\ Premium/.$hash2
fi

echo " Done"</code></pre>
<h2 style="" id="3.%E5%B0%86%E8%84%9A%E6%9C%AC%E6%B7%BB%E5%8A%A0%E5%88%B0%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%EF%BC%8C%E6%97%B6%E9%97%B4%E8%87%AA%E5%AE%9A%E4%B9%89%E8%B0%83%E8%8A%82">3.将脚本添加到定时任务，时间自定义调节</h2>
<p style="">打开终端（Terminal）编辑crontab文件，输入以下命令：</p>
<pre><code>crontab -e</code></pre>
<p style="">在打开的crontab编辑器中，添加以下内容：</p>
<pre><code>40 9 * * * /Users/liuchunming/file/navicat.sh &gt; /Users/liuchunming/file/navicat.log 2&gt;&amp;1 &amp;</code></pre>
<p style="">这条命令的含义是每天早上9点40分执行<code>navicat.sh</code>脚本，并将输出重定向到<code>navicat.log</code>文件中。如果希望在后台运行，请在命令末尾加上<code>&amp;</code>符号。</p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/navicat-17-wu-xian-shi-yong</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fnavicat.png&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><category>技术杂文</category><pubDate>Fri, 5 Jul 2024 02:14:49 GMT</pubDate></item><item><title><![CDATA[探讨如何为开源项目提交 issue]]></title><link>https://xiaoming728.com/archives/tan-tao-ru-he-wei-kai-yuan-xiang-mu-ti-jiao-issue</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E6%8E%A2%E8%AE%A8%E5%A6%82%E4%BD%95%E4%B8%BA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E6%8F%90%E4%BA%A4%20issue&amp;url=/archives/tan-tao-ru-he-wei-kai-yuan-xiang-mu-ti-jiao-issue" width="1" height="1" alt="" style="opacity:0;">
<h2 style="text-align: start; " id="%E4%BB%80%E4%B9%88%E6%98%AF-github-issue">什么是 GitHub Issue</h2>
<p style="text-align: start; ">GitHub 官方文档（<a rel="nofollow" href="https://docs.github.com/en/issues/tracking-your-work-with-issues/about-issues" class="external-link"><u>About issues - GitHub Docs</u></a>）给出的解释是 <strong>Use GitHub Issues to track ideas, feedback, tasks, or bugs for work on GitHub.</strong>，即通过 GitHub Issues 来跟踪 GitHub 项目相关的想法、反馈、任务及缺陷。从这段描述当中我们可以发现 Issue 不仅代表了 Bug、缺陷，它可以是任何跟项目有关且需要项目维护者知晓的内容，如果将其直译为<strong>问题</strong>可能会引起歧义，所以接下来我们直接使用 Issue 这个单词来表示它。</p>
<p style="text-align: start; ">另一个值得注意的地方是 GitHub Issues 是一种异步沟通体系。所谓异步，即我提交了一个 Issue 以后，并不意味着立即就能够收到反馈。异步沟通虽然不像同步沟通那样来得直接，但并不代表它不高效。因为不期望能立即收到反馈来进行进一步地沟通，双方都需要尽可能地将信息一次性传达到位，在一定程度上反而可以提高沟通效率。</p>
<h2 style="text-align: start; " id="%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%8F%90%E4%BA%A4-issue">为什么要提交 Issue</h2>
<p style="text-align: start; ">上文中提到了 Issue 所代表的含义，它可能是关于项目的一个新想法，也可能是对于某个特定功能的使用反馈，还有可能是使用过程中碰到的各种各样的 Bug。提交 Issue 的目的自然是让项目维护者知道有这样的一个 Issue 存在。这看起来是一句废话，但实际上不同的 Issue 内容，能够达成这个目的的效果可能千差万别。一个好的 Issue 能够清晰、准确地表达出自己希望传达的信息，项目维护者跟提交者之间不需要再次进行额外的沟通就能够顺利地处理掉这个 Issue。反之一个不好的 Issue 不仅起不到这样的效果，反而可能在你来我往的交互中逐渐跑偏甚至变得火药味十足。</p>
<h2 style="text-align: start; " id="%E5%A6%82%E4%BD%95%E6%8F%90%E4%BA%A4%E4%B8%80%E4%B8%AA-github-issue">如何提交一个 GitHub Issue</h2>
<p style="text-align: start; ">提交一个 Issue 很简单，打开 GitHub 登录下自己的账号，点几下鼠标敲几下键盘就可以提交一个 Issue。但是要提交一个好的 Issue 还是需要一定技巧的。在此之前，建议先阅读下&nbsp;<a rel="nofollow" href="https://wiki.fit2cloud.com/pages/viewpage.action?pageId=45778869"><u>提问的智慧</u></a> 这篇文章，虽然是来自英文版的直白翻译，其文化背景、用词风格以及社会环境跟国内有所差异，描绘的场景跟 GitHub Issues 的使用场景也略有区别，但是其中关于提问相关的注意事项还是很值得参考的。这里也借鉴下这篇文章的格式，尝试总结下如何提交一个好的 GitHub Issue。</p>
<h2 style="text-align: start; " id="%E6%8F%90%E4%BA%A4%E4%B9%8B%E5%89%8D">提交之前</h2>
<p style="text-align: start; ">首先，提交一个 Issue 不仅仅是为了请求帮助或者报告问题，也有可能会帮助后续遇到此问题的使用者。从某种意义上来说，这也是对整个开源生态非常具有价值的贡献。所以，不要抱着把问题抛出去等待解决的心理，需要认真对待提交 Issue 这件事，因为你也是一个<strong>贡献者</strong>。在提交 Issue 前，建议先做以下几件事：</p>
<ol>
 <li>
  <p style=""><strong>理清楚你遇到的问题，需要有一定的逻辑性。</strong></p></li>
 <li>
  <p style=""><strong>查阅项目文档，搜索是否有相关功能、问题的说明。</strong>幸运的话在这个环节我们的问题就可以得到解决，也可能会发现某个功能是设计如此，当然这个情况下我们仍可以提出自己的想法，但相应的措辞就需要有所转变。</p></li>
 <li>
  <p style=""><strong>在该项目的 Issues 中搜索有没有类似问题。</strong>同样的，如果能够找到类似问题并且在回复列表中看到解决、规避方案，又可以省下我们好多等待时间。如果问题看起来类似，但又不完全一致，可以就在此 Issue 进行回复并说明你遇到的问题。</p></li>
 <li>
  <p style=""><strong>在 GitHub 项目之外进行搜索，包括但不限于搜索引擎、内部知识库、项目论坛、交流群等等任何你能想到的地方。</strong>如果该项目跟某个其他项目使用了同样的底层库或依赖，有可能类似的问题在其他项目相关资料中可以找到解决方案；如果该项目足够活跃，可能会有用户在其他论坛或博客中分享使用经验或某个问题的解决方案。</p></li>
 <li>
  <p style=""><strong>Double Check 一下问题确实存在。</strong>在项目维护过程中，包括我自己的软件使用经历中，出现过很多因为自己的各种失误而引起的问题，例如某个单词拼写错误、某个功能的前置操作没有完成等等等等。如果这样的 Issue 被提交上来，不仅会浪费双方的时间，还会降低自己在项目维护者心中的可信度，使得后续真正的问题不被重视（参考狼来了的故事）。</p></li>
 <li>
  <p style=""><strong>在新版本中尝试验证。</strong></p></li>
 <li>
  <p style=""><strong>如果是同时遇到多个不相关的问题，不要提在同一个 Issue 里，可以提交多个 Issue，方便维护者跟踪。</strong></p></li>
</ol>
<p style="text-align: start; ">当你真正准备好提交一个问题时，请再次回想下 GitHub Issues 是一个异步沟通系统，假设你只能够发一条消息给项目维护者，你要怎么做才能够让自己的问题得到解决。</p>
<h2 style="text-align: start; " id="%E6%8F%90%E4%BA%A4%E7%9A%84%E5%86%85%E5%AE%B9">提交的内容</h2>
<h3 style="text-align: start; " id="%E4%B8%80%E4%B8%AA%E5%A5%BD%E7%9A%84-issue-%E6%A0%87%E9%A2%98"><strong>一个好的 Issue 标题</strong></h3>
<ol>
 <li>
  <p style="">尽可能使用一句话作为标题，但要求直截了当。千万不要直接把问题的详细内容直接写在标题。</p></li>
 <li>
  <p style="">给定一个范围标记，用于缩小这个问题的范围，方便维护者和后续使用者索引。</p></li>
 <li>
  <p style="">仅填写问题简述，不要添加无关内容。</p></li>
</ol>
<h4 style="text-align: start; " id="%E4%B8%80%E4%BA%9B%E6%A1%88%E4%BE%8B"><strong>一些案例</strong></h4>
<div style="overflow-x: auto; overflow-y: hidden;">
 <table style="width: 984px">
  <colgroup>
   <col style="width: 447px">
   <col style="width: 377px">
   <col style="width: 160px">
  </colgroup>
  <tbody>
   <tr style="box-sizing: border-box; border-top: 1px solid var(--color-border-muted); border-right: 0px solid rgb(229, 231, 235); border-bottom: 0px solid rgb(229, 231, 235); border-left: 0px solid rgb(229, 231, 235); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; background-color: var(--color-canvas-default); height: 60px;">
    <th colspan="1" rowspan="1" colwidth="447" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px; font-weight: var(--base-text-weight-semibold, 600);">
     <p style=""><strong>Bad</strong></p></th>
    <th colspan="1" rowspan="1" colwidth="377" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px; font-weight: var(--base-text-weight-semibold, 600);">
     <p style=""><strong>Good</strong></p></th>
    <th colspan="1" rowspan="1" colwidth="160" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px; font-weight: var(--base-text-weight-semibold, 600);">
     <p style=""><strong>Why</strong></p></th>
   </tr>
   <tr style="box-sizing: border-box; border-top: 1px solid var(--color-border-muted); border-right: 0px solid rgb(229, 231, 235); border-bottom: 0px solid rgb(229, 231, 235); border-left: 0px solid rgb(229, 231, 235); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; background-color: var(--color-canvas-subtle); height: 60px;">
    <td colspan="1" rowspan="1" colwidth="447" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">表格渲染异常</p></td>
    <td colspan="1" rowspan="1" colwidth="377" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">编辑器：表格语法渲染异常，在编辑器可以正常渲染，发布后无法渲染</p></td>
    <td colspan="1" rowspan="1" colwidth="160" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">问题描述不够清晰</p></td>
   </tr>
   <tr style="box-sizing: border-box; border-top: 1px solid var(--color-border-muted); border-right: 0px solid rgb(229, 231, 235); border-bottom: 0px solid rgb(229, 231, 235); border-left: 0px solid rgb(229, 231, 235); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; background-color: var(--color-canvas-default); height: 60px;">
    <td colspan="1" rowspan="1" colwidth="447" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">安装报错</p></td>
    <td colspan="1" rowspan="1" colwidth="377" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">安装站点异常，提示 Internal Server Error</p></td>
    <td colspan="1" rowspan="1" colwidth="160" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">问题描述不够清晰</p></td>
   </tr>
   <tr style="box-sizing: border-box; border-top: 1px solid var(--color-border-muted); border-right: 0px solid rgb(229, 231, 235); border-bottom: 0px solid rgb(229, 231, 235); border-left: 0px solid rgb(229, 231, 235); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; background-color: var(--color-canvas-subtle); height: 60px;">
    <td colspan="1" rowspan="1" colwidth="447" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">列表无法渲染</p></td>
    <td colspan="1" rowspan="1" colwidth="377" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">文章列表：后台文章列表渲染空白，可能是src/components/PostList.vue中的 xx 属性为空</p></td>
    <td colspan="1" rowspan="1" colwidth="160" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">问题描述不够清晰</p></td>
   </tr>
   <tr style="box-sizing: border-box; border-top: 1px solid var(--color-border-muted); border-right: 0px solid rgb(229, 231, 235); border-bottom: 0px solid rgb(229, 231, 235); border-left: 0px solid rgb(229, 231, 235); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; background-color: var(--color-canvas-default); height: 60px;">
    <td colspan="1" rowspan="1" colwidth="447" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">数学公式问题</p></td>
    <td colspan="1" rowspan="1" colwidth="377" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">文章页面：数学公式原样输出，没有经过渲染</p></td>
    <td colspan="1" rowspan="1" colwidth="160" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">问题描述不够清晰</p></td>
   </tr>
   <tr style="box-sizing: border-box; border-top: 1px solid var(--color-border-muted); border-right: 0px solid rgb(229, 231, 235); border-bottom: 0px solid rgb(229, 231, 235); border-left: 0px solid rgb(229, 231, 235); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; background-color: var(--color-canvas-subtle); height: 60px;">
    <td colspan="1" rowspan="1" colwidth="447" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">上传附件 bug 提交，请作者看看，谢谢</p></td>
    <td colspan="1" rowspan="1" colwidth="377" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">附件上传：在附件管理中上传附件提示失败，浏览器控制台提示 413 Request Entity Too Large</p></td>
    <td colspan="1" rowspan="1" colwidth="160" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">问题描述不够清晰，包含无关内容</p></td>
   </tr>
   <tr style="box-sizing: border-box; border-top: 1px solid var(--color-border-muted); border-right: 0px solid rgb(229, 231, 235); border-bottom: 0px solid rgb(229, 231, 235); border-left: 0px solid rgb(229, 231, 235); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; background-color: var(--color-canvas-default); height: 60px;">
    <td colspan="1" rowspan="1" colwidth="447" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">将一个带有 “”接口用例且接口用例里面有环境配置 “” 的接口 导出来，然后 导入另外一个项目，再创建接口用例，配置环境，有时候出现新接口用例使用的是旧的环境，根本原因是 接口导出的竟然把 对应用例的环境 ID给导出来。</p></td>
    <td colspan="1" rowspan="1" colwidth="377" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">接口定义导出再导入到另一个项目时，新导入接口下的用例使用了原项目中的环境</p></td>
    <td colspan="1" rowspan="1" colwidth="160" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">问题描述不够简洁，将详细内容直接写在了标题里</p></td>
   </tr>
   <tr style="box-sizing: border-box; border-top: 1px solid var(--color-border-muted); border-right: 0px solid rgb(229, 231, 235); border-bottom: 0px solid rgb(229, 231, 235); border-left: 0px solid rgb(229, 231, 235); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; background-color: var(--color-canvas-subtle); height: 60px;">
    <td colspan="1" rowspan="1" colwidth="447" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">(没有标题)</p></td>
    <td colspan="1" rowspan="1" colwidth="377" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style=""></p></td>
    <td colspan="1" rowspan="1" colwidth="160" style="box-sizing: border-box; border: 1px solid var(--color-border-default); --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ; padding: 6px 13px;">
     <p style="">如果不是有责任维护项目，甚至都不想点进去看内容</p></td>
   </tr>
  </tbody>
 </table>
</div>
<h3 style="text-align: start; " id="%E9%97%AE%E9%A2%98%E6%8F%8F%E8%BF%B0"><strong>问题描述</strong></h3>
<p style="text-align: start; ">建议参考以下几点：</p>
<ol>
 <li>
  <p style="">避免使用大量啰嗦的文字来描述，保证干练和逻辑性即可。如果无法描述清楚，可以通过补充复现步骤来辅助。一个好的 Issue 能够<strong>恰到好处</strong>地给出所需的信息。所谓恰到好处，是指信息不多也不少，刚刚好能够支持项目维护者解决这个问题或者了解这个需求。</p></li>
 <li>
  <p style="">内容仅针对此问题，不要包含遇到的其他问题，其他问题可以另外提交 Issue。</p></li>
 <li>
  <p style="">不要包含与问题无关的话语。</p></li>
 <li>
  <p style="">没有明显的错别字，合理使用标点符号</p></li>
</ol>
<h3 style="text-align: start; " id="%E6%8F%90%E4%BE%9B%E8%AF%A6%E7%BB%86%E7%9A%84%E7%8E%AF%E5%A2%83%E4%BF%A1%E6%81%AF"><strong>提供详细的环境信息</strong></h3>
<p style="text-align: start; ">一般来说，目前 GitHub 上的项目中都会包含 Issue 模板，会提示让你填写一些环境信息，按照具体要求填写即可。如果没有，也建议根据软件特性提供必要的信息，比如：浏览器类型及版本、服务器版本、使用的设备、使用的数据库版本等。这往往能帮助维护者快速定位某些在特定条件下才会出现的问题。</p>
<h3 style="text-align: start; " id="%E8%AF%A6%E7%BB%86%E7%9A%84%E5%A4%8D%E7%8E%B0%E6%AD%A5%E9%AA%A4"><strong>详细的复现步骤</strong></h3>
<p style="text-align: start; ">这一步主要告诉维护者出现这个问题的前因后果或者上下文，因为某些问题是必须在特定的操作下才可能复现的，甚至某些问题可能是某种意义上随机出现的，如果不提供复现步骤，维护者可能会非常难以复现。这也往往增加了来回询问的过程。另外，为了更加直观的描述问题，可以提供步骤截图，GIF 动图，甚至视频（目前 GitHub 已经支持视频）。</p>
<h3 style="text-align: start; " id="%E6%8F%90%E4%BE%9B%E6%97%A5%E5%BF%97"><strong>提供日志</strong></h3>
<p style="text-align: start; ">日志对于维护者排查问题也是至关重要的，提供日志建议参考以下几点：</p>
<ol>
 <li>
  <p style="">对一些敏感信息脱敏处理，某些软件可能会在日志中打印一些敏感信息，建议提供之前检查一遍并手动脱敏。</p></li>
 <li>
  <p style="">仅截取相关的日志，如果不知道哪部分是相关的，建议手动复现一次这个问题并快速去查阅最新打印的日志。</p></li>
 <li>
  <p style="">不仅仅是软件内提供的日志，如果这个项目是基于浏览器运行的，也可以提供一下浏览器控制台打印的日志。</p></li>
 <li>
  <p style="">提供日志的时候请务必使用 Markdown 的代码块（```）语法包裹，否则日志会非常难以查阅。</p></li>
</ol>
<h3 style="text-align: start; " id="%E5%B7%B2%E7%BB%8F%E5%B0%9D%E8%AF%95%E8%BF%87%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E5%BC%8F"><strong>已经尝试过的解决方式</strong></h3>
<p style="text-align: start; ">如果有自己尝试过解决，建议补充一下。因为维护者可能在不知道的情况下也提供出你已经尝试过的解决方式，这无疑增加了来回交流的次数，一定程度地影响效率。</p>
<h2 style="text-align: start; " id="%E5%90%8E%E7%BB%AD%E8%B7%9F%E8%B8%AA">后续跟踪</h2>
<p style="text-align: start; ">当你提交完成一个 Issue 之后，项目维护者可能并不会及时回复，这也是<strong>异步沟通</strong>的一个特点。但请耐心等待回复，不要在短时间内评论催促，有内容需要补充除外。</p>
<p style="text-align: start; ">当你提交的 Issue 收到回复后，GitHub 会向你的邮箱中发送通知，也可以使用 GitHub 内置的通知功能进行跟进。</p>
<p style="text-align: start; ">几个建议：</p>
<ol>
 <li>
  <p style="">如果收到回复，你的邮箱可能会收到 GitHub 的提醒邮件，虽然 GitHub 支持通过邮件回复 Issue，但请<strong>千万不要</strong>使用邮件回复，因为各个邮件客户端的差异，很可能导致最终的评论混乱，如：<a href="https://github.com/halo-dev/halo/issues/1799#issuecomment-1084632899"><u>https://github.com/halo-dev/halo/issues/1799#issuecomment-1084632899</u></a></p></li>
 <li>
  <p style="">与维护者交流的时候不要像使用 IM 即时交流工具一样交流，尽可能一次提供完整的信息。</p></li>
 <li>
  <p style="">如果问题已经解决，一定要及时在 Issue 中告知，必要的时候也可以提供后续的解决方式。</p></li>
</ol>
<h2 style="text-align: start; " id="%E7%A4%BC%E4%BB%AA%E7%9B%B8%E5%85%B3">礼仪相关</h2>
<ol>
 <li>
  <p style="">不要包含任何的抱怨、吐槽、隐晦等不舒适的话语。即便你说的都对，但可以换一种更加友好的表达方式，仅表述问题，不输出情绪。</p></li>
 <li>
  <p style="">不要要求项目或维护者做任何事。</p></li>
</ol>
<h2 style="text-align: start; " id="%E5%AF%B9%E4%BA%8E%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86%E5%91%98%E6%88%96%E7%BB%B4%E6%8A%A4%E8%80%85">对于项目管理员或维护者</h2>
<h3 style="text-align: start; " id="%E6%8F%90%E4%BE%9B%E4%B8%80%E4%B8%AA%E8%B4%A1%E7%8C%AE%E8%80%85%E6%96%87%E6%A1%A3"><strong>提供一个贡献者文档</strong></h3>
<p style="text-align: start; ">详尽地描述提交 Issue 和 PR 的步骤和要求。</p>
<h3 style="text-align: start; " id="%E6%8F%90%E4%BE%9B-issue-%E6%8F%90%E4%BA%A4%E6%A8%A1%E6%9D%BF"><strong>提供 Issue 提交模板</strong></h3>
<p style="text-align: start; ">目前 GitHub 已经提供了基于 yaml 描述表单的功能，可以提供更加直观的 Issue 提交表单，并支持表单验证，这能更加规范所有 Issue 的内容。你可以针对项目的特点，列出提交 Issue 所需的信息，如：服务器环境、浏览器版本、所使用的设备等。</p>
<p style="text-align: start; ">相关链接：</p>
<ul>
 <li>
  <p style="">GitHub 官方文档：<a rel="nofollow" href="https://docs.github.com/cn/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#creating-issue-forms" class="external-link"><u>为仓库配置议题模板 - GitHub Docs</u></a></p></li>
 <li>
  <p style="">使用案例：<a rel="nofollow" href="https://github.com/halo-dev/halo/issues/new?assignees=&amp;labels=bug&amp;template=bug_report.zh.yml" class="external-link"><u>New Issue · halo-dev/halo (</u></a><a href="http://github.com"><u>github.com</u></a><a rel="nofollow" href="https://github.com/halo-dev/halo/issues/new?assignees=&amp;labels=bug&amp;template=bug_report.zh.yml" class="external-link"><u>)</u></a></p></li>
 <li>
  <p style="">使用案例：<a rel="nofollow" href="https://github.com/metersphere/metersphere/issues/new?assignees=youliyuan-fit2cloud&amp;labels=%E7%B1%BB%E5%9E%8B%3A+%E7%BC%BA%E9%99%B7&amp;template=bug.yml&amp;title=%5BBUG%5D" class="external-link"><u>New Issue · metersphere/metersphere (</u></a><a href="http://github.com"><u>github.com</u></a><a rel="nofollow" href="https://github.com/metersphere/metersphere/issues/new?assignees=youliyuan-fit2cloud&amp;labels=%E7%B1%BB%E5%9E%8B%3A+%E7%BC%BA%E9%99%B7&amp;template=bug.yml&amp;title=%5BBUG%5D" class="external-link"><u>)</u></a></p></li>
 <li>
  <p style="">使用案例：<a rel="nofollow" href="https://github.com/home-assistant/core/issues/new?assignees=&amp;labels=&amp;template=bug_report.yml" class="external-link"><u>New Issue · home-assistant/core (</u></a><a href="http://github.com"><u>github.com</u></a><a rel="nofollow" href="https://github.com/home-assistant/core/issues/new?assignees=&amp;labels=&amp;template=bug_report.yml" class="external-link"><u>)</u></a></p></li>
</ul>
<h3 style="text-align: start; " id="%E8%B7%9F%E8%B8%AA-issue"><strong>跟踪 Issue</strong></h3>
<ol>
 <li>
  <p style="">及时排查问题，给 Issue 打上对应的标签。</p></li>
 <li>
  <p style="">不建议将 issue 当做工单使用。</p></li>
 <li>
  <p style="">不要回复之后立马关闭 issue，可以等待提交 issue 的作者回复。</p></li>
 <li>
  <p style="">不要在解决这个 issue 之前就关闭 issue。</p></li>
 <li>
  <p style="">在任何时候，都不建议直接删除 issue 或者 issue 回复。除非十分有必要，比如留下了敏感信息。</p></li>
 <li>
  <p style="">在 Issue 中交流的时候不要像使用 IM 即时交流工具一样交流。</p></li>
 <li>
  <p style="">如果在后续的代码提交中已经修复了此问题，建议在 PR 中关联此 Issue（ 可以使用 <code>Fixes #xxx</code>），并在 Issue 中回复。</p></li>
</ol>
<blockquote>
 <p style="">转载：<strong>Ryan Wang</strong></p>
 <p style="">链接：<a href="https://ryanc.cc/archives/open-source-repo-issue">https://ryanc.cc/archives/open-source-repo-issue</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/tan-tao-ru-he-wei-kai-yuan-xiang-mu-ti-jiao-issue</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FGit.png&amp;size=m" type="image/jpeg" length="0"/><category>Git技术</category><pubDate>Tue, 9 Apr 2024 05:42:00 GMT</pubDate></item><item><title><![CDATA[Docker安装wordpress]]></title><link>https://xiaoming728.com/archives/dockeran-zhuang-wordpress</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E5%AE%89%E8%A3%85wordpress&amp;url=/archives/dockeran-zhuang-wordpress" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6%E5%AE%89%E8%A3%85mysql">前提条件安装Mysql</h3>
<p style="">参考：<a href="https://xiaoming728.com/archives/1702298774737" target="_blank"><strong>Docker环境安装Mysql</strong></a></p>
<h3 style="" id="%E5%AE%89%E8%A3%85wordpress">安装WordPress</h3>
<pre><code>docker run --name wordpress --link mysql:mysql -p 80:80 -d wordpress:latest</code></pre>
<p style="">进入页面进行配置即可。</p>]]></description><guid isPermaLink="false">/archives/dockeran-zhuang-wordpress</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fwordpress.gif&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 8 Apr 2024 02:59:00 GMT</pubDate></item><item><title><![CDATA[EasyPoi动态表头导入]]></title><link>https://xiaoming728.com/archives/easypoidong-tai-biao-tou-dao-ru</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=EasyPoi%E5%8A%A8%E6%80%81%E8%A1%A8%E5%A4%B4%E5%AF%BC%E5%85%A5&amp;url=/archives/easypoidong-tai-biao-tou-dao-ru" width="1" height="1" alt="" style="opacity:0;">
<p style="">独特的功能</p>
<ul>
 <li>
  <p style="">基于注解的导入导出,修改注解就可以修改Excel</p></li>
 <li>
  <p style="">支持常用的样式自定义</p></li>
 <li>
  <p style="">基于map可以灵活定义的表头字段</p></li>
 <li>
  <p style="">支持一堆多的导出,导入</p></li>
 <li>
  <p style="">支持模板的导出,一些常见的标签,自定义标签</p></li>
 <li>
  <p style="">支持HTML/Excel转换,如果模板还不能满足用户的变态需求,请用这个功能</p></li>
 <li>
  <p style="">支持word的导出,支持图片,Excel</p></li>
</ul>
<h3 style="" id="%E5%BC%95%E5%85%A5pom.xml">引入pom.xml</h3>
<p style=""><span color="#dc2626" style="color: #dc2626">注：版本小于 4.5.0 存在读取日期格式为 null 的情况</span></p>
<pre><code>&lt;!-- EasyPoi --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;cn.afterturn&lt;/groupId&gt;
    &lt;artifactId&gt;easypoi-spring-boot-starter&lt;/artifactId&gt;
    &lt;version&gt;4.5.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<h3 style="" id="%E4%BB%A3%E7%A0%81%E7%BC%96%E5%86%99">代码编写</h3>
<h4 style="" id="%E6%A0%B9%E6%8D%AE%E4%B8%8D%E5%90%8Cexcel%E6%A0%BC%E5%BC%8F%E8%AF%BB%E5%8F%96%E6%95%B0%E6%8D%AE">根据不同Excel格式读取数据</h4>
<pre><code class="language-java">try {
    if (file.getOriginalFilename().endsWith(".xls") || file.getOriginalFilename().endsWith(".xlsx") ||
            file.getOriginalFilename().endsWith(".XLS") || file.getOriginalFilename().endsWith(".XLSX")) {
        // 如果是xls、xlsx使用ExcelImportUtil.importExcel方法
        ImportParams params = new ImportParams();
        params.setTitleRows(template.getTitleLine());
        params.setHeadRows(template.getDataLine());
        // 获取excel sheet数量
        Workbook hssfWorkbook = WorkbookFactory.create(file.getInputStream());
        params.setSheetNum(hssfWorkbook.getNumberOfSheets());
        data = ExcelImportUtil.importExcel(file.getInputStream(), Map.class, params);
    } else if (file.getOriginalFilename().endsWith(".csv") || file.getOriginalFilename().endsWith(".CSV")   ) {
        // 如果是csv使用CsvImportUtil.importCsv方法
        CsvImportParams csvImportParams = new CsvImportParams();
        csvImportParams.setTitleRows(template.getTitleLine());
        csvImportParams.setHeadRows(template.getDataLine());
        data = CsvImportUtil.importCsv(file.getInputStream(), Map.class, csvImportParams);
    } else {
        throw new ServiceException("文件格式错误");
    }
} catch (Exception e) {
    e.printStackTrace();
    throw new ServiceException("文件格式错误");
}</code></pre>
<h4 style="" id="%E8%8E%B7%E5%8F%96%E5%8A%A8%E6%80%81%E8%A1%A8%E5%A4%B4">获取动态表头</h4>
<pre><code class="language-java">// 根据template获取表头信息再解析数据
List&lt;ExcelExportEntity&gt; entityList = new ArrayList&lt;&gt;();
List&lt;JSONObject&gt; templateJsonArray = JSON.parseArray(template.getJson(), JSONObject.class);
// templateJsonArray结构 [{"fieldName":"store","fieldValue":"门店名称","templateTitle":"业务机构名称"}]
for (JSONObject entries : templateJsonArray) {
    entityList.add(new ExcelExportEntity(entries.get("templateTitle").toString(), entries.get("fieldName")));
}</code></pre>
<h4 style="" id="%E6%A0%B9%E6%8D%AE%E8%A1%A8%E5%A4%B4%E8%A7%A3%E6%9E%90%E6%95%B0%E6%8D%AE">根据表头解析数据</h4>
<pre><code>for (Map&lt;String, Object&gt; map : data) {
    JSONObject jsonObject = new JSONObject();
    JSONObject nameJson = new JSONObject();
    for (ExcelExportEntity entity : entityList) {
        jsonObject.put(entity.getKey().toString(), map.get(entity.getName()));
        nameJson.put(entity.getKey().toString(), entity.getName());
    } 
...</code></pre>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/easypoidong-tai-biao-tou-dao-ru</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FEasyPoi-h3g0.png&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Sun, 7 Apr 2024 07:29:00 GMT</pubDate></item><item><title><![CDATA[Git小白专场: Merge the incoming changes into the current branc和Rebase the current branch on top of the]]></title><link>https://xiaoming728.com/archives/xiao-bai-zhuan-chang-merge-git</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Git%E5%B0%8F%E7%99%BD%E4%B8%93%E5%9C%BA%3A%20Merge%20the%20incoming%20changes%20into%20the%20current%20branc%E5%92%8CRebase%20the%20current%20branch%20on%20top%20of%20the&amp;url=/archives/xiao-bai-zhuan-chang-merge-git" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">原文链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_39463175/article/details/119636762/">https://blog.csdn.net/qq_39463175/article/details/119636762/</a></p>
 <p style="">参考链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.atlassian.com/git/tutorials/merging-vs-rebasing#the-golden-rule-of-rebasing">https://www.atlassian.com/git/tutorials/merging-vs-rebasing#the-golden-rule-of-rebasing</a></p>
</blockquote>
<h1 style="" id="%E4%B8%80%E3%80%81%E8%83%8C%E6%99%AF%E4%BB%8B%E7%BB%8D">一、背景介绍</h1>
<p style="">使用idea更新代码时，有2个选项，一个是Merge the incoming changes into the current branch， 另一个是Rebase the current branch on top of the incoming changes。由于是多人多分支开发，笔者经常用的是Rebase这个选项。也不知道为什么用这个，看着别人也是用这个，不懂为什么要用rebase，而不用merge；在后面介绍过程中，会涉及到git merge的使用，以及git rebase对应的黄金规则，优缺点等等，对于git merge的初步学习和深入理解还是很有必要的。</p>
<p style="">总之，新手用merge！</p>
<p style=""><strong>老手用rebase!</strong></p>
<p style=""><strong>老手用rebase!</strong></p>
<p style=""><strong>老手用rebase!</strong></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-zpbg.png&amp;size=m" style="display: inline-block"></p>
<h1 style="" id="%E4%BA%8C%E3%80%81merge-the-incoming-changes-into-the-current-branch">二、Merge the incoming changes into the current branch</h1>
<p style="">merge这个命令我比较熟悉，就是合并分支，自己也经常用过。那我们就从merge介绍开始，然后在说rebase。</p>
<p style="">一般在开发过程中，具体的步骤如下（下面是一个简单说明）：</p>
<h3 style="" id="1-%E4%B8%8B%E8%BD%BD%E9%A1%B9%E7%9B%AE%E4%BB%A3%E7%A0%81">1 下载项目代码</h3>
<p style="">git clone xxxx</p>
<h3 style="" id="2-%E6%96%B0%E5%BB%BA%E4%B8%80%E4%B8%AA%E8%87%AA%E5%B7%B1%E7%9A%84%E5%88%86%E6%94%AF(dev)">2 新建一个自己的分支(dev)</h3>
<p style="">git checkout -b dev</p>
<h3 style="" id="3-%E7%84%B6%E5%90%8E%E5%9C%A8%E8%87%AA%E5%B7%B1%E6%9C%AC%E5%9C%B0%E8%BF%9B%E8%A1%8C%E5%BC%80%E5%8F%91%E6%B5%8B%E8%AF%95">3 然后在自己本地进行开发测试</h3>
<h3 style="" id="4-%E7%8E%B0%E5%9C%A8%E9%9C%80%E8%A6%81%E5%90%88%E5%B9%B6%E5%9B%A2%E9%98%9F%E7%9A%84%E6%9C%80%E6%96%B0%E4%BB%A3%E7%A0%81%EF%BC%88%E9%BB%98%E8%AE%A4%E5%9C%A8master%E4%B8%8A%EF%BC%89%2C-%E4%B8%8B%E9%9D%A2%E5%91%BD%E4%BB%A4%E6%98%AF%E6%8A%8Amaster%E7%9A%84%E4%BB%A3%E7%A0%81%E5%90%88%E5%B9%B6%E5%88%B0dev%E4%B8%8A%EF%BC%8C%E5%A6%82%E6%9E%9C%E6%9C%89%E5%86%B2%E7%AA%81%E5%B0%B1%E8%A7%A3%E5%86%B3%E5%86%B2%E7%AA%81%E3%80%82">4 现在需要合并团队的最新代码（默认在master上）, 下面命令是把master的代码合并到dev上，如果有冲突就解决冲突。</h3>
<p style="">git merge master</p>
<h3 style="" id="5-%E6%9C%80%E5%90%8Egit-push-dev%2C-%E6%89%A7%E8%A1%8Cmr%2C%E5%8D%B3%E6%89%A7%E8%A1%8C%E5%90%88%E5%B9%B6%E8%AF%B7%E6%B1%82%EF%BC%8Cmaster%E5%88%86%E6%94%AF%E4%B8%BB%E4%BA%BA%E5%90%8C%E6%84%8F%E5%90%8E%EF%BC%8C%E4%BD%A0%E7%9A%84%E6%9B%B4%E6%96%B0%E4%BB%A3%E7%A0%81%E5%B0%B1%E4%B8%8A%E5%8E%BB%E4%BA%86%E3%80%82">5 最后git push dev, 执行MR,即执行合并请求，master分支主人同意后，你的更新代码就上去了。</h3>
<p style="">Merge the incoming changes into the current branch:</p>
<p style="">select this option to perform merge during the update. This is equivalent to running git fetch and then git merge, or git pull --no-rebase.</p>
<p style="">以上知识了解后，言归正传；下面开始正式介绍：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-squz.png&amp;size=m" style="display: inline-block"></p>
<p style="">现在你在Feature分支，其他团队人员在main分支上进行开发，他们也会进行代码更新和commit等等操作。现在你开始merge main.</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-wczf.png&amp;size=m" style="display: inline-block"></p>
<p style="">Main分支代码更新到你的分支了。可以看到图中，产生了一个新的commit node节点信息，此时会多一个commit信息。</p>
<p style="">好处：merge 是非常好的，因为它是非破坏性的操作。现有的分支不会以任何方式改变。这避免了rebase(将在下面讨论)的所有潜在缺陷。</p>
<p style="">坏处：如果main分支是非常活跃的(即更新频繁)，这可能会污染你的featue分支, 因为提交的commit历史很多。虽然使用高级的git log选项可以缓解这个问题，但它会让其他开发人员难以理解项目的历史。</p>
<p style="">这个坏处比较难理解，笔者用实际例子说明：现在A项目有7个分支，每个分支的历史本来应该是不同的，因为不同的人在不同分支。</p>
<p style="">但是现在，项目历史特别乱,多个分支历史互相交叠，每个分支上历史基本上都是一样的（这就是merge的坏处）：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-rhyy.png&amp;size=m" style="display: inline-block"></p>
<p style="">再来说一下rebase, 这个命令会始终把你最新的修改放到最前头。比如你对主branch进行rebase以后, 你的所有修改就会在主branch当前所有的修改之前。你会更有信心保证你的代码运行畅通无阻。通过你自己的测试以后, 你就可以放心的把代码合并到主的branch里面了。</p>
<h1 style="" id="%E4%B8%89%E3%80%81rebase-the-current-branch-on-top-of-the-incoming-changes">三、Rebase the current branch on top of the incoming changes</h1>
<p style="">关于git rebase，首先要知道的是，它解决的是与git merge相同的问题。这两个命令的设计目的都是将一个分支的更改合并到另一个分支——它们只是采用了不同的方式。</p>
<h3 style="" id="1-%E5%88%87%E6%8D%A2%E5%88%B0feature%E5%88%86%E6%94%AF">1 切换到feature分支</h3>
<p style="">git checkout feature</p>
<h3 style="" id="2-%E5%90%88%E5%B9%B6main%E5%88%86%E6%94%AF%E5%86%85%E5%AE%B9%E5%88%B0feature">2 合并main分支内容到feature</h3>
<p style="">git rebase main</p>
<p style=""><strong>Rebase the current branch on top of the incoming changes:</strong></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-mlne.png&amp;size=m" style="display: inline-block"></p>
<p style="">select this option to perform rebase during the update. This is equivalent to running git fetch and then git rebase, or git pull --rebase (all local commits will be put on top of the updated upstream head).</p>
<p style="">The major benefit of rebasing is that you get a much cleaner project history.</p>
<p style="">First, it eliminates the unnecessary merge commits required by git merge. （历史信息少了）</p>
<p style="">Second, as you can see in the above diagram, rebasing also results in a perfectly linear project history—you can follow the tip of feature all the way to the beginning of the project without any forks. This makes it easier to navigate your project with commands like git log, git bisect, and gitk. （项目开发历史更线性，不会产生其他分叉-forks，方便其他操作，如查看历史）</p>
<p style="">该命令会合并分支的commit历史，以达到简化历史信息的作用。总的来说，此时，你的个人分支 是一条线，没有其他分叉会干扰你。原理是基于Main分支，把你的3个变更 commit node 加到 main分支的后面，这样做后，你的的代码在最新的节点。后续还可以，使用 git rebase main -i 的命令进行交互控制，控制历史commit 记录节点 的 压缩，删除，移动顺序等操作。</p>
<p style="">下面是关于如何平衡安全性和可追踪行介绍：</p>
<p style="">there are two trade-offs for this pristine commit history: safety and traceability.</p>
<p style="">If you don’t follow the Golden Rule of Rebasing, re-writing project history can be potentially catastrophic for your collaboration workflow. And, less importantly, rebasing loses the context provided by a merge commit—you can’t see when upstream changes were incorporated into the feature.（个人理解：使用rebase命令会合并其他分支的历史进入到feature分支，失去了合并前的上下文信息，可能会带来潜在的灾难，因为你不知道合并前，main分支的修改到底做了什么，因为一些信息被合并了（缩减了提交历史））</p>
<h2 style="" id="%E5%B0%8F%E8%8A%82">小节</h2>
<p style="">当我们开发一个功能时，可能会在本地有无数次commit，而实际上，你只想master分支上显示一次提交记录就好了，其他的提交记录并不想保留在你的master分支上，那么使用rebase吧，rebase可以将本地多次的commit合并成一个commit，还可以修改commit的描述等</p>
<h1 style="" id="%E5%9B%9B%E3%80%81rebase%E4%BD%BF%E7%94%A8%E7%9A%84%E9%BB%84%E9%87%91%E8%A7%84%E5%88%99">四、rebase使用的黄金规则</h1>
<p style="">The Golden Rule of Rebasing，即什么时候适合用rebase</p>
<p style="">The golden rule of git rebase is to never use it on public branches. 不要使用rebase 命令在公共分支。以下面这个图说明：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-dqfq.png&amp;size=m" style="display: inline-block"></p>
<p style="">使用rebase 命令在公共分支后，细心的读者会发现（前后2张图对比），your main 分支在分支历史开头了。</p>]]></description><guid isPermaLink="false">/archives/xiao-bai-zhuan-chang-merge-git</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FGit.png&amp;size=m" type="image/jpeg" length="0"/><category>Git技术</category><pubDate>Mon, 4 Mar 2024 08:07:00 GMT</pubDate></item><item><title><![CDATA[Certbot免费SSL证书]]></title><link>https://xiaoming728.com/archives/certbotmian-fei-sslzheng-shu</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Certbot%E5%85%8D%E8%B4%B9SSL%E8%AF%81%E4%B9%A6&amp;url=/archives/certbotmian-fei-sslzheng-shu" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="certbot">Certbot</h1>
<p style=""><span style="font-size: 16px; color: rgb(128, 128, 128)">Certbot是一款免费且开源的自动化安全证书管理工具，由电子前沿基金会（EFF）开发和维护，是在Linux、Apache和Nginx服务器上配置和管理SSL/TLS证书的一种机制。Certbot可以自动完成域名的认证并安装证书。</span></p>
<p style=""><span style="font-size: 16px; color: rgb(128, 128, 128)"><strong><mark data-color="#fef9c3" style="background-color: #fef9c3; color: inherit">免费证书只有3个月有效期，所以需要写自动脚本更新。</mark></strong></span></p>
<h1 style="" id="%E5%AE%98%E7%BD%91%E5%9C%B0%E5%9D%80">官网地址</h1>
<pre><code>https://certbot.eff.org/instructions?ws=nginx&amp;os=centosrhel7</code></pre>
<h1 style="" id="%E7%AC%AC%E4%B8%80%E7%A7%8D%E6%96%B9%E6%A1%88">第一种方案</h1>
<h2 style="" id="%E5%AE%89%E8%A3%85snapd-%EF%BC%88%E6%B3%A8%E6%84%8F%EF%BC%9A%E6%AF%8F%E4%B8%80%E6%AD%A5%E6%89%A7%E8%A1%8C%E6%85%A2%E4%B8%80%E7%82%B9%EF%BC%89">安装snapd （注意：每一步执行慢一点）</h2>
<pre><code>sudo yum install snapd
sudo systemctl enable --now snapd.socket
sudo ln -s /var/lib/snapd/snap /snap</code></pre>
<h2 style="" id="%E5%AE%89%E8%A3%85certbot%EF%BC%88%E6%B3%A8%E6%84%8F%EF%BC%9A%E6%AF%8F%E4%B8%80%E6%AD%A5%E6%89%A7%E8%A1%8C%E6%85%A2%E4%B8%80%E7%82%B9%EF%BC%89">安装certbot（注意：每一步执行慢一点）</h2>
<pre><code>sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot</code></pre>
<h2 style="" id="%E8%BF%90%E8%A1%8C%E6%AD%A4%E5%91%BD%E4%BB%A4%E6%9D%A5%E8%8E%B7%E5%8F%96%E8%AF%81%E4%B9%A6%EF%BC%8C%E5%B9%B6%E8%AE%A9-certbot-%E8%87%AA%E5%8A%A8%E7%BC%96%E8%BE%91%E6%82%A8%E7%9A%84-nginx-%E9%85%8D%E7%BD%AE%E6%9D%A5%E4%B8%BA%E5%85%B6%E6%8F%90%E4%BE%9B%E6%9C%8D%E5%8A%A1%EF%BC%8C%E4%B8%80%E6%AD%A5%E5%8D%B3%E5%8F%AF%E6%89%93%E5%BC%80-https-%E8%AE%BF%E9%97%AE">运行此命令来获取证书，并让 Certbot 自动编辑您的 nginx 配置来为其提供服务，一步即可打开 HTTPS 访问</h2>
<pre><code>sudo certbot --nginx</code></pre>
<h2 style="" id="%E6%88%96%E8%80%85%E7%9B%B4%E6%8E%A5%E6%8B%BF%E4%B8%AA%E8%AF%81%E4%B9%A6%EF%BC%8C%E6%89%8B%E5%8A%A8%E6%9B%B4%E6%94%B9nginx">或者直接拿个证书，手动更改nginx</h2>
<pre><code>sudo certbot certonly --nginx</code></pre>
<h2 style="" id="%E6%B5%8B%E8%AF%95%E8%87%AA%E5%8A%A8%E7%BB%AD%E8%AE%A2">测试自动续订</h2>
<pre><code>sudo certbot renew --dry-run</code></pre>
<h1 style="" id="%E7%AC%AC%E4%BA%8C%E7%A7%8D%E6%96%B9%E6%A1%88">第二种方案</h1>
<blockquote>
 <p style=""><a rel="noopener noreferrer nofollow" href="https://unix.stackexchange.com/questions/744633/how-to-install-certbot-via-snap-on-amazon-linux-2023" target="_blank">https://unix.stackexchange.com/questions/744633/how-to-install-certbot-via-snap-on-amazon-linux-20</a></p>
</blockquote>
<blockquote>
 <p style=""><a href="https://certbot.eff.org/instructions?ws=nginx&amp;os=pip" target="_blank">https://certbot.eff.org/instructions?ws=nginx&amp;os=pip</a></p>
</blockquote>
<pre><code class="language-bash"># create an isolated python environment for certbot purposes alone
python3 -m venv /opt/certbot

# Modify environment for the current shell only to make python modify
# the virtual environment and not your system libraries
source /opt/certbot/bin/activate

# Install certbot
sudo pip install certbot certbot-nginx</code></pre>
<h2 style="" id="%E8%BF%9B%E5%85%A5-python-%E8%99%9A%E6%8B%9F%E7%A9%BA%E9%97%B4">进入 Python 虚拟空间</h2>
<pre><code class="language-bash">source /opt/certbot/bin/activate</code></pre>
<h2 style="" id="%E8%BF%90%E8%A1%8C%E6%AD%A4%E5%91%BD%E4%BB%A4%E6%9D%A5%E8%8E%B7%E5%8F%96%E8%AF%81%E4%B9%A6%EF%BC%8C%E5%B9%B6%E8%AE%A9-certbot-%E8%87%AA%E5%8A%A8%E7%BC%96%E8%BE%91%E6%82%A8%E7%9A%84-nginx-%E9%85%8D%E7%BD%AE%E6%9D%A5%E4%B8%BA%E5%85%B6%E6%8F%90%E4%BE%9B%E6%9C%8D%E5%8A%A1%EF%BC%8C%E4%B8%80%E6%AD%A5%E5%8D%B3%E5%8F%AF%E6%89%93%E5%BC%80-https-%E8%AE%BF%E9%97%AE-1">运行此命令来获取证书，并让 Certbot 自动编辑您的 nginx 配置来为其提供服务，一步即可打开 HTTPS 访问</h2>
<pre><code>sudo certbot --nginx</code></pre>
<h2 style="" id="%E6%88%96%E8%80%85%E7%9B%B4%E6%8E%A5%E6%8B%BF%E4%B8%AA%E8%AF%81%E4%B9%A6%EF%BC%8C%E6%89%8B%E5%8A%A8%E6%9B%B4%E6%94%B9nginx-1">或者直接拿个证书，手动更改nginx</h2>
<pre><code>sudo certbot certonly --nginx</code></pre>
<h2 style="" id="%E6%B5%8B%E8%AF%95%E8%87%AA%E5%8A%A8%E7%BB%AD%E8%AE%A2-1">测试自动续订</h2>
<pre><code>sudo certbot renew --dry-run</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/certbotmian-fei-sslzheng-shu</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FCertbot.png&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Wed, 7 Feb 2024 23:16:00 GMT</pubDate></item><item><title><![CDATA[微信公众号之扫码关注后自动登录]]></title><link>https://xiaoming728.com/archives/wei-xin-gong-zhong-hao-zhi-sao-ma-guan-zhu-hou-zi-dong-deng-lu</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E4%B9%8B%E6%89%AB%E7%A0%81%E5%85%B3%E6%B3%A8%E5%90%8E%E8%87%AA%E5%8A%A8%E7%99%BB%E5%BD%95&amp;url=/archives/wei-xin-gong-zhong-hao-zhi-sao-ma-guan-zhu-hou-zi-dong-deng-lu" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">转载自<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/Zack_tzh/article/details/116005482">https://blog.csdn.net/Zack_tzh/article/details/116005482</a></p>
</blockquote>
<p style=""></p>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(85, 86, 102)">原来公司的官网就支持账号密码、手机验证码、QQ扫码授权、微信扫码授权等多种登录方式。昨天分享了</span><a target="_blank" rel="nofollow" href="https://editor.csdn.net/md/?articleId=115898016">微信公众号之扫码登录</a><span style="font-size: 16px; color: rgb(85, 86, 102)">，今天接到需求说要为了咋们的公众号涨粉，要扫码后关注公众号后才可以登录，我一想这不是耍流氓嘛，登个网站还非得我关注。。。算了，产品爸爸说了算。</span></p>
</blockquote>
<h1 style="" id="%E4%B8%80%E3%80%81%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C"><strong>一、准备工作</strong></h1>
<p style="text-align: start; ">查阅了微信公众号开发文档，发现可以用<a target="_blank" rel="nofollow" href="https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html">微信生成带参数二维码</a>实现。看看人家的介绍</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-tpzk.png&amp;size=m" style="display: inline-block"></p>
<p style="text-align: start; ">这不就是我们这个需求准备的接口嘛😏</p>
<h1 style="" id="%E4%BA%8C%E3%80%81%E6%95%B4%E4%B8%AA%E6%B5%81%E7%A8%8B%E8%AF%B4%E6%98%8E">二、整个流程说明</h1>
<p style="">先来看看整个流程的时序图</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-aoff.png&amp;size=m" style="display: inline-block"></p>
<p style="">然后咋们来捋一下整个流程</p>
<p style="">先是用户来访问咋们的网站，然后点击微信登录。</p>
<p style="">这是后我们肯定是要给他一个二维码扫，肯是需要去请求微信开放平台的，先获取token，然后获取我们想要的二维码给用户。</p>
<p style="">然后用户扫码关注后，我们就会收到微信的回调事件，我们只需要处理这个回调事件就知道这个用户登录了。然后获取该用户的基本信息完成登录。</p>
<p style="">这里有的小伙伴就迷糊了，会想我收到回调怎么知道是谁登录了，这个二维码扫码的页面怎知道登录成功了没啊。这里我们需要用到带参二维码里的场景值，只要我们在场景值中放入一个uuid就可以区分了，接受到回调的时候就把uuid作为key缓存起来，然后在登录页面去一直轮询访问这个uuid对应的是否有人登录就行了。是不是很简单。哈哈哈哈</p>
<h1 style="" id="%E4%B8%89%E3%80%81%E5%85%B7%E4%BD%93%E6%8E%A5%E5%8F%A3"><strong>三、具体接口</strong></h1>
<h2 style="" id="%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E7%94%9F%E6%88%90%E4%B8%B4%E6%97%B6%E5%B8%A6%E5%8F%82%E4%BA%8C%E7%BB%B4%E7%A0%81"><strong>第一步：生成临时带参二维码</strong></h2>
<pre><code>http请求方式: POST URL: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN POST数据格式：json POST数据例子：{"expire_seconds": 604800, "action_name": "QR_SCENE", "action_info": {"scene": {"scene_id": 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数：{"expire_seconds": 604800, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}</code></pre>
<p style=""></p>
<ul>
 <li>
  <p style=""><strong>参数说明</strong></p>
  <table>
   <tbody>
    <tr>
     <th colspan="1" rowspan="1" colwidth="94">
      <p style=""><strong><span style="font-size: 14px; color: rgb(79, 79, 79)">参数</span></strong></p></th>
     <th colspan="1" rowspan="1" colwidth="680">
      <p style=""><strong><span style="font-size: 14px; color: rgb(79, 79, 79)">说明</span></strong></p></th>
    </tr>
    <tr>
     <td colspan="1" rowspan="1" colwidth="94" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px; font-weight: normal; border: 1px solid rgb(221, 221, 221); overflow-wrap: break-word; font-size: 14px; color: rgb(79, 79, 79); line-height: 22px; vertical-align: middle; word-break: normal !important;">
      <p style="">expire_seconds</p></td>
     <td colspan="1" rowspan="1" colwidth="680">
      <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">该二维码有效时间，以秒为单位。 最大不超过2592000（即30天），此字段如果不填，则默认有效期为30秒。</span></p></td>
    </tr>
    <tr>
     <td colspan="1" rowspan="1" colwidth="94">
      <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">action_name</span></p></td>
     <td colspan="1" rowspan="1" colwidth="680">
      <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">二维码类型，QR_SCENE为临时的整型参数值，QR_STR_SCENE为临时的字符串参数值，QR_LIMIT_SCENE为永久的整型参数值，QR_LIMIT_STR_SCENE为永久的字符串参数值</span></p></td>
    </tr>
    <tr>
     <td colspan="1" rowspan="1" colwidth="94">
      <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">action_info</span></p></td>
     <td colspan="1" rowspan="1" colwidth="680">
      <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">二维码详细信息</span></p></td>
    </tr>
    <tr>
     <td colspan="1" rowspan="1" colwidth="94">
      <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">scene_id</span></p></td>
     <td colspan="1" rowspan="1" colwidth="680">
      <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">场景值ID，临时二维码时为32位非0整型，永久二维码时最大值为100000（目前参数只支持1–100000）</span></p></td>
    </tr>
    <tr>
     <td colspan="1" rowspan="1" colwidth="94">
      <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">scene_str</span></p></td>
     <td colspan="1" rowspan="1" colwidth="680">
      <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">场景值ID（字符串形式的ID），字符串类型，长度限制为1到64</span></p></td>
    </tr>
   </tbody>
  </table></li>
</ul>
<p style=""><strong>返回说明</strong></p>
<p style="">正确的Json返回结果:</p>
<pre><code>{"ticket":"gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm
3sUw==","expire_seconds":60,"url":"http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI"}</code></pre>
<table>
 <tbody>
  <tr>
   <th colspan="1" rowspan="1" colwidth="100" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px; font-weight: 700; border: 1px solid rgb(221, 221, 221); overflow-wrap: break-word; font-size: 14px; color: rgb(79, 79, 79); line-height: 22px; vertical-align: middle; word-break: normal !important; background-color: rgb(239, 243, 245);">
    <p style=""><strong>参数</strong></p></th>
   <th colspan="1" rowspan="1" colwidth="713" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px; font-weight: 700; border: 1px solid rgb(221, 221, 221); overflow-wrap: break-word; font-size: 14px; color: rgb(79, 79, 79); line-height: 22px; vertical-align: middle; word-break: normal !important; background-color: rgb(239, 243, 245);">
    <p style=""><strong>说明</strong></p></th>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px; font-weight: normal; border: 1px solid rgb(221, 221, 221); overflow-wrap: break-word; font-size: 14px; color: rgb(79, 79, 79); line-height: 22px; vertical-align: middle; word-break: normal !important;">
    <p style="">ticket</p></td>
   <td colspan="1" rowspan="1" colwidth="713" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px; font-weight: normal; border: 1px solid rgb(221, 221, 221); overflow-wrap: break-word; font-size: 14px; color: rgb(79, 79, 79); line-height: 22px; vertical-align: middle; word-break: normal !important;">
    <p style="">获取的二维码ticket，凭借此ticket可以在有效时间内换取二维码。</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px; font-weight: normal; border: 1px solid rgb(221, 221, 221); overflow-wrap: break-word; font-size: 14px; color: rgb(79, 79, 79); line-height: 22px; vertical-align: middle; word-break: normal !important;">
    <p style="">expire_seconds</p></td>
   <td colspan="1" rowspan="1" colwidth="713" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px; font-weight: normal; border: 1px solid rgb(221, 221, 221); overflow-wrap: break-word; font-size: 14px; color: rgb(79, 79, 79); line-height: 22px; vertical-align: middle; word-break: normal !important;">
    <p style="">该二维码有效时间，以秒为单位。 最大不超过2592000（即30天）。</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px; font-weight: normal; border: 1px solid rgb(221, 221, 221); overflow-wrap: break-word; font-size: 14px; color: rgb(79, 79, 79); line-height: 22px; vertical-align: middle; word-break: normal !important;">
    <p style="">url</p></td>
   <td colspan="1" rowspan="1" colwidth="713" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px; font-weight: normal; border: 1px solid rgb(221, 221, 221); overflow-wrap: break-word; font-size: 14px; color: rgb(79, 79, 79); line-height: 22px; vertical-align: middle; word-break: normal !important;">
    <p style="">二维码图片解析后的地址，开发者可根据该地址自行生成需要的二维码图片</p></td>
  </tr>
 </tbody>
</table>
<p style=""></p>
<h2 style="" id="%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E9%80%9A%E8%BF%87ticket%E6%8D%A2%E5%8F%96%E4%BA%8C%E7%BB%B4%E7%A0%81"><strong>第二步：通过ticket换取二维码</strong></h2>
<p style="">获取二维码ticket后，开发者可用ticket换取二维码图片。请注意，本接口无须登录态即可调用。</p>
<p style=""><strong>请求说明</strong></p>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(85, 86, 102)">HTTP GET请求（请使用https协议）</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET"><span style="font-size: 16px; color: rgb(85, 86, 102)">https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET</span></a><span style="font-size: 16px; color: rgb(85, 86, 102)"> 提醒：TICKET记得进行UrlEncode</span></p>
</blockquote>
<p style=""><strong>返回说明</strong></p>
<p style="">ticket正确情况下，http 返回码是200，是一张图片，可以直接展示或者下载。</p>
<pre><code>HTTP头（示例）如下： Accept-Ranges:bytes Cache-control:max-age=604800 Connection:keep-alive Content-Length:28026 Content-Type:image/jpg Date:Wed, 16 Oct 2013 06:37:10 GMT Expires:Wed, 23 Oct 2013 14:37:10 +0800 Server:nginx/1.4.1</code></pre>
<p style=""></p>
<p style="">错误情况下（如ticket非法）返回HTTP错误码404。</p>
<p style="">第三步：扫描带参数二维码事件</p>
<p style="">用户扫描带场景值二维码时，可能推送以下两种事件：</p>
<ol>
 <li>
  <p style="">如果用户还未关注公众号，则用户可以关注公众号，关注后微信会将带场景值关注事件推送给开发者。</p></li>
 <li>
  <p style="">如果用户已经关注公众号，则微信会将带场景值扫描事件推送给开发者。</p></li>
</ol>
<p style="">1. 用户未关注时，进行关注后的事件推送</p>
<p style="">推送XML数据包示例：</p>
<pre><code>&lt;xml&gt;
  &lt;ToUserName&gt;&lt;![CDATA[toUser]]&gt;&lt;/ToUserName&gt;
  &lt;FromUserName&gt;&lt;![CDATA[FromUser]]&gt;&lt;/FromUserName&gt;
  &lt;CreateTime&gt;123456789&lt;/CreateTime&gt;
  &lt;MsgType&gt;&lt;![CDATA[event]]&gt;&lt;/MsgType&gt;
  &lt;Event&gt;&lt;![CDATA[subscribe]]&gt;&lt;/Event&gt;
  &lt;EventKey&gt;&lt;![CDATA[qrscene_123123]]&gt;&lt;/EventKey&gt;
  &lt;Ticket&gt;&lt;![CDATA[TICKET]]&gt;&lt;/Ticket&gt;
&lt;/xml&gt;</code></pre>
<p style=""><strong>参数说明：</strong></p>
<table>
 <tbody>
  <tr>
   <th colspan="1" rowspan="1" colwidth="100">
    <p style="">参数</p></th>
   <th colspan="1" rowspan="1" colwidth="698">
    <p style="">描述</p></th>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">ToUserName</p></td>
   <td colspan="1" rowspan="1" colwidth="698">
    <p style="">开发者微信号</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">FromUserName</p></td>
   <td colspan="1" rowspan="1" colwidth="698">
    <p style="">发送方帐号（一个OpenID）</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">CreateTime</p></td>
   <td colspan="1" rowspan="1" colwidth="698">
    <p style="">消息创建时间 （整型）</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">MsgType</p></td>
   <td colspan="1" rowspan="1" colwidth="698">
    <p style="">消息类型，event</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">Event</p></td>
   <td colspan="1" rowspan="1" colwidth="698">
    <p style="">事件类型，subscribe</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">EventKey</p></td>
   <td colspan="1" rowspan="1" colwidth="698">
    <p style="">事件KEY值，qrscene_为前缀，后面为二维码的参数值</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">Ticket</p></td>
   <td colspan="1" rowspan="1" colwidth="698">
    <p style="">二维码的ticket，可用来换取二维码图片</p></td>
  </tr>
 </tbody>
</table>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/wei-xin-gong-zhong-hao-zhi-sao-ma-guan-zhu-hou-zi-dong-deng-lu</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FWeChat.webp&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Thu, 25 Jan 2024 07:52:00 GMT</pubDate></item><item><title><![CDATA[Mac安装HomeBrew]]></title><link>https://xiaoming728.com/archives/macan-zhuang-homebrew</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Mac%E5%AE%89%E8%A3%85HomeBrew&amp;url=/archives/macan-zhuang-homebrew" width="1" height="1" alt="" style="opacity:0;">
<pre><code>/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/macan-zhuang-homebrew</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FHomeBrew.jpg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Tue, 23 Jan 2024 08:45:00 GMT</pubDate></item><item><title><![CDATA[docker 部署 禅道的搭建]]></title><link>https://xiaoming728.com/archives/docker-bu-shu-shan-dao-de-da-jian</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=docker%20%E9%83%A8%E7%BD%B2%20%E7%A6%85%E9%81%93%E7%9A%84%E6%90%AD%E5%BB%BA&amp;url=/archives/docker-bu-shu-shan-dao-de-da-jian" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="%E4%B8%80%E3%80%81%E7%A6%85%E9%81%93%E7%AE%80%E4%BB%8B">一、禅道简介</h1>
<p style=""><span style="font-size: 14px">为敏捷团队提供免费的应用程序生命周期资源。</span></p>
<p style=""></p>
<h1 style="" id="%E4%BA%8C%E3%80%81docker%E9%87%8C%E7%A6%85%E9%81%93%E7%9A%84%E6%90%AD%E5%BB%BA">二、docker里禅道的搭建</h1>
<p style="">dokcer安装MinIO命令</p>
<pre><code>sudo docker run --name zentao -p 20080:80 \
-v /data/zentao:/data \
-e MYSQL_INTERNAL=true \
-d hub.zentao.net/app/zentao:latest</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/docker-bu-shu-shan-dao-de-da-jian</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fzentao.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Wed, 17 Jan 2024 15:21:00 GMT</pubDate></item><item><title><![CDATA[IJPay PayPal SpringBoot Demo联调测试]]></title><link>https://xiaoming728.com/archives/ijpay-paypal-springboot-demolian-diao-ce-shi</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=IJPay%20PayPal%20SpringBoot%20Demo%E8%81%94%E8%B0%83%E6%B5%8B%E8%AF%95&amp;url=/archives/ijpay-paypal-springboot-demolian-diao-ce-shi" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">IJPay: <a target="_blank" rel="noopener noreferrer nofollow" href="https://javen205.gitee.io/ijpay/">https://javen205.gitee.io/ijpay/</a></p>
</blockquote>
<h2 style="text-align: start; " id="%E5%AE%8C%E6%95%B4%E7%A4%BA%E4%BE%8B"><strong>完整示例</strong></h2>
<ul>
 <li>
  <p style=""><a target="_blank" rel="noopener noreferrer" href="https://gitee.com/javen205/IJPay/tree/master/IJPay-Demo-SpringBoot"><strong>IJPay-Demo-SpringBoot</strong></a></p></li>
</ul>
<p style="">官方文档中并未详细描述demo运行，下面介绍联调方式</p>
<h2 style="" id="demo">Demo</h2>
<h3 style="" id="1.%E4%B8%8B%E8%BD%BDgit%E4%BB%A3%E7%A0%81%EF%BC%8C%E5%A4%8D%E5%88%B6resources%2Fproduction%E7%9B%AE%E5%BD%95%E5%9C%A8%E5%90%8C%E7%BA%A7resources%2Fdev">1.下载git代码，复制resources/production目录在同级resources/dev</h3>
<h3 style="" id="2.%E4%BF%AE%E6%94%B9paypal.properties">2.修改paypal.properties</h3>
<pre><code>paypal.clientId=xxx
paypal.secret=xxx
paypal.sandBox=true
paypal.domain=http://192.168.0.1 # 这里要注意一定要携带http:// 不然会报错，并且排查不到原因</code></pre>
<p style="">沙盒账号获取方式：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/wulvla020311/article/details/114575982">https://blog.csdn.net/wulvla020311/article/details/114575982</a></p>
<h3 style="" id="3.%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E8%B0%83%E7%94%A8%E5%88%9B%E5%BB%BA%E8%AE%A2%E5%8D%95%E6%8E%A5%E5%8F%A3%2Fpaypal%2Fcreateorder">3.在浏览器调用创建订单接口/payPal/createOrder</h3>
<p style="">回调至PayPal付款页面，<strong><span style="font-size: 18px; color: rgb(37, 99, 235)">登录使用沙盒账号Personal Account的账号密码</span></strong></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-rvpu.png&amp;size=m" width="817px" height="783px" style="display: inline-block">付款后会回到接口，在浏览器展示<strong><span style="font-size: 18px; color: rgb(37, 99, 235)">订单ID</span></strong></p>
<h3 style="" id="4.%E5%8F%AF%E6%9F%A5%E8%AF%A2%E8%AE%A2%E5%8D%95%E4%BF%A1%E6%81%AF">4.可查询订单信息</h3>
<p style="">调用<code>/payPal/queryOrder?id=xxx</code> 查看订单信息</p>
<h3 style="" id="5.%E5%AE%8C%E6%88%90%E8%AE%A2%E5%8D%95">5.完成订单</h3>
<p style="">调用<code>/payPal/captureOrder?id=xxx</code> 确认订单</p>
<h3 style="" id="6.%E6%9F%A5%E8%AF%A2%E5%B7%B2%E5%AE%8C%E6%88%90%E8%AE%A2%E5%8D%95">6.查询已完成订单</h3>
<p style="">调用<code>/payPal/captureQuery?captureId=xxx</code> 查询已完成订单，<span style="font-size: 18px; color: rgb(29, 78, 216)">注意这里需要使用 captureId</span></p>
<h3 style="" id="7.%E9%80%80%E6%AC%BE"><span style="font-size: 18px">7.退款</span></h3>
<p style="">调用<code>/payPal/refund?id=xxx</code> 对订单进行退款，注意这里也需要使用<span style="font-size: 18px; color: rgb(29, 78, 216)">captureId，并且需要完成订单后3分钟才可以退款</span></p>
<p style=""><span style="font-size: 18px">8.退款查询</span></p>
<p style="">调用<code>/payPal/refundQuery?id=xxx</code> 查询退款详情。</p>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/ijpay-paypal-springboot-demolian-diao-ce-shi</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FIJPay.png&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Mon, 15 Jan 2024 10:23:00 GMT</pubDate></item><item><title><![CDATA[专利技术交底书(涉及计算机软件的发明)范例说明]]></title><link>https://xiaoming728.com/archives/zhuan-li-ji-shu-jiao-di-shu-she-ji-ji-suan-ji-ruan-jian-de-fa-ming-fan-li-shuo-ming</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E4%B8%93%E5%88%A9%E6%8A%80%E6%9C%AF%E4%BA%A4%E5%BA%95%E4%B9%A6%28%E6%B6%89%E5%8F%8A%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%BD%AF%E4%BB%B6%E7%9A%84%E5%8F%91%E6%98%8E%29%E8%8C%83%E4%BE%8B%E8%AF%B4%E6%98%8E&amp;url=/archives/zhuan-li-ji-shu-jiao-di-shu-she-ji-ji-suan-ji-ruan-jian-de-fa-ming-fan-li-shuo-ming" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px">来自：</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.fity.cn/"><span style="font-size: 15px">未来往事</span></a></p>
<p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.fity.cn/"><span style="font-size: 15px">https://www.fity.cn/post/599.html</span></a></p>
<p style=""><span style="font-size: 15px">日期：2016-12-08 22:54:01</span></p>
<p style=""></p>
<p style=""><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">专利技术交底书(涉及计算机软件的发明)范例 编写格式要求说明</span></strong><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">发明名称</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">（25个字以内） 简明、准确地表明专利请求保护的主题。名称中不应含有非技术性词语，不得使用商标、型号、人名、地名或商品名称等</span><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">本专利属于以下哪一种：</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">1、纯软件的算法发明，如一个能实现游客在室内外精确定位的导航软件； 2、软硬结合，即通过算法对相关硬件的功能进行改进，如采用某一算法使内存读取速率大为改进。</span><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">一、所属技术领域（即其直接所属或应用的技术领域）</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">本技术所属的技术领域为……</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">示例：本技术所属的技术领域为计算机领域，其涉及到一种GIS应用系统开发中，数据编辑时的图形节点坐标捕捉方式的设计与实现。</span><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">二、背景技术（即现有技术）</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">指对专利的理解、检索、审查有用的技术，可以引证反映这些背景技术的文件。 简要说明与本发明的工艺或方法最接近的现有技术的相关内容，涉及科技术语的话需要给出明确定义。如果可能，客观地指出现有技术（包括市场上的产品、技术、现有已公开的专利文件、公开发表的期刊论文等等）存在的问题和缺点（要着重笔墨针对本专利要重点解决的技术问题、而现有技术中存在的该缺点做描述，其他本专利不能解决的现有技术缺点可以不写），并结合其工艺或原理说明存在这些问题和缺点的原因。引证文献、资料的，应写明其出处；需要引用已公开专利文件的，请写明专利名称与申请号。</span><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">三、本专利要解决的主要技术问题（即发明主要目的）</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">即本专利最重要的改进是解决了现有技术中哪些问题，对应前面的“背景技术”的缺点来写。</span><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">四、技术方案</span><span style="font-size: 15px; color: rgb(255, 0, 0)">【核心部分】</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">是申请人对其要解决的技术问题所采取的技术措施的集合。技术措施通常是由技术特征来体现的。涉及硬件的发明，技术方案应当清楚、完整地说明硬件的形状、构造特征、连接关系，说明技术方案是如何解决技术问题的，必要时应说明技术方案所依据的科学原理。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">技术方案，区别于现有技术，特别是要尽可能清楚、具体详细地描述，特别注意要将方案细化到以本领域内的普通技术人员能够不需发挥创造力便可照本专利实现为准，也就是说，要细化到每个点都是现有技术或公知常识，在理解上不存在模糊和歧义。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">专利中所有技术术语都要尽可能地使用本领域内的通用术语，且尽量注明含义；臆造的术语、公式或算法里出现的代词都要清楚地写明在本专利中指代的含义；注意整篇专利文件中使用的技术术语要统一，一个含义不要出现若干个称呼； </span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">在描述技术方案的每项技术手段（发明点）时，最好说明其在发明中所起的作用；并显著标识出哪些发明点是实现本发明主要目的所必须，哪些仅仅是起到优化本专利某个性能或带来其他附属效果的发明点；任何发明都有所要解决的主要技术问题，从而存在发明点1、2、3……，其中，有些发明点是解决该主要技术问题所必须的，没有它们发明便不可能实现；此外还可能存在一些起优化作用的发明点，没有它们发明也仍能实现、仍可以解决主要技术问题，只不过加上这些发明点能够使本发明更加优化。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">例如，一个带按摩功能的椅子的发明申请，要解决的主要技术问题是如何让椅子有按摩功能。它的必要的发明点：椅子背部安装有按摩部件；它的非必要发明点：该按摩部件是由某某材质组成，从而使使用者更感觉舒适。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">每个发明点的替代方案如果有，也要尽量提出，并显著标识出来；</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">注意：如果是方法发明，在方法流程中涉及到某些具体设备时，避免使用“……模块”、“……系统”等模糊不清的词语（非具体设备，可以用模块、系统作为功能性名称），要尽可能写出设备具体的名称，若该设备是自有设备或其他在市面上买不到的产品，则应指明出处。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">由于算法专利的特殊性，需要将算法流程详尽写出，有需要的可以写明软件的运行环境与技术支持，不需要提供程序代码；在撰写交底书时，可以按自己的思路先写出一个总的算法流程，然后将详细的方案放在后面按顺序撰写，但要标注出其对应总的算法流程的哪一个步骤。</span><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">五、有益效果（技术效果）</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">结合本专利写明其与现有技术相比具有哪些优点？比如安全性、识别性、简易性、普适性、可控性、扩展性等；</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">注意要对应前面的“背景技术”、“本专利要解决的主要技术问题”，将带来的各种有益效果要主次分明地进行描述；</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">最好有数据或图表证明其效果。</span><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">六、附图及附图的简单说明</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">根据情况提供算法流程图，可以包括总流程、分流程等等，并简要写出该附图所表示的内容；</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">只要给出最能够体现本专利的示意图即可，而不必过多的考虑编程技巧、程序的简化等问题；</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">涉及装臵的改进，要提供硬件结构及各个部件连接关系的示意图； 附图中不应有任何填充的颜色、大面积的阴影，均为白底黑线的图案； 不要将图名画进图里，而应在图以外的部分用文字表示； 图里面不要包含域；</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">产品结构图中不要标注构件名称，而应引线出来标上阿拉伯数字，再在说明书文字部分说明各个阿拉伯数字代表的含义，所有附图里的同一部件应用同一阿拉伯数字表示； 附图最好以jpg格式打包发给我，不需粘贴在本页面上，以便后期的修改。</span><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">七、具体实施方式</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">是本发明优选的具体实施例。实施方式应与技术方案相一致，并且应当对权利要求的技术特征给予详细说明，以支持权利要求。如果有多个实施例，每个实施例都必须与本发明所要解决的技术问题及其有益效果相一致。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">对技术方案进行具体详细地描述。这种描述的具体化程度应达到使所属技术领域的普通技术人员按照所描述的内容能够重现本发明。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">若技术方案部分已经详尽，则此部分可以设定具体运行环境、具体条件，代入具体数值，演示本算法程序，并导出最终结果。注意结合附图进行演示与说明。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">注意：通过至少一个实施例具体说明本申请的全部工作过程，务必在实施例中涵盖技术方案中的所有发明点。</span><span style="font-size: 15px"><br><br></span><strong><span style="font-size: 15px; color: rgb(93, 104, 111)">注意：</span></strong><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">1、专利中所有技术术语都要尽可能地使用本领域内的通用术语，且尽量注明含义；</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">臆造的术语、公式或算法里出现的代词都要清楚地写明在本专利中指代的含义；</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">所有含义一样的技术术语请统一名称，全称较长的术语，可以在第一次出现的时候在其后加括号，注明简称，而后统一用简称代替；</span><span style="font-size: 15px"><br><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">2、检查本专利的技术方案是否达到充分公开，即本专利的每个步骤均需细化到都是现有技术或共知常识，使得本领域的一般技术人员仅根据现在的描述，不需发挥创造性思维便可以照着实施本专利，在理解上不存在模糊和歧义；如不能，请补充资料。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">3、对于方法发明，方法步骤中涉及到设备的请清楚、具体地写明设备名称，若不是公知的设备还要注明出处（如生产厂商、该设备的专利信息等），最好不要笼统地称呼为某某模块、某某系统。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(93, 104, 111)">4、本专利中如有涉及或引用其他文献资料（含专利、论文、期刊杂志、书籍等）中的特殊内容，如公式、算法、概念等，需将该特殊内容以及出处罗列出，不能仅标注“请参见……”。</span></p>]]></description><guid isPermaLink="false">/archives/zhuan-li-ji-shu-jiao-di-shu-she-ji-ji-suan-ji-ruan-jian-de-fa-ming-fan-li-shuo-ming</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E5%259B%25BD%25E5%25AE%25B6%25E4%25B8%2593%25E5%2588%25A9.png&amp;size=m" type="image/jpeg" length="0"/><category>技术专利</category><pubDate>Fri, 12 Jan 2024 13:20:00 GMT</pubDate></item><item><title><![CDATA[技术交底软件_软件专利技术交底书撰写要点]]></title><link>https://xiaoming728.com/archives/ji-shu-jiao-di-ruan-jian_ruan-jian-zhuan-li-ji-shu-jiao-di-shu-zhuan-xie-yao-dian</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E6%8A%80%E6%9C%AF%E4%BA%A4%E5%BA%95%E8%BD%AF%E4%BB%B6_%E8%BD%AF%E4%BB%B6%E4%B8%93%E5%88%A9%E6%8A%80%E6%9C%AF%E4%BA%A4%E5%BA%95%E4%B9%A6%E6%92%B0%E5%86%99%E8%A6%81%E7%82%B9&amp;url=/archives/ji-shu-jiao-di-ruan-jian_ruan-jian-zhuan-li-ji-shu-jiao-di-shu-zhuan-xie-yao-dian" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px">来自：CSDN-</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/weixin_39725594"><span style="font-size: 15px">weixin_39725594</span></a></p>
<p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/weixin_39725594"><span style="font-size: 15px">https://blog.csdn.net/weixin_39725594/article/details/111348808</span></a></p>
<p style=""><span style="font-size: 15px">日期：2020-12-15 18:17:05</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">专利交底书是发明人在申请专利时需向专利代理人提供的一份技术文档，是专利代理人与发明人沟通的桥梁。写好专利交底书，不仅可以方便代理人更快速的理解技术方案，提高沟通效率，也能也有效提高专利申请文件的撰写质量。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">对于软件专利来说，发明人在研发过程中形成的文档实际上大多是软件代码。而对于自己的发明如何用自然语言来进行表达，撰写出优质的专利交底书，对于发明人来比发明本身更加困难。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">根据以往平台对接处理软件案过程中的沟通了解，建议发明人可以从以下几个方面来考虑如何撰写软件案的交底书：</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">01克服心态</span></p>
<p style=""><span style="font-size: 15px">专利交底书并没有想象中的那么难写，只要能够将软件的技术方案详尽的讲述出来，就能写好一份软件案的交底书。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">代理人需要清楚掌握的技术方案，才能撰写出一份高质量的专利申请文件。因此，撰写交底书时发明人只需要全面详实的介绍技术方案即可。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">02需求、代码文档不是技术交底</span></p>
<p style=""><span style="font-size: 15px">软件的发明人在研发过程中通常会形成两种文档：需求文档和代码文档。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">需求文档通常用于挖掘专利。需求的背后通常意味着问题解决问题的方案，而“问题+方案”即构成了专利。需求文档中虽然也会涉及的技术方案相关描述，更多的是用户层面去描述需要做一些什么事情去解决某一问题，其中通常会包含较多与专利无关的内容，易给代理人造成困扰。因此，需求文档不能直接作为交底书。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">代码文档通常被研发人员误认为即是技术方案。但是实际上，软件专利的技术方案并不是代码本身，而是代码在运行时所要实现的步骤。故此，不能用代码的形式去表述软件专利中的技术方案。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">综上，软件专利的交底书既不是需求说明书，也不是代码本身，而是通过文字描述出来的构思。发明人需要采用自然语言去描述软件专利的技术方案。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">03专利交底书需要包含三大部分</span></p>
<p style=""><span style="font-size: 15px">专利是针对现有技术提出的新的技术方案，一个专利的骨架是技术问题、技术手段和有益效果，因此，一份专利交底书至少需要包含这三大部分。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">具体的，可以从以下几个方面撰写软件案的交底书：</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">术语解释</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">由于软件适用于各行各业，因此软件专利涉及到的领域非常多，特别是随着互联网的发展，新生词语层出不穷。因此，需要对该领域的技术词语进行解释说明，如果有英文还需要给出中文注释或解释。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">本发明的发明点概述</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">每一个软件专利都有一定的发明原理，用简单的一段话来描述本发明对现有技术的改进之处，可以让代理人快速理解方案，掌握到本发明的发明点。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">背景技术</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">背景技术是对该项发明之前的技术现状的描述。在描述了现有技术的方案可以引出现有技术存在的缺陷和不足，以提出本发明要解决的技术问题。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">本发明的技术方案</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">这是整个交底书中最重要的部分，需要发明人详细描述是通过怎样的技术手段和方法来解决背景技术中提出的技术问题。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">对于软件案来说，则需要发明人通过自然语言描述软件实现的方法流程。对于方法流程中的每个步骤，需写清楚每个步骤的执行主体，例如是由终端执行还是由服务器来执行。并且在描述每个步骤的具体实现时，可多举例和结合具体的应用场景进行描述。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">在技术方案描述中还需要写清楚数据流向，包括数据的产生、处理及输出等。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">为更清楚的描述技术方案，可以结合各种图来描述，包括有流程图、界面图、时序图、系统架构图、网络拓扑图、原理图和应用环境图等等。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">具体的，根据软件案的类型可包含以下几种情况：</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">如涉及到软件应用产品，可以分别从产品侧和技术侧两个角度来进行描述，产品侧可描述产品即前端的形态(结合界面图)，而技术侧描述后台的数据处理(结合流程图)；</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">如果涉及到多端交互，需要以步骤形式从每一端出发写出该端涉及到的处理(结合时序图)；</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">如果涉及到系统架构，需要描述系统中各个组成部分的作用，各组成部分之间的关系以及各组成部分之间的交互过程(结合系统架构图、网络拓扑图等)。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">如果涉及到公式，需要写出具体的公式形式，并给出公式中每个参数的物理含义；</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">如果涉及到算法，需要以步骤形式写出具体的算法逻辑规则；</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">本发明产生的技术效果</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">这部分是跟技术问题对应的，列出本发明所有能够实现的技术效果即可。</span></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/weixin_39725594/article/details/111348808"><span style="font-size: 15px"><br></span></a></p>]]></description><guid isPermaLink="false">/archives/ji-shu-jiao-di-ruan-jian_ruan-jian-zhuan-li-ji-shu-jiao-di-shu-zhuan-xie-yao-dian</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E5%259B%25BD%25E5%25AE%25B6%25E4%25B8%2593%25E5%2588%25A9.png&amp;size=m" type="image/jpeg" length="0"/><category>技术专利</category><pubDate>Fri, 12 Jan 2024 13:19:00 GMT</pubDate></item><item><title><![CDATA[SpringSecurity授权的三种方法详解]]></title><link>https://xiaoming728.com/archives/springsecurityshou-quan-de-san-zhong-fang-fa-xiang-jie</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=SpringSecurity%E6%8E%88%E6%9D%83%E7%9A%84%E4%B8%89%E7%A7%8D%E6%96%B9%E6%B3%95%E8%AF%A6%E8%A7%A3&amp;url=/archives/springsecurityshou-quan-de-san-zhong-fang-fa-xiang-jie" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%40enableglobalmethodsecurity%E4%B8%89%E6%96%B9%E6%B3%95%E8%AF%A6%E8%A7%A3">@EnableGlobalMethodSecurity三方法详解</h3>
<p style="">要开启<code>Spring</code>方法级安全，在添加了<code>@Configuration</code>注解的类上再添加<code>@EnableGlobalMethodSecurity</code>注解即可</p>
<pre><code>@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
}</code></pre>
<p style="">其中注解<code>@EnableGlobalMethodSecurity</code>有几个方法：</p>
<ul>
 <li>
  <p style=""><code>prePostEnabled</code><strong>：</strong> 确定 前置注解<code>[@PreAuthorize,@PostAuthorize,..]</code> 是否启用</p></li>
 <li>
  <p style=""><code>securedEnabled</code><strong>：</strong> 确定安全注解 <code>[@Secured]</code> 是否启用</p></li>
 <li>
  <p style=""><code>jsr250Enabled</code><strong>：</strong> 确定 <code>JSR-250注解 [@RolesAllowed..]</code>是否启用</p></li>
</ul>
<p style="">在同一个应用程序中，可以启用多个类型的注解，但是只应该设置一个注解对于行为类的接口或者类。如：</p>
<ul>
 <li>
  <p style="">一个程序启用多个类型注解：</p></li>
</ul>
<pre><code>@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true))
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
}</code></pre>
<ul>
 <li>
  <p style="">但是只应该设置一个注解对于行为类的接口或者类</p></li>
</ul>
<pre><code>public interface UserService {
  List&lt;User&gt; findAllUsers();
  @PreAuthorize("hasAnyRole('user')")
  void updateUser(User user);
    // 下面不能设置两个注解，如果设置两个，只有其中一个生效
    // @PreAuthorize("hasAnyRole('user')")
  @Secured({ "ROLE_user", "ROLE_admin" })
  void deleteUser();
}</code></pre>
<h4 style="" id=""></h4>
<h4 style="" id="%E4%B8%80%E3%80%81%E5%90%AF%E7%94%A8securedenabled">一、启用securedEnabled</h4>
<p style="">先启用<code>securedEnabled</code>：</p>
<pre><code>@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true))
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
}</code></pre>
<p style="">在调用的接口或方法使用如下：</p>
<pre><code>public interface UserService {
    List&lt;User&gt; findAllUsers();
    
    @Secured({"ROLE_user"})
    void updateUser(User user);
    @Secured({"ROLE_admin", "ROLE_user1"})
    void deleteUser();
}</code></pre>
<p style=""><code>@Secured</code>注解是用来定义业务方法的安全配置。在需要安全[角色/权限等]的方法上指定 @Secured，并且只有那些角色/权限的用户才可以调用该方法。</p>
<p style=""><code>@Secured</code>缺点（限制）就是不支持<code>Spring EL</code>表达式。不够灵活。并且指定的角色必须以<code>ROLE_</code>开头，不可省略。</p>
<p style="">在上面的例子中，<code>updateUser</code> 方法只能被拥有<code>user</code>权限的用户调用。<code>deleteUser</code> 方法只能够被拥有<code>admin</code> 或者<code>user1</code> 权限的用户调用。而如果想要指定<code>"AND"</code>条件，即调用<code>deleteUser</code>方法需同时拥有<code>ADMIN</code>和<code>DBA</code>角色的用户，<code>@Secured</code>便不能实现。</p>
<p style="">这时就需要使用<code>prePostEnabled</code>提供的注解<code>@PreAuthorize/@PostAuthorize</code></p>
<h4 style="" id=""></h4>
<h4 style="" id="%E4%BA%8C%E3%80%81%E5%90%AF%E7%94%A8prepostenabled">二、启用prePostEnabled</h4>
<p style="">先启用<code>prePostEnabled</code></p>
<pre><code>@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
}</code></pre>
<p style="">在调用的接口或方法使用：</p>
<pre><code>public interface UserService {
    List&lt;User&gt; findAllUsers();
    @PostAuthorize ("returnObject.type == authentication.name")
    User findById(int id);
    @PreAuthorize("hasRole('ADMIN')")
    void updateUser(User user);
    
    @PreAuthorize("hasRole('ADMIN') AND hasRole('DBA')")
    void deleteUser(int id);
}</code></pre>
<p style="">该注解更适合方法级的安全,也支持Spring 表达式语言，提供了基于表达式的访问控制。参见常见<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://docs.spring.io/spring-security/site/docs/4.0.1.RELEASE/reference/htmlsingle/#el-common-built-in">内置表达式</a>了解支持表达式的完整列表</p>
<p style="">上面只使用到了一个注解<code>@PreAuthorize</code>，启用<code>prePostEnabled</code>后，提供有四个注解：</p>
<ul>
 <li>
  <p style=""><code>@PreAuthorize</code><strong>：</strong> 进入方法之前验证授权。可以将登录用户的<code>roles</code>参数传到方法中验证。<br>
   一些用法：</p></li>
</ul>
<pre><code>// 只能user角色可以访问
@PreAuthorize ("hasAnyRole('user')")
// user 角色或者 admin 角色都可访问
@PreAuthorize ("hasAnyRole('user') or hasAnyRole('admin')")
// 同时拥有 user 和 admin 角色才能访问
@PreAuthorize ("hasAnyRole('user') and hasAnyRole('admin')")
// 限制只能查询 id 小于 10 的用户
@PreAuthorize("#id &lt; 10")
User findById(int id);
// 只能查询自己的信息
 @PreAuthorize("principal.username.equals(#username)")
User find(String username);
// 限制只能新增用户名称为abc的用户
@PreAuthorize("#user.name.equals('abc')")
void add(User user)</code></pre>
<ul>
 <li>
  <p style=""><code>@PostAuthorize</code><strong>：</strong> 该注解使用不多，在方法执行后再进行权限验证。 适合验证带有返回值的权限。<code>Spring EL</code> 提供 返回对象能够在表达式语言中获取返回的对象<code>returnObject</code>。如：</p></li>
</ul>
<pre><code>// 查询到用户信息后，再验证用户名是否和登录用户名一致
@PostAuthorize("returnObject.name == authentication.name")
@GetMapping("/get-user")
public User getUser(String name){
    return userService.getUser(name);
}
// 验证返回的数是否是偶数
@PostAuthorize("returnObject % 2 == 0")
public Integer test(){
    // ...
    return id;
}</code></pre>
<ul>
 <li>
  <p style=""><code>@PreFilter</code><strong>：</strong> 对集合类型的参数执行过滤，移除结果为<code>false</code>的元素</p></li>
</ul>
<pre><code>// 指定过滤的参数，过滤偶数
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List&lt;Integer&gt; ids, List&lt;String&gt; username)</code></pre>
<ul>
 <li>
  <p style=""><code>@PostFilter</code><strong>：</strong> 对集合类型的返回值进行过滤，移除结果为<code>false</code>的元素</p></li>
</ul>
<pre><code>@PostFilter("filterObject.id%2==0")
public List&lt;User&gt; findAll(){
    ...
    return userList;
}</code></pre>
<p style="">对于前面使用<code>@Secured</code>注解的缺点，现在使用<code>@PreAuthorize/@PostAuthorize</code>：</p>
<pre><code>public interface UserService {
    List&lt;User&gt; findAllUsers();
    @PostAuthorize ("returnObject.type == authentication.name")
    User findById(int id);
    @PreAuthorize("hasRole('ADMIN')")
    void updateUser(User user);
    
    @PreAuthorize("hasRole('ADMIN') AND hasRole('DBA')")
    void deleteUser(int id);
}</code></pre>
<h5 style="" id="%E5%86%85%E7%BD%AE%E8%A1%A8%E8%BE%BE%E5%BC%8F%EF%BC%9A">内置表达式：</h5>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">hasRole([role])</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">如果有当前角色，则返回true(会自动添加<code>ROLE_</code></p>
    <p style="">前缀)</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">hasAnyRole([role1,role2])</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">如果有任一角色即可通过校验，返回true(会自动添加<code>ROLE_</code></p>
    <p style="">前缀)</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">hasAuthority([authority])</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(38, 38, 38)" style="color: rgb(38, 38, 38)">如果有指定权限，则返回true</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">hasAnyAuthority([authority1,authority2])</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(38, 38, 38)" style="color: rgb(38, 38, 38)">如果有指定任一权限，则返回true</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">principal</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(38, 38, 38)" style="color: rgb(38, 38, 38)">获取当前用户的主体对象</span></p></td>
  </tr>
 </tbody>
</table>
<p style=""></p>
<h4 style="" id="%E4%B8%89%E3%80%81%E5%90%AF%E7%94%A8jsr250enabled">三、启用jsr250Enabled</h4>
<p style="">jsr250Enabled注解比较简单，只有</p>
<ul>
 <li>
  <p style=""><code>@DenyAll</code><strong>：</strong> 拒绝所有访问</p></li>
 <li>
  <p style=""><code>@RolesAllowed({"USER", "ADMIN"})</code><strong>：</strong> 该方法只要具有<code>"USER"</code>, <code>"ADMIN"</code>任意一种权限就可以访问。这里可以省略前缀<code>ROLE_</code>，实际的权限可能是<code>ROLE_ADMIN</code></p></li>
 <li>
  <p style=""><code>@PermitAll</code><strong>：</strong> 允许所有访问</p></li>
</ul>
<p style=""></p>
<h3 style="" id="springsecurity%E4%B8%AD%E8%A7%92%E8%89%B2%E6%9D%83%E9%99%90%E7%9A%84%E5%8C%BA%E5%88%AB">SpringSecurity中角色权限的区别</h3>
<p style="">我们先来看 <a target="_blank" rel="noopener noreferrer nofollow" href="http://docs.spring.io/spring-security/site/docs/4.0.1.RELEASE/reference/htmlsingle/#el-common-built-in">org.springframework.security</a>.core.userdetails.User 中的一段代码：</p>
<pre><code>public UserBuilder roles(String... roles) {
    List&lt;GrantedAuthority&gt; authorities = new ArrayList&lt;&gt;(roles.length);
    for (String role : roles) {
        Assert.isTrue(!role.startsWith("ROLE_"), () -&gt; role
        + " cannot start with ROLE_ (it is automatically added)");
        authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
    }
    return authorities(authorities);
}</code></pre>
<p style="">代码中 UserBuilder 在设置角色的时候会自动在角色前面添加 "ROLE_" 并把设置的角色信息保存到&nbsp;List&lt;GrantedAuthority&gt; 权限集合中，从这看出用户的角色权限信息都保证一个集合中，区别就是用户角色有&nbsp;"ROLE_" 前缀。</p>
<pre><code>@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Set&lt;GrantedAuthority&gt; authorities = new HashSet&lt;&gt;();
    authorities.add(new SimpleGrantedAuthority("ADMIN"));
    User user = new User("admin", "admin123", authorities);
    return user;
}</code></pre>
<p style="">那么我们在鉴权的时候要注意了：如果我们使用&nbsp;<span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">hasRole([role])&nbsp;</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">鉴权，系统</span>会自动添加<code>ROLE_</code>前缀，然后去用户权限集合进行对比，如果我们给用户配置权限是通过上面那个方法 authorities.add(<span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">new </span>SimpleGrantedAuthority(<span fontsize="" color="rgb(106, 135, 89)" style="color: rgb(106, 135, 89)">"ADMIN"</span>)) 配置， 这种方式是鉴权不到用户角色的，设置用户角色的时候需要这样authorities.add(<span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">new </span>SimpleGrantedAuthority(<span fontsize="" color="rgb(106, 135, 89)" style="color: rgb(106, 135, 89)">"ROLE_ADMIN"</span>)) 配置，就可以设置为角色权限了。</p>
<p style="">如果使用&nbsp;<span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">hasAuthority([authority])&nbsp;</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">鉴权，就不用在设置用户权限的时候加</span><code>ROLE_</code>前缀了。</p>]]></description><guid isPermaLink="false">/archives/springsecurityshou-quan-de-san-zhong-fang-fa-xiang-jie</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FSpringSecurity.png&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Fri, 12 Jan 2024 13:18:00 GMT</pubDate></item><item><title><![CDATA[使用seata、sharding-sphere和dubbo搭建框架]]></title><link>https://xiaoming728.com/archives/shi-yong-seata-sharding-spherehe-dubboda-jian-kuang-jia</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E4%BD%BF%E7%94%A8seata%E3%80%81sharding-sphere%E5%92%8Cdubbo%E6%90%AD%E5%BB%BA%E6%A1%86%E6%9E%B6&amp;url=/archives/shi-yong-seata-sharding-spherehe-dubboda-jian-kuang-jia" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1.%E4%BB%8B%E7%BB%8D">1.介绍</h3>
<p style=""></p>
<p style="">本篇将介绍,如何进行seata1.2.0、sharding-sphere4.1.0和dubbo2.7.5 的整合,以及使用nacos作为我们的配置中心和注册中心。如果你还是一个初学者，先建议学习一下，陈建斌的<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">七步带你集成Seata 1.2 高可用搭建</a>，这篇文章清楚的阐述了初学者容易遇到的5个问题，并且都提供完整的解决思路。</p>
<p style=""></p>
<h3 style="" id="2.%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE">2.环境配置</h3>
<p style=""></p>
<ul>
 <li>
  <p style="">mysql: 5.7.12</p></li>
 <li>
  <p style="">nacos: &nbsp;1.2.1</p></li>
 <li>
  <p style="">spring-boot: &nbsp;2.2.6.RELEASE</p></li>
 <li>
  <p style="">seata: 1.2.0</p></li>
 <li>
  <p style="">dubbo:2.7.5</p></li>
 <li>
  <p style="">sharding-sphere: 4.1.0</p></li>
 <li>
  <p style="">开发环境: jdk1.8.0</p></li>
</ul>
<p style=""></p>
<h4 style="" id="2.1-nacos%E5%AE%89%E8%A3%85">2.1 nacos安装</h4>
<p style=""></p>
<p style="">nacos下载：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">https://github.com/alibaba/nacos/releases/tag/1.2.1</a></p>
<p style=""></p>
<p style="">Nacos 快速入门：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">https://nacos.io/en-us/docs/quick-start.html</a></p>
<p style=""></p>
<pre><code>sh startup.sh -m standalone</code></pre>
<p style=""></p>
<p style="">在浏览器打开Nacos web 控制台：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">http://127.0.0.1:8848/nacos/index.html</a></p>
<p style=""></p>
<p style="">输入nacos的账号和密码 分别为<code>nacos：nacos</code></p>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1617845016289-a56266f9-ce63-4e83-8c62-fddbd9a1615d.png&amp;size=m" width="1893" style="display: inline-block"></p>
<p style="">这是时候naocs 就正常启动了。</p>
<p style=""></p>
<h4 style="" id="2.2-seata1.2.0%E5%AE%89%E8%A3%85">2.2 seata1.2.0安装</h4>
<p style=""></p>
<h5 style="" id="2.2.1-%E5%9C%A8-seata-release-%E4%B8%8B%E8%BD%BD%E6%9C%80%E6%96%B0%E7%89%88%E7%9A%84-seata-server-%E5%B9%B6%E8%A7%A3%E5%8E%8B%E5%BE%97%E5%88%B0%E5%A6%82%E4%B8%8B%E7%9B%AE%E5%BD%95%EF%BC%9A">2.2.1 在 <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://github.com/seata/seata/releases/tag/v1.2.0">Seata Release</a> 下载最新版的 Seata Server 并解压得到如下目录：</h5>
<p style=""></p>
<pre><code>.
├──bin
├──conf
└──lib</code></pre>
<p style=""></p>
<h5 style="" id="2.2.2-%E4%BF%AE%E6%94%B9-conf%2Fregistry.conf-%E9%85%8D%E7%BD%AE%EF%BC%8C">2.2.2 修改 conf/registry.conf 配置，</h5>
<p style=""></p>
<p style="">目前seata支持如下的file、nacos 、apollo、zk、consul的注册中心和配置中心。这里我们以<code>nacos</code> 为例。</p>
<p style="">将 type 改为 nacos</p>
<p style=""></p>
<pre><code>registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    namespace = "40508bb4-179e-4c98-a2f1-c2c031c20b3c"
    cluster = "default"
    username = "worker2"
    password = "xxxxxxx"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "40508bb4-179e-4c98-a2f1-c2c031c20b3c"
    group = "SEATA_GROUP"
    username = "worker2"
    password = "xxxxxxx"
  }
}</code></pre>
<p style=""></p>
<ul>
 <li>
  <p style="">serverAddr = "127.0.0.1:8848" &nbsp; ：nacos 的地址</p></li>
 <li>
  <p style="">namespace = "" ：nacos的命名空间默认为``</p></li>
 <li>
  <p style="">cluster = "default" &nbsp;：集群设置未默认 <code>default</code></p></li>
</ul>
<p style=""></p>
<h5 style="" id="2.2.3-%E4%BF%AE%E6%94%B9-conf%2Fconfig.txt%E9%85%8D%E7%BD%AE">2.2.3 修改 conf/config.txt配置</h5>
<p style=""></p>
<pre><code>service.vgroupMapping.order-service-seata-service-group=default
service.vgroupMapping.account-service-seata-service-group=default
service.vgroupMapping.storage-service-seata-service-group=default
service.vgroupMapping.business-service-seata-service-group=default
store.mode=db
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.datasource=druid
store.db.dbType=mysql
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=root
store.db.password=123456
store.db.minConn=1
store.db.maxConn=3
store.db.global.table=global_table
store.db.branch.table=branch_table
store.db.query-limit=100
store.db.lockTable=lock_table</code></pre>
<p style=""></p>
<p style="">配置的详细说明参考官网：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">https://seata.io/zh-cn/docs/user/configurations.html</a></p>
<p style=""></p>
<p style="">这里主要修改了如下几项：</p>
<p style=""></p>
<ul>
 <li>
  <p style="">store.mode :存储模式 默认file &nbsp;这里我修改为db 模式 ，并且需要三个表<code>global_table</code>、<code>branch_table</code>和<code>lock_table</code></p></li>
 <li>
  <p style="">store.db.driverClassName： 0.8.0版本默认没有，会报错。添加了 <code>com.mysql.jdbc.Driver</code></p></li>
 <li>
  <p style="">store.db.datasource=dbcp ：数据源 dbcp</p></li>
 <li>
  <p style="">store.db.db-type=mysql : 存储数据库的类型为<code>mysql</code></p></li>
 <li>
  <p style="">store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true : 修改为自己的数据库<code>url</code>、<code>port</code>、<code>数据库名称</code></p></li>
 <li>
  <p style="">store.db.user=lidong :数据库的账号</p></li>
 <li>
  <p style="">store.db.password=cwj887766@@ :数据库的密码</p></li>
 <li>
  <p style="">service.vgroupMapping.order-service-seata-service-group=default</p></li>
 <li>
  <p style="">service.vgroupMapping.account-service-seata-service-group=default</p></li>
 <li>
  <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">service.vgroupMapping.storage</a>-service-seata-service-group=default</p></li>
 <li>
  <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">service.vgroupMapping.business</a>-service-seata-service-group=default</p></li>
</ul>
<p style=""></p>
<h5 style="" id="2.2.4-db%E6%A8%A1%E5%BC%8F%E4%B8%8B%E7%9A%84%E6%89%80%E9%9C%80%E7%9A%84%E4%B8%89%E4%B8%AA%E8%A1%A8">2.2.4 db模式下的所需的三个表</h5>
<p style=""></p>
<p style="">数据库脚本位于<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">https://github.com/seata/seata/tree/develop/script/server/db</a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1617845016293-e96e114c-e431-46b8-8abb-d3a83da8599c.png&amp;size=m" width="1149" style="display: inline-block"></p>
<p style="">这里我用的是mysql数据库，直接下载mysq.sql就可以了。</p>
<p style=""></p>
<p style=""><code>global_table</code>的表结构</p>
<p style=""></p>
<pre><code>CREATE TABLE `global_table` (
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(64) DEFAULT NULL,
  `transaction_service_group` varchar(64) DEFAULT NULL,
  `transaction_name` varchar(64) DEFAULT NULL,
  `timeout` int(11) DEFAULT NULL,
  `begin_time` bigint(20) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;</code></pre>
<p style=""></p>
<p style=""><code>branch_table</code>的表结构</p>
<p style=""></p>
<pre><code>CREATE TABLE `branch_table` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `resource_group_id` varchar(32) DEFAULT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `lock_key` varchar(128) DEFAULT NULL,
  `branch_type` varchar(8) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  `client_id` varchar(64) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;</code></pre>
<p style=""></p>
<p style=""><code>lock_table</code>的表结构</p>
<p style=""></p>
<pre><code>create table `lock_table` (
  `row_key` varchar(128) not null,
  `xid` varchar(96),
  `transaction_id` long ,
  `branch_id` long,
  `resource_id` varchar(256) ,
  `table_name` varchar(32) ,
  `pk` varchar(32) ,
  `gmt_create` datetime ,
  `gmt_modified` datetime,
  primary key(`row_key`)
);</code></pre>
<p style=""></p>
<h5 style="" id="2.2.5-%E5%B0%86-seata-%E9%85%8D%E7%BD%AE%E6%B7%BB%E5%8A%A0%E5%88%B0-nacos-%E4%B8%AD">2.2.5 将 Seata 配置添加到 Nacos 中</h5>
<p style=""></p>
<p style="">nacos导入脚本位于<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">https://github.com/seata/seata/tree/develop/script/config-center/nacos</a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1617845016332-4b1479b8-073d-4394-b1f2-d11277ea536f.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">使用方法：</p>
<p style=""></p>
<pre><code>sh ${SEATAPATH}/script/config-center/nacos/nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 40508bb4-179e-4c98-a2f1-c2c031c20b3c -u worker-w xxxxxx</code></pre>
<p style=""></p>
<p style="">参数描述:</p>
<p style=""></p>
<ul>
 <li>
  <p style="">-h: host, 默认值 <a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">localhost</a>.</p></li>
 <li>
  <p style="">-p: port, 默认值 is 8848.</p></li>
 <li>
  <p style="">-g: 配置分组 默认值 'SEATA_GROUP'.</p></li>
 <li>
  <p style="">-t: 命名空间.</p></li>
 <li>
  <p style="">-u: 用户名, nacos 1.2.0+ 之后添加权限验证 默认为“”</p></li>
 <li>
  <p style="">-w: 密码, nacos 1.2.0+ 之后添加权限验证 默认为“”</p></li>
 <li>
  <p style=""></p></li>
</ul>
<p style=""></p>
<p style="">在 Nacos 管理页面应该可以看到Group 为SEATA_GROUP的配置</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1617845016581-77ac2cd4-7ce8-4269-875b-4866406b6347.png&amp;size=m" width="1701" style="display: inline-block"></p>
<p style="">这样seata-sever就搭建完成。</p>
<p style=""></p>
<h3 style="" id="3.sharding-sphere%E4%B8%ADseata%E6%9F%94%E6%80%A7%E4%BA%8B%E5%8A%A1%E5%AE%9E%E7%8E%B0">3.sharding-sphere中seata柔性事务实现</h3>
<p style=""></p>
<h4 style="" id="3.1-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86">3.1 实现原理</h4>
<p style=""></p>
<p style="">整合<code>Seata AT</code>事务时，需要把<code>TM</code>，<code>RM</code>，<code>TC</code>的模型融入到<code>ShardingSphere</code> 分布式事务的<code>SPI</code>的生态中。在数据库资源上，<code>Seata</code>通过对接<code>DataSource</code>接口，让<code>JDBC</code>操作可以同<code>TC</code>进行<code>RPC</code>通信。同样，<code>ShardingSphere</code>也是面向<code>DataSource</code>接口对用户配置的物理<code>DataSource</code>进行了聚合，因此把物理<code>DataSource</code>二次包装为<code>Seata</code>的<code>DataSource</code>后，就可以把<code>Seata AT</code>事务融入到<code>ShardingSphere</code>的分片中。</p>
<p style=""></p>
<h4 style="" id="3.2%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E5%9B%BE">3.2实现原理图</h4>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1617845016352-118d2217-68ef-4cba-998d-5608d281729d.png&amp;size=m" width="1647" style="display: inline-block"></p>
<p style=""></p>
<h4 style="" id="3.3-%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%AD%A5%E9%AA%A4">3.3 实现的步骤</h4>
<p style=""></p>
<ol>
 <li>
  <p style="">Init（Seata引擎初始化）<br>
   包含Seata柔性事务的应用启动时，用户配置的数据源会按seata.conf的配置，适配为Seata事务所需的DataSourceProxy，并且注册到RM中。</p></li>
 <li>
  <p style="">Begin（开启Seata全局事务）<br>
   TM控制全局事务的边界，TM通过向TC发送Begin指令，获取全局事务ID，所有分支事务通过此全局事务ID，参与到全局事务中；全局事务ID的上下文存放在当前线程变量中。</p></li>
 <li>
  <p style="">执行分片物理SQL<br>
   处于Seata全局事务中的分片SQL通过RM生成undo快照，并且发送participate指令到TC，加入到全局事务中。ShardingSphere的分片物理SQL是按多线程方式执行，因此整合Seata AT事务时，需要在主线程和子线程间进行全局事务ID的上下文传递，这同服务间的上下文传递思路完全相同。</p></li>
 <li>
  <p style="">Commit/rollback（提交Seata事务）<br>
   提交Seata事务时，TM会向TC发送全局事务的commit和rollback指令，TC根据全局事务ID协调所有分支事务进行commit和rollback。</p></li>
</ol>
<p style=""></p>
<h3 style="" id="4.sharding-sphere%E4%B8%ADseata%E7%9A%84%E6%95%B4%E5%90%88">4.sharding-sphere中seata的整合</h3>
<p style=""></p>
<h5 style="" id="4.1%E4%BD%BF%E7%94%A8spring-boot%E5%BC%95%E5%85%A5maven%E4%BE%9D%E8%B5%96">4.1使用Spring-boot引入Maven依赖</h5>
<p style=""></p>
<pre><code>&lt;dependency&gt;
    &lt;groupId&gt;org.apache.shardingsphere&lt;/groupId&gt;
    &lt;artifactId&gt;sharding-jdbc-spring-boot-starter&lt;/artifactId&gt;
    &lt;version&gt;${shardingsphere.version}&lt;/version&gt;
&lt;/dependency&gt;

&lt;!-- 使用BASE事务时，需要引入此模块 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.apache.shardingsphere&lt;/groupId&gt;
    &lt;artifactId&gt;sharding-transaction-base-seata-at&lt;/artifactId&gt;
    &lt;version&gt;${sharding-sphere.version}&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<p style=""></p>
<h5 style="" id="4.2.seata%E7%9A%84at%E6%A8%A1%E5%BC%8F%E4%BD%BF%E7%94%A8%E7%9A%84base%E6%9F%94%E6%80%A7%E4%BA%8B%E5%8A%A1%E7%AE%A1%E7%90%86%E5%99%A8">4.2.Seata的AT模式使用的BASE柔性事务管理器</h5>
<p style=""></p>
<p style="">在每一个分片数据库实例中执创建undo_log表（以MySQL为例）</p>
<p style=""></p>
<pre><code>CREATE TABLE IF NOT EXISTS `undo_log`
(
  `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT COMMENT 'increment id',
  `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
  `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
  `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
  `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created`   DATETIME     NOT NULL COMMENT 'create datetime',
  `log_modified`  DATETIME     NOT NULL COMMENT 'modify datetime',
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';</code></pre>
<p style=""></p>
<h5 style="" id="4.3.%E5%9C%A8classpath%E4%B8%AD%E5%A2%9E%E5%8A%A0seata.conf">4.3.在classpath中增加seata.conf</h5>
<p style=""></p>
<pre><code>client {
    application.id = example    ## 应用唯一id
    transaction.service.group = my_test_tx_group   ## 所属事务组
}</code></pre>
<p style=""></p>
<h5 style="" id="4.4%E4%B8%9A%E5%8A%A1%E6%96%B9%E5%8F%91%E8%B5%B7%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1%EF%BC%8C%E9%85%8D%E7%BD%AE%E6%9F%94%E6%80%A7%E4%BA%8B%E5%8A%A1%E7%B1%BB%E5%9E%8B">4.4业务方发起全局事务，配置柔性事务类型</h5>
<p style=""></p>
<pre><code> @GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-example")
 @Override
public ObjectResponse handleBusiness(BusinessDTO businessDTO) {
        TransactionTypeHolder.set(TransactionType.BASE);
        //执行业务逻辑
}</code></pre>
<p style=""></p>
<p style=""><strong>备注</strong>：也可是使用注解<code>@ShardingTransactionType</code>的形式</p>
<p style=""></p>
<pre><code> @GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-example")
 @ShardingTransactionType(TransactionType.BASE)
@Override
 public ObjectResponse handleBusiness(BusinessDTO businessDTO) {
     //执行业务逻辑  
}</code></pre>
<p style=""></p>
<h3 style="" id="5.%E6%A1%88%E4%BE%8B%E5%AE%9E%E7%8E%B0">5.案例实现</h3>
<p style=""></p>
<p style="">参考官网中用户购买商品的业务逻辑。整个业务逻辑由4个微服务提供支持：</p>
<p style=""></p>
<ul>
 <li>
  <p style="">库存服务：扣除给定商品的存储数量。</p></li>
 <li>
  <p style="">订单服务：根据购买请求创建订单。</p></li>
 <li>
  <p style="">帐户服务：借记用户帐户的余额。</p></li>
 <li>
  <p style="">业务服务：处理业务逻辑。</p></li>
</ul>
<p style=""></p>
<p style="">请求逻辑架构</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1617845016396-5a54e9f6-1fc0-47c8-a601-22c5eaf04403.png&amp;size=m" width="1732" style="display: inline-block"></p>
<p style=""></p>
<h4 style="" id="5.1-%E6%BA%90%E7%A0%81%E5%9C%B0%E5%9D%80">5.1 源码地址</h4>
<p style=""></p>
<ul>
 <li>
  <p style="">samples-common ：公共模块</p></li>
 <li>
  <p style="">samples-account ：用户账号模块</p></li>
 <li>
  <p style="">samples-order ：订单模块</p></li>
 <li>
  <p style="">samples-storage ：库存模块</p></li>
 <li>
  <p style="">samples-business ：业务模块</p></li>
</ul>
<p style=""></p>
<h4 style="" id="5.2-%E6%95%B0%E6%8D%AE%E5%BA%93">5.2 数据库</h4>
<p style=""></p>
<p style="">注意: MySQL必须使用<code>InnoDB engine</code>.</p>
<p style=""></p>
<p style="">如下，并且每个库中都需要一个undo_log表</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1617845016701-55f2415a-bb47-4a96-99f2-65990914873e.png&amp;size=m" width="411" style="display: inline-block"></p>
<p style=""></p>
<h4 style="" id="5.3-%E4%BB%A5%E8%B4%A6%E5%8F%B7%E6%9C%8D%E5%8A%A1%E4%B8%BA%E4%BE%8B">5.3 以账号服务为例</h4>
<p style=""></p>
<p style="">分析需要项目中所需要的配置</p>
<p style=""></p>
<h5 style="" id="5.3.1-%E5%BC%95%E5%85%A5%E7%9A%84%E4%BE%9D%E8%B5%96">5.3.1 引入的依赖</h5>
<p style=""></p>
<pre><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
    &lt;parent&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
        &lt;version&gt;2.2.6.RELEASE&lt;/version&gt;
        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;
    &lt;/parent&gt;
    &lt;artifactId&gt;seata-spring-boot-dubbo-nacos-shardingsphere-examples&lt;/artifactId&gt;
    &lt;packaging&gt;pom&lt;/packaging&gt;
    &lt;name&gt;seata-spring-boot-dubbo-nacos-shardingsphere-examples&lt;/name&gt;
    &lt;groupId&gt;io.seata&lt;/groupId&gt;
    &lt;version&gt;1.2.0&lt;/version&gt;
    &lt;description&gt;Demo project for Spring Boot Dubbo&lt;/description&gt;

    &lt;modules&gt;
        &lt;module&gt;samples-common-service&lt;/module&gt;
        &lt;module&gt;samples-account-service&lt;/module&gt;
        &lt;module&gt;samples-order-service&lt;/module&gt;
        &lt;module&gt;samples-storage-service&lt;/module&gt;
        &lt;module&gt;samples-business-service&lt;/module&gt;
    &lt;/modules&gt;

    &lt;properties&gt;
        &lt;springboot.verison&gt;2.2.6.RELEASE&lt;/springboot.verison&gt;
        &lt;java.version&gt;1.8&lt;/java.version&gt;
        &lt;mybatis-plus.version&gt;2.3&lt;/mybatis-plus.version&gt;
        &lt;nacos.version&gt;0.2.3&lt;/nacos.version&gt;
        &lt;lombok.version&gt;1.16.22&lt;/lombok.version&gt;
        &lt;dubbo.version&gt;2.7.5&lt;/dubbo.version&gt;
        &lt;nacos-client.verison&gt;1.2.1&lt;/nacos-client.verison&gt;
        &lt;seata.version&gt;1.2.0&lt;/seata.version&gt;
        &lt;netty.version&gt;4.1.32.Final&lt;/netty.version&gt;
        &lt;sharding-sphere.version&gt;4.1.0&lt;/sharding-sphere.version&gt;
    &lt;/properties&gt;

    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
            &lt;version&gt;${springboot.verison}&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;${springboot.verison}&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
            &lt;version&gt;${springboot.verison}&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;com.baomidou&lt;/groupId&gt;
            &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;3.3.1&lt;/version&gt;
        &lt;/dependency&gt;


        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.dubbo&lt;/groupId&gt;
            &lt;artifactId&gt;dubbo&lt;/artifactId&gt;
            &lt;version&gt;${dubbo.version}&lt;/version&gt;
            &lt;exclusions&gt;
                &lt;exclusion&gt;
                    &lt;artifactId&gt;spring&lt;/artifactId&gt;
                    &lt;groupId&gt;org.springframework&lt;/groupId&gt;
                &lt;/exclusion&gt;
            &lt;/exclusions&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.dubbo&lt;/groupId&gt;
            &lt;artifactId&gt;dubbo-spring-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;${dubbo.version}&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-config-spring --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.dubbo&lt;/groupId&gt;
            &lt;artifactId&gt;dubbo-configcenter-nacos&lt;/artifactId&gt;
            &lt;version&gt;${dubbo.version}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.dubbo&lt;/groupId&gt;
            &lt;artifactId&gt;dubbo-registry-nacos&lt;/artifactId&gt;
            &lt;version&gt;${dubbo.version}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.dubbo&lt;/groupId&gt;
            &lt;artifactId&gt;dubbo-metadata-report-nacos&lt;/artifactId&gt;
            &lt;version&gt;${dubbo.version}&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;!-- https://mvnrepository.com/artifact/io.seata/seata-all --&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;io.seata&lt;/groupId&gt;
            &lt;artifactId&gt;seata-spring-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;${seata.version}&lt;/version&gt;
        &lt;/dependency&gt;


        &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.nacos&lt;/groupId&gt;
            &lt;artifactId&gt;nacos-client&lt;/artifactId&gt;
            &lt;version&gt;${nacos-client.verison}&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
            &lt;version&gt;${springboot.verison}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
            &lt;version&gt;${lombok.version}&lt;/version&gt;
        &lt;/dependency&gt;


        &lt;dependency&gt;
            &lt;groupId&gt;io.netty&lt;/groupId&gt;
            &lt;artifactId&gt;netty-all&lt;/artifactId&gt;
            &lt;version&gt;${netty.version}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.httpcomponents&lt;/groupId&gt;
            &lt;artifactId&gt;httpclient&lt;/artifactId&gt;
            &lt;version&gt;4.5&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;mysql&lt;/groupId&gt;
            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
            &lt;version&gt;5.1.47&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.shardingsphere&lt;/groupId&gt;
            &lt;artifactId&gt;sharding-jdbc-core&lt;/artifactId&gt;
            &lt;version&gt;${sharding-sphere.version}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.shardingsphere&lt;/groupId&gt;
            &lt;artifactId&gt;sharding-jdbc-spring-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;${sharding-sphere.version}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.shardingsphere&lt;/groupId&gt;
            &lt;artifactId&gt;sharding-transaction-base-seata-at&lt;/artifactId&gt;
            &lt;version&gt;${sharding-sphere.version}&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;com.zaxxer&lt;/groupId&gt;
            &lt;artifactId&gt;HikariCP&lt;/artifactId&gt;
            &lt;version&gt;3.3.1&lt;/version&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;


    &lt;build&gt;
        &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
                &lt;artifactId&gt;maven-deploy-plugin&lt;/artifactId&gt;
                &lt;configuration&gt;
                    &lt;skip&gt;true&lt;/skip&gt;
                &lt;/configuration&gt;
            &lt;/plugin&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
                &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
                &lt;configuration&gt;
                    &lt;source&gt;${java.version}&lt;/source&gt;
                    &lt;target&gt;${java.version}&lt;/target&gt;
                &lt;/configuration&gt;
            &lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;
&lt;/project&gt;</code></pre>
<p style=""></p>
<p style="">注意：</p>
<p style=""></p>
<ul>
 <li>
  <p style=""><code>seata-spring-boot-starter</code>: 这个是spring-boot seata 所需的主要依赖，1.0.0版本开始加入支持。</p></li>
 <li>
  <p style=""><code>dubbo-spring-boot-starter</code>: &nbsp; springboot dubbo的依赖</p></li>
 <li>
  <p style=""><code>sharding-transaction-base-seata-at</code> ：sharding和seata整合的依赖</p></li>
</ul>
<p style=""></p>
<p style="">其他的就不一一介绍，其他的一目了然，就知道是干什么的。</p>
<p style=""></p>
<h5 style="" id="5.3.2-application.yml%E9%85%8D%E7%BD%AE">5.3.2 &nbsp;application.yml配置</h5>
<p style=""></p>
<pre><code>server:
  port: 8102
spring:
  shardingsphere:
    datasource:
      names: ds0
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3306/ds0?useUnicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false
        username: root
        password: 123456
    sharding:
      tables:
        t_account:
          actual-data-nodes: ds0.t_account$-&gt;{0..1}
          table-strategy:
            inline:
              sharding-column: id
              algorithm-expression: t_account$-&gt;{id % 2}
    props:
      sql.show: true

#====================================Dubbo config===============================================
dubbo:
  application:
    id: dubbo-account-example
    name: dubbo-account-example
    qosEnable: false
  protocol:
    id: dubbo
    name: dubbo
    port: 20883
  registry:
    id: dubbo-account-example-registry
    address: nacos://127.0.0.1:8848?namespace=40508bb4-179e-4c98-a2f1-c2c031c20b3c
  config-center:
    address: nacos://127.0.0.1:8848?namespace=40508bb4-179e-4c98-a2f1-c2c031c20b3c
  metadata-report:
    address: nacos://127.0.0.1:8848?namespace=40508bb4-179e-4c98-a2f1-c2c031c20b3c
#====================================mybatis-plus config===============================================
mybatis-plus:
  mapperLocations: classpath*:/mapper/*.xml
  typeAliasesPackage: io.seata.samples.integration.*.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto

#====================================Seata Config===============================================

seata:
  enabled: true
  application-id: account-seata-example
  tx-service-group: account-service-seata-service-group # 事务群组（可以每个应用独立取名，也可以使用相同的名字）
  registry:
    file:
      name: file.conf
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: 40508bb4-179e-4c98-a2f1-c2c031c20b3c
      cluster: default
  config:
    file:
      name: file.conf
    type: nacos
    nacos:
      namespace: 40508bb4-179e-4c98-a2f1-c2c031c20b3c
      server-addr: localhost:8848
      group: SEATA_GROUP
  enable-auto-data-source-proxy: true
  use-jdk-proxy: true</code></pre>
<p style=""></p>
<h5 style="" id="5.3.3-%E5%9C%A8classpath%E4%B8%AD%E5%A2%9E%E5%8A%A0seata.conf">5.3.3 &nbsp;在classpath中增加seata.conf</h5>
<p style=""></p>
<pre><code>client {
    application.id = account-seata-example ## 应用唯一id
    transaction.service.group = account-service-seata-service-group ## 所属事务组
}</code></pre>
<p style=""></p>
<h5 style="" id="5.3.4-%E5%90%AF%E5%8A%A8%E6%89%80%E6%9C%89%E7%9A%84sample%E6%A8%A1%E5%9D%97">5.3.4 启动所有的sample模块</h5>
<p style=""></p>
<p style="">启动 <code>samples-account-service</code>、<code>samples-order-service</code>、<code>samples-storage-service</code>、<code>samples-business-service</code></p>
<p style=""></p>
<p style="">并且在nocos的控制台查看注册情况: <a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">http://192.168.10.200:8848/nacos/#/serviceManagement</a></p>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1617845016713-f6f4fb50-85ee-415c-b281-941941cb8611.png&amp;size=m" width="1915" style="display: inline-block"></p>
<p style="">我们可以看到上面的服务都已经注册成功。</p>
<p style=""></p>
<h3 style="" id="6.%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C">6.测试结果</h3>
<p style=""></p>
<h3 style="" id="6.-1-%E5%8F%91%E9%80%81%E4%B8%80%E4%B8%AA%E4%B8%8B%E5%8D%95%E8%AF%B7%E6%B1%82(%E6%AD%A3%E5%B8%B8%E6%83%85%E5%86%B5)">6. 1 发送一个下单请求(正常情况)</h3>
<p style=""></p>
<p style="">使用postman 发送 ：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">http://localhost:8104/business/dubbo/buy</a></p>
<p style=""></p>
<p style="">请求参数：</p>
<p style=""></p>
<pre><code>{
    "userId": 1,
    "commodityCode":"C201901140001",
    "name":"fan",
    "count":50,
    "amount":"100"
}</code></pre>
<p style=""></p>
<p style="">返回参数</p>
<p style=""></p>
<pre><code>{
    "status": 200,
    "message": "成功",
    "data": null
}</code></pre>
<p style=""></p>
<p style="">这时候控制台：</p>
<p style=""></p>
<h5 style="" id="6.1.1-businessservice-%E6%9C%8D%E5%8A%A1%E6%97%A5%E5%BF%97">6.1.1 BusinessService 服务日志</h5>
<p style=""></p>
<pre><code>2020-05-22 09:15:54.763  INFO 13384 --- [nio-8104-exec-4] i.s.s.i.c.controller.BusinessController  : 请求参数：BusinessDTO(userId=1, commodityCode=C201901140001, name=fan, count=50, amount=100)
2020-05-22 09:15:54.794  INFO 13384 --- [nio-8104-exec-4] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [192.168.10.107:8091:2012243535]
2020-05-22 09:15:54.794  INFO 13384 --- [nio-8104-exec-4] i.s.s.i.c.service.BusinessServiceImpl    : 开始全局事务，XID = 192.168.10.107:8091:2012243535
2020-05-22 09:15:55.527  INFO 13384 --- [nio-8104-exec-4] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.10.107:8091:2012243535] commit status: Committed</code></pre>
<p style=""></p>
<h5 style="" id="6.1.2-accountservice-%E6%9C%8D%E5%8A%A1%E6%97%A5%E5%BF%97">6.1.2 AccountService 服务日志</h5>
<p style=""></p>
<pre><code>2020-05-22 09:15:54.959  INFO 8792 --- [:20883-thread-3] i.s.s.i.a.dubbo.AccountDubboServiceImpl  : 全局事务id ：192.168.10.107:8091:2012243535
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@29020a6b]
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@23fa611b] will be managed by Spring
==&gt;  Preparing: update t_account set amount = amount-100.0 where id = 1 
==&gt; Parameters: 
2020-05-22 09:15:54.960  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : Logic SQL: SELECT id, amount FROM t_account WHERE id = 1 FOR UPDATE
2020-05-22 09:15:54.960  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@60fae881, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@66f8eb00), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@66f8eb00, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=16, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=amount, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@5f10e4e6, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@57d40a06, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@27d65b5e, containsSubquery=false)
2020-05-22 09:15:54.960  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT id, amount FROM t_account1 WHERE id = 1 FOR UPDATE
2020-05-22 09:15:54.962  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : Logic SQL: update t_account set amount = amount-100.0 where id = 1
2020-05-22 09:15:54.962  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : SQLStatement: UpdateStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.UpdateStatement@5ba704b6, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@294b424c), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@294b424c)
2020-05-22 09:15:54.962  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: update t_account1 set amount = amount-100.0 where id = 1
2020-05-22 09:15:54.964  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : Logic SQL: SELECT id, amount FROM t_account WHERE id in (?)
2020-05-22 09:15:54.964  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@697bcc02, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@299ee29), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@299ee29, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=16, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=amount, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@1b05b402, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@68afcc0c, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@4352f7f9, containsSubquery=false)
2020-05-22 09:15:54.964  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT id, amount FROM t_account1 WHERE id in (?) ::: [1]
&lt;==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@29020a6b]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@29020a6b]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@29020a6b]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@29020a6b]
2020-05-22 09:15:55.078  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : Logic SQL: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now())
2020-05-22 09:15:55.079  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@354923b3, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@2ebdd5c8), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@2ebdd5c8, columnNames=[branch_id, xid, context, rollback_info, log_status, log_created, log_modified], insertValueContexts=[InsertValueContext(parametersCount=5, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=109, stopIndex=109, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=112, stopIndex=112, parameterMarkerIndex=1), ParameterMarkerExpressionSegment(startIndex=115, stopIndex=115, parameterMarkerIndex=2), ParameterMarkerExpressionSegment(startIndex=118, stopIndex=118, parameterMarkerIndex=3), ParameterMarkerExpressionSegment(startIndex=121, stopIndex=121, parameterMarkerIndex=4), org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@5fd3f282, org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@2e3bb335], parameters=[2012243539, 192.168.10.107:8091:2012243535, serializer=jackson, javax.sql.rowset.serial.SerialBlob@d4e81a62, 0])], generatedKeyContext=Optional.empty)
2020-05-22 09:15:55.079  INFO 8792 --- [:20883-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now()) ::: [2012243539, 192.168.10.107:8091:2012243535, serializer=jackson, javax.sql.rowset.serial.SerialBlob@d4e81a62, 0]
2020-05-22 09:15:55.562  INFO 8792 --- [atch_RMROLE_1_8] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=192.168.10.107:8091:2012243535,branchId=2012243539,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/ds0,applicationData=null
2020-05-22 09:15:55.563  INFO 8792 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.10.107:8091:2012243535 2012243539 jdbc:mysql://127.0.0.1:3306/ds0 null
2020-05-22 09:15:55.564  INFO 8792 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
2020-05-22 09:15:56.217  INFO 8792 --- [  AsyncWorker_1] ShardingSphere-SQL                       : Logic SQL: DELETE FROM undo_log WHERE  branch_id IN  (?)  AND xid IN  (?) 
2020-05-22 09:15:56.217  INFO 8792 --- [  AsyncWorker_1] ShardingSphere-SQL                       : SQLStatement: DeleteStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.DeleteStatement@5b1d0b10, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@5f2284ab), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@5f2284ab)
2020-05-22 09:15:56.217  INFO 8792 --- [  AsyncWorker_1] ShardingSphere-SQL                       : Actual SQL: ds0 ::: DELETE FROM undo_log WHERE  branch_id IN  (?)  AND xid IN  (?)  ::: [2012243539, 192.168.10.107:8091:2012243535]</code></pre>
<p style=""></p>
<h5 style="" id="6.1.3-storageservice-%E6%9C%8D%E5%8A%A1%E6%97%A5%E5%BF%97">6.1.3 StorageService 服务日志</h5>
<p style=""></p>
<pre><code>2020-05-22 09:15:54.796  INFO 9580 --- [:20888-thread-3] i.s.s.i.s.dubbo.StorageDubboServiceImpl  : 全局事务id ：192.168.10.107:8091:2012243535
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48378073]
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@42cfeb03] will be managed by Spring
==&gt;  Preparing: SELECT id,commodity_code,name,count FROM t_storage WHERE (commodity_code = ?) 
==&gt; Parameters: C201901140001(String)
2020-05-22 09:15:54.798  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Logic SQL: SELECT  id,commodity_code,name,count  FROM t_storage 
 
 WHERE (commodity_code = ?)
2020-05-22 09:15:54.798  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@5ec9be76, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@7cd6d3e), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@7cd6d3e, projectionsContext=ProjectionsContext(startIndex=8, stopIndex=35, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=commodity_code, alias=Optional.empty), ColumnProjection(owner=null, name=name, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@738d41ac, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@3680c897, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@b2abab7, containsSubquery=false)
2020-05-22 09:15:54.798  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT  id,commodity_code,name,count  FROM t_storage0 
 
 WHERE (commodity_code = ?) ::: [C201901140001]
2020-05-22 09:15:54.798  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT  id,commodity_code,name,count  FROM t_storage1 
 
 WHERE (commodity_code = ?) ::: [C201901140001]
&lt;==    Columns: id, commodity_code, name, count
&lt;==        Row: 1, C201901140001, 水杯, 650
&lt;==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48378073]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48378073] from current transaction
==&gt;  Preparing: update t_storage set count = count-50 where id = 1 
==&gt; Parameters: 
2020-05-22 09:15:54.802  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Logic SQL: SELECT id, count FROM t_storage WHERE id = 1 FOR UPDATE
2020-05-22 09:15:54.802  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@1460703d, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@63ac0932), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@63ac0932, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=15, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@44882968, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@7694cb15, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@15dbf90b, containsSubquery=false)
2020-05-22 09:15:54.802  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT id, count FROM t_storage1 WHERE id = 1 FOR UPDATE
2020-05-22 09:15:54.804  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Logic SQL: update t_storage set count = count-50 where id = 1
2020-05-22 09:15:54.804  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : SQLStatement: UpdateStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.UpdateStatement@7029adff, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@7341e361), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@7341e361)
2020-05-22 09:15:54.804  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: update t_storage1 set count = count-50 where id = 1
2020-05-22 09:15:54.817  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Logic SQL: SELECT id, count FROM t_storage WHERE id in (?)
2020-05-22 09:15:54.817  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@44300acf, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@4680ebc4), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@4680ebc4, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=15, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@3e86252e, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@42a08374, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@e1a5a04, containsSubquery=false)
2020-05-22 09:15:54.817  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT id, count FROM t_storage1 WHERE id in (?) ::: [1]
&lt;==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48378073]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48378073]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48378073]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48378073]
2020-05-22 09:15:54.885  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Logic SQL: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now())
2020-05-22 09:15:54.885  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@23a45738, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@6d82d00), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@6d82d00, columnNames=[branch_id, xid, context, rollback_info, log_status, log_created, log_modified], insertValueContexts=[InsertValueContext(parametersCount=5, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=109, stopIndex=109, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=112, stopIndex=112, parameterMarkerIndex=1), ParameterMarkerExpressionSegment(startIndex=115, stopIndex=115, parameterMarkerIndex=2), ParameterMarkerExpressionSegment(startIndex=118, stopIndex=118, parameterMarkerIndex=3), ParameterMarkerExpressionSegment(startIndex=121, stopIndex=121, parameterMarkerIndex=4), org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@10b328c0, org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@48eb75ea], parameters=[2012243537, 192.168.10.107:8091:2012243535, serializer=jackson, javax.sql.rowset.serial.SerialBlob@d6553184, 0])], generatedKeyContext=Optional.empty)
2020-05-22 09:15:54.885  INFO 9580 --- [:20888-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now()) ::: [2012243537, 192.168.10.107:8091:2012243535, serializer=jackson, javax.sql.rowset.serial.SerialBlob@d6553184, 0]
2020-05-22 09:15:55.528  INFO 9580 --- [atch_RMROLE_1_8] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=192.168.10.107:8091:2012243535,branchId=2012243537,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/ds2,applicationData=null
2020-05-22 09:15:55.529  INFO 9580 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.10.107:8091:2012243535 2012243537 jdbc:mysql://127.0.0.1:3306/ds2 null
2020-05-22 09:15:55.529  INFO 9580 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
2020-05-22 09:15:55.532  INFO 9580 --- [  AsyncWorker_1] ShardingSphere-SQL                       : Logic SQL: DELETE FROM undo_log WHERE  branch_id IN  (?)  AND xid IN  (?) 
2020-05-22 09:15:55.532  INFO 9580 --- [  AsyncWorker_1] ShardingSphere-SQL                       : SQLStatement: DeleteStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.DeleteStatement@6eb59ea6, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@6a8a17a8), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@6a8a17a8)
2020-05-22 09:15:55.532  INFO 9580 --- [  AsyncWorker_1] ShardingSphere-SQL                       : Actual SQL: ds0 ::: DELETE FROM undo_log WHERE  branch_id IN  (?)  AND xid IN  (?)  ::: [2012243537, 192.168.10.107:8091:2012243535]</code></pre>
<p style=""></p>
<h5 style="" id="6.1.4-orderservice-%E6%9C%8D%E5%8A%A1%E6%97%A5%E5%BF%97">6.1.4 OrderService 服务日志</h5>
<p style=""></p>
<pre><code>2020-05-22 09:15:54.956  INFO 6268 --- [:20880-thread-3] i.s.s.i.o.dubbo.OrderDubboServiceImpl    : 全局事务id ：192.168.10.107:8091:2012243535
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@23bc1e40] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@2d7be447] will not be managed by Spring
==&gt;  Preparing: insert into t_order values(?,?,1,?,50,100.0) 
==&gt; Parameters: 1263639694564524034(String), 4e7d738e311a40cd8176795aafb8a247(String), C201901140001(String)
2020-05-22 09:15:55.132  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : Logic SQL: insert into t_order values(?,?,1,?,50,100.0)
2020-05-22 09:15:55.132  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@385e4984, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@29447530), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@29447530, columnNames=[id, order_no, user_id, commodity_code, count, amount], insertValueContexts=[InsertValueContext(parametersCount=3, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=27, stopIndex=27, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=29, stopIndex=29, parameterMarkerIndex=1), LiteralExpressionSegment(startIndex=31, stopIndex=31, literals=1), ParameterMarkerExpressionSegment(startIndex=33, stopIndex=33, parameterMarkerIndex=2), LiteralExpressionSegment(startIndex=35, stopIndex=36, literals=50), LiteralExpressionSegment(startIndex=38, stopIndex=42, literals=100.0)], parameters=[1263639694564524034, 4e7d738e311a40cd8176795aafb8a247, C201901140001])], generatedKeyContext=Optional.empty)
2020-05-22 09:15:55.132  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: insert into t_order1 values(?, ?, 1, ?, 50, 100.0) ::: [1263639694564524034, 4e7d738e311a40cd8176795aafb8a247, C201901140001]
2020-05-22 09:15:55.135  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : Logic SQL: SELECT * FROM t_order WHERE id in (?)
2020-05-22 09:15:55.135  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@64adbefc, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@5de023c8), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@5de023c8, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=7, distinctRow=false, projections=[ShorthandProjection(owner=Optional.empty, actualColumns=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=order_no, alias=Optional.empty), ColumnProjection(owner=null, name=user_id, alias=Optional.empty), ColumnProjection(owner=null, name=commodity_code, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty), ColumnProjection(owner=null, name=amount, alias=Optional.empty)])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@578730b1, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@c691133, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@7ecd35e7, containsSubquery=false)
2020-05-22 09:15:55.135  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM t_order0 WHERE id in (?) ::: [1263639694564524034]
2020-05-22 09:15:55.135  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM t_order1 WHERE id in (?) ::: [1263639694564524034]
2020-05-22 09:15:55.202  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : Logic SQL: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now())
2020-05-22 09:15:55.202  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@79890300, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@1f521715), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@1f521715, columnNames=[branch_id, xid, context, rollback_info, log_status, log_created, log_modified], insertValueContexts=[InsertValueContext(parametersCount=5, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=109, stopIndex=109, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=112, stopIndex=112, parameterMarkerIndex=1), ParameterMarkerExpressionSegment(startIndex=115, stopIndex=115, parameterMarkerIndex=2), ParameterMarkerExpressionSegment(startIndex=118, stopIndex=118, parameterMarkerIndex=3), ParameterMarkerExpressionSegment(startIndex=121, stopIndex=121, parameterMarkerIndex=4), org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@6d458949, org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@3204530f], parameters=[2012243541, 192.168.10.107:8091:2012243535, serializer=jackson, javax.sql.rowset.serial.SerialBlob@c45d57c1, 0])], generatedKeyContext=Optional.empty)
2020-05-22 09:15:55.203  INFO 6268 --- [:20880-thread-3] ShardingSphere-SQL                       : Actual SQL: ds0 ::: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now()) ::: [2012243541, 192.168.10.107:8091:2012243535, serializer=jackson, javax.sql.rowset.serial.SerialBlob@c45d57c1, 0]
&lt;==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@23bc1e40]
2020-05-22 09:15:55.596  INFO 6268 --- [atch_RMROLE_1_8] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=192.168.10.107:8091:2012243535,branchId=2012243541,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/ds1,applicationData=null
2020-05-22 09:15:55.597  INFO 6268 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.10.107:8091:2012243535 2012243541 jdbc:mysql://127.0.0.1:3306/ds1 null
2020-05-22 09:15:55.597  INFO 6268 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
2020-05-22 09:15:56.526  INFO 6268 --- [  AsyncWorker_1] ShardingSphere-SQL                       : Logic SQL: DELETE FROM undo_log WHERE  branch_id IN  (?)  AND xid IN  (?) 
2020-05-22 09:15:56.526  INFO 6268 --- [  AsyncWorker_1] ShardingSphere-SQL                       : SQLStatement: DeleteStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.DeleteStatement@3d80f8b5, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@1c45d32), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@1c45d32)
2020-05-22 09:15:56.526  INFO 6268 --- [  AsyncWorker_1] ShardingSphere-SQL                       : Actual SQL: ds0 ::: DELETE FROM undo_log WHERE  branch_id IN  (?)  AND xid IN  (?)  ::: [2012243541, 192.168.10.107:8091:2012243535]</code></pre>
<p style=""></p>
<h3 style="" id="6.-2-%E5%8F%91%E9%80%81%E4%B8%80%E4%B8%AA%E4%B8%8B%E5%8D%95%E8%AF%B7%E6%B1%82(%E5%BC%82%E5%B8%B8%E5%9B%9E%E6%BB%9A%E6%83%85%E5%86%B5)">6. 2 发送一个下单请求(异常回滚情况)</h3>
<p style=""></p>
<p style="">我们<code>samples-business</code>将<code>BusinessServiceImpl</code>的<code>handleBusiness2</code> 下面的代码去掉注释</p>
<p style=""></p>
<pre><code>if (!flag) {
  throw new RuntimeException("测试抛异常后，分布式事务回滚！");
}</code></pre>
<p style=""></p>
<p style="">使用postman 发送 ：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">http://localhost:8104/business/dubbo/buy2</a></p>
<p style=""></p>
<pre><code>{
    "userId":1,
    "commodityCode":"C201901140001",
    "name":"fan",
    "count":50,
    "amount":"100"
}</code></pre>
<p style=""></p>
<p style="">响应结果：</p>
<p style=""></p>
<pre><code>{
    "timestamp": "2020-05-22T01:27:53.517+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "测试抛异常后，分布式事务回滚！",
    "path": "/business/dubbo/buy2"
}</code></pre>
<p style=""></p>
<h5 style="" id="6.2.1-businessservice-%E6%9C%8D%E5%8A%A1%E6%97%A5%E5%BF%97">6.2.1 BusinessService 服务日志</h5>
<p style=""></p>
<pre><code>2020-05-22 09:27:52.386  INFO 13384 --- [nio-8104-exec-7] i.s.s.i.c.controller.BusinessController  : 请求参数：BusinessDTO(userId=1, commodityCode=C201901140001, name=fan, count=50, amount=100)
2020-05-22 09:27:52.422  INFO 13384 --- [nio-8104-exec-7] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [192.168.10.107:8091:2012243545]
2020-05-22 09:27:52.422  INFO 13384 --- [nio-8104-exec-7] i.s.s.i.c.service.BusinessServiceImpl    : 开始全局事务，XID = 192.168.10.107:8091:2012243545
2020-05-22 09:27:53.515  INFO 13384 --- [nio-8104-exec-7] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.10.107:8091:2012243545] rollback status: Rollbacked
2020-05-22 09:27:53.516 ERROR 13384 --- [nio-8104-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 测试抛异常后，分布式事务回滚！] with root cause

java.lang.RuntimeException: 测试抛异常后，分布式事务回滚！
	at io.seata.samples.integration.call.service.BusinessServiceImpl.handleBusiness2(BusinessServiceImpl.java:99) ~[classes/:na]
	at io.seata.samples.integration.call.service.BusinessServiceImpl$$FastClassBySpringCGLIB$$2ab3d645.invoke(&lt;generated&gt;) ~[classes/:na]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at io.seata.spring.annotation.GlobalTransactionalInterceptor$1.execute(GlobalTransactionalInterceptor.java:109) ~[seata-all-1.2.0.jar:1.2.0]
	at io.seata.tm.api.TransactionalTemplate.execute(TransactionalTemplate.java:104) ~[seata-all-1.2.0.jar:1.2.0]
	at io.seata.spring.annotation.GlobalTransactionalInterceptor.handleGlobalTransaction(GlobalTransactionalInterceptor.java:106) ~[seata-all-1.2.0.jar:1.2.0]
	at io.seata.spring.annotation.GlobalTransactionalInterceptor.invoke(GlobalTransactionalInterceptor.java:83) ~[seata-all-1.2.0.jar:1.2.0]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at io.seata.samples.integration.call.service.BusinessServiceImpl$$EnhancerBySpringCGLIB$$11be97b5.handleBusiness2(&lt;generated&gt;) ~[classes/:na]
	at io.seata.samples.integration.call.controller.BusinessController.handleBusiness2(BusinessController.java:48) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_144]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_144]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_144]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_144]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:660) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1594) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_144]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_144]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.33.jar:9.0.33]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_144]</code></pre>
<p style=""></p>
<h5 style="" id="6.2.2-accountservice-%E6%9C%8D%E5%8A%A1%E6%97%A5%E5%BF%97">6.2.2 AccountService 服务日志</h5>
<p style=""></p>
<pre><code>2020-05-22 09:27:52.635  INFO 8792 --- [:20883-thread-4] i.s.s.i.a.dubbo.AccountDubboServiceImpl  : 全局事务id ：192.168.10.107:8091:2012243545
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58622fda]
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@529a44aa] will be managed by Spring
==&gt;  Preparing: update t_account set amount = amount-100.0 where id = 1 
==&gt; Parameters: 
2020-05-22 09:27:52.637  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : Logic SQL: SELECT id, amount FROM t_account WHERE id = 1 FOR UPDATE
2020-05-22 09:27:52.637  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@60fae881, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@1798d09d), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@1798d09d, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=16, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=amount, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@5ecd214b, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@2645a0de, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@26fa0fa8, containsSubquery=false)
2020-05-22 09:27:52.637  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT id, amount FROM t_account1 WHERE id = 1 FOR UPDATE
2020-05-22 09:27:52.640  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : Logic SQL: update t_account set amount = amount-100.0 where id = 1
2020-05-22 09:27:52.640  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : SQLStatement: UpdateStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.UpdateStatement@5ba704b6, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@73f934c4), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@73f934c4)
2020-05-22 09:27:52.640  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: update t_account1 set amount = amount-100.0 where id = 1
2020-05-22 09:27:52.643  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : Logic SQL: SELECT id, amount FROM t_account WHERE id in (?)
2020-05-22 09:27:52.643  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@697bcc02, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@38a68471), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@38a68471, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=16, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=amount, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@77bfb086, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@4ddaf5a1, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@13ea38b2, containsSubquery=false)
2020-05-22 09:27:52.643  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT id, amount FROM t_account1 WHERE id in (?) ::: [1]
&lt;==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58622fda]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58622fda]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58622fda]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58622fda]
2020-05-22 09:27:52.757  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : Logic SQL: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now())
2020-05-22 09:27:52.757  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@354923b3, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@4d520553), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@4d520553, columnNames=[branch_id, xid, context, rollback_info, log_status, log_created, log_modified], insertValueContexts=[InsertValueContext(parametersCount=5, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=109, stopIndex=109, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=112, stopIndex=112, parameterMarkerIndex=1), ParameterMarkerExpressionSegment(startIndex=115, stopIndex=115, parameterMarkerIndex=2), ParameterMarkerExpressionSegment(startIndex=118, stopIndex=118, parameterMarkerIndex=3), ParameterMarkerExpressionSegment(startIndex=121, stopIndex=121, parameterMarkerIndex=4), org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@5fd3f282, org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@2e3bb335], parameters=[2012243550, 192.168.10.107:8091:2012243545, serializer=jackson, javax.sql.rowset.serial.SerialBlob@4e4593ee, 0])], generatedKeyContext=Optional.empty)
2020-05-22 09:27:52.757  INFO 8792 --- [:20883-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now()) ::: [2012243550, 192.168.10.107:8091:2012243545, serializer=jackson, javax.sql.rowset.serial.SerialBlob@4e4593ee, 0]
2020-05-22 09:27:53.190  INFO 8792 --- [atch_RMROLE_1_8] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=192.168.10.107:8091:2012243545,branchId=2012243550,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/ds0,applicationData=null
2020-05-22 09:27:53.190  INFO 8792 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.10.107:8091:2012243545 2012243550 jdbc:mysql://127.0.0.1:3306/ds0
2020-05-22 09:27:53.191  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: SELECT * FROM undo_log WHERE branch_id = ? AND xid = ? FOR UPDATE
2020-05-22 09:27:53.191  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@6481a025, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@59a7b23f), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@59a7b23f, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=7, distinctRow=false, projections=[ShorthandProjection(owner=Optional.empty, actualColumns=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=branch_id, alias=Optional.empty), ColumnProjection(owner=null, name=xid, alias=Optional.empty), ColumnProjection(owner=null, name=context, alias=Optional.empty), ColumnProjection(owner=null, name=rollback_info, alias=Optional.empty), ColumnProjection(owner=null, name=log_status, alias=Optional.empty), ColumnProjection(owner=null, name=log_created, alias=Optional.empty), ColumnProjection(owner=null, name=log_modified, alias=Optional.empty), ColumnProjection(owner=null, name=ext, alias=Optional.empty)])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@5760477b, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@535fd94f, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@165d4d40, containsSubquery=false)
2020-05-22 09:27:53.191  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM undo_log WHERE branch_id = ? AND xid = ? FOR UPDATE ::: [2012243550, 192.168.10.107:8091:2012243545]
2020-05-22 09:27:53.193  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: SELECT * FROM t_account WHERE id in (?)
2020-05-22 09:27:53.193  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@761b9b19, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@2781f95b), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@2781f95b, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=7, distinctRow=false, projections=[ShorthandProjection(owner=Optional.empty, actualColumns=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=amount, alias=Optional.empty)])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@a39c945, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@7672b20f, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@7de97fec, containsSubquery=false)
2020-05-22 09:27:53.194  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM t_account1 WHERE id in (?) ::: [1]
2020-05-22 09:27:53.195  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: UPDATE t_account SET amount = ? WHERE id = ?
2020-05-22 09:27:53.195  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: UpdateStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.UpdateStatement@4df03ffe, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@113d823e), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@113d823e)
2020-05-22 09:27:53.195  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: UPDATE t_account1 SET amount = ? WHERE id = ? ::: [3600.0, 1]
2020-05-22 09:27:53.212  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: DELETE FROM undo_log WHERE branch_id = ? AND xid = ?
2020-05-22 09:27:53.212  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: DeleteStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.DeleteStatement@2baab4f5, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@723ca8dc), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@723ca8dc)
2020-05-22 09:27:53.212  INFO 8792 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: DELETE FROM undo_log WHERE branch_id = ? AND xid = ? ::: [2012243550, 192.168.10.107:8091:2012243545]
2020-05-22 09:27:53.286  INFO 8792 --- [atch_RMROLE_1_8] i.s.r.d.undo.AbstractUndoLogManager      : xid 192.168.10.107:8091:2012243545 branch 2012243550, undo_log deleted with GlobalFinished
2020-05-22 09:27:53.287  INFO 8792 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked</code></pre>
<p style=""></p>
<h5 style="" id="6.2.3-storageservice-%E6%9C%8D%E5%8A%A1%E6%97%A5%E5%BF%97">6.2.3 StorageService 服务日志</h5>
<p style=""></p>
<pre><code>2020-05-22 09:27:52.425  INFO 9580 --- [:20888-thread-4] i.s.s.i.s.dubbo.StorageDubboServiceImpl  : 全局事务id ：192.168.10.107:8091:2012243545
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3aaa9dd2]
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@3052f6a2] will be managed by Spring
==&gt;  Preparing: SELECT id,commodity_code,name,count FROM t_storage WHERE (commodity_code = ?) 
==&gt; Parameters: C201901140001(String)
2020-05-22 09:27:52.428  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Logic SQL: SELECT  id,commodity_code,name,count  FROM t_storage 
 
 WHERE (commodity_code = ?)
2020-05-22 09:27:52.428  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@5ec9be76, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@75506ecc), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@75506ecc, projectionsContext=ProjectionsContext(startIndex=8, stopIndex=35, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=commodity_code, alias=Optional.empty), ColumnProjection(owner=null, name=name, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@1b7a39b9, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@44efbcfd, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@632fb524, containsSubquery=false)
2020-05-22 09:27:52.428  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT  id,commodity_code,name,count  FROM t_storage0 
 
 WHERE (commodity_code = ?) ::: [C201901140001]
2020-05-22 09:27:52.428  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT  id,commodity_code,name,count  FROM t_storage1 
 
 WHERE (commodity_code = ?) ::: [C201901140001]
&lt;==    Columns: id, commodity_code, name, count
&lt;==        Row: 1, C201901140001, 水杯, 600
&lt;==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3aaa9dd2]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3aaa9dd2] from current transaction
==&gt;  Preparing: update t_storage set count = count-50 where id = 1 
==&gt; Parameters: 
2020-05-22 09:27:52.432  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Logic SQL: SELECT id, count FROM t_storage WHERE id = 1 FOR UPDATE
2020-05-22 09:27:52.432  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@1460703d, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@782b9d5a), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@782b9d5a, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=15, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@1131e855, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@585126e2, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@2209f5bf, containsSubquery=false)
2020-05-22 09:27:52.432  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT id, count FROM t_storage1 WHERE id = 1 FOR UPDATE
2020-05-22 09:27:52.433  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Logic SQL: update t_storage set count = count-50 where id = 1
2020-05-22 09:27:52.433  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : SQLStatement: UpdateStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.UpdateStatement@7029adff, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@18814e21), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@18814e21)
2020-05-22 09:27:52.433  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: update t_storage1 set count = count-50 where id = 1
2020-05-22 09:27:52.445  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Logic SQL: SELECT id, count FROM t_storage WHERE id in (?)
2020-05-22 09:27:52.445  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@44300acf, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@247fbd61), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@247fbd61, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=15, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty)]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@41198f32, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@53fb3176, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@2a5b57d9, containsSubquery=false)
2020-05-22 09:27:52.445  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT id, count FROM t_storage1 WHERE id in (?) ::: [1]
&lt;==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3aaa9dd2]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3aaa9dd2]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3aaa9dd2]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3aaa9dd2]
2020-05-22 09:27:52.541  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Logic SQL: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now())
2020-05-22 09:27:52.541  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@23a45738, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@7401fc21), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@7401fc21, columnNames=[branch_id, xid, context, rollback_info, log_status, log_created, log_modified], insertValueContexts=[InsertValueContext(parametersCount=5, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=109, stopIndex=109, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=112, stopIndex=112, parameterMarkerIndex=1), ParameterMarkerExpressionSegment(startIndex=115, stopIndex=115, parameterMarkerIndex=2), ParameterMarkerExpressionSegment(startIndex=118, stopIndex=118, parameterMarkerIndex=3), ParameterMarkerExpressionSegment(startIndex=121, stopIndex=121, parameterMarkerIndex=4), org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@10b328c0, org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@48eb75ea], parameters=[2012243547, 192.168.10.107:8091:2012243545, serializer=jackson, javax.sql.rowset.serial.SerialBlob@e6006345, 0])], generatedKeyContext=Optional.empty)
2020-05-22 09:27:52.541  INFO 9580 --- [:20888-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now()) ::: [2012243547, 192.168.10.107:8091:2012243545, serializer=jackson, javax.sql.rowset.serial.SerialBlob@e6006345, 0]
2020-05-22 09:27:53.340  INFO 9580 --- [atch_RMROLE_1_8] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=192.168.10.107:8091:2012243545,branchId=2012243547,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/ds2,applicationData=null
2020-05-22 09:27:53.340  INFO 9580 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.10.107:8091:2012243545 2012243547 jdbc:mysql://127.0.0.1:3306/ds2
2020-05-22 09:27:53.340  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: SELECT * FROM undo_log WHERE branch_id = ? AND xid = ? FOR UPDATE
2020-05-22 09:27:53.340  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@361d5f71, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@12dcbe90), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@12dcbe90, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=7, distinctRow=false, projections=[ShorthandProjection(owner=Optional.empty, actualColumns=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=branch_id, alias=Optional.empty), ColumnProjection(owner=null, name=xid, alias=Optional.empty), ColumnProjection(owner=null, name=context, alias=Optional.empty), ColumnProjection(owner=null, name=rollback_info, alias=Optional.empty), ColumnProjection(owner=null, name=log_status, alias=Optional.empty), ColumnProjection(owner=null, name=log_created, alias=Optional.empty), ColumnProjection(owner=null, name=log_modified, alias=Optional.empty), ColumnProjection(owner=null, name=ext, alias=Optional.empty)])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@1c1b52bb, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@1824d5e0, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@5d265880, containsSubquery=false)
2020-05-22 09:27:53.341  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM undo_log WHERE branch_id = ? AND xid = ? FOR UPDATE ::: [2012243547, 192.168.10.107:8091:2012243545]
2020-05-22 09:27:53.343  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: SELECT * FROM t_storage WHERE id in (?)
2020-05-22 09:27:53.343  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@19c73cee, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@6c3144f4), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@6c3144f4, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=7, distinctRow=false, projections=[ShorthandProjection(owner=Optional.empty, actualColumns=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=commodity_code, alias=Optional.empty), ColumnProjection(owner=null, name=name, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty)])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@52840747, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@3d09a7cf, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@5dbcf0f1, containsSubquery=false)
2020-05-22 09:27:53.343  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM t_storage1 WHERE id in (?) ::: [1]
2020-05-22 09:27:53.344  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: UPDATE t_storage SET count = ? WHERE id = ?
2020-05-22 09:27:53.344  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: UpdateStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.UpdateStatement@2c4d9b68, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@49808f57), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@49808f57)
2020-05-22 09:27:53.344  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: UPDATE t_storage1 SET count = ? WHERE id = ? ::: [600, 1]
2020-05-22 09:27:53.346  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: DELETE FROM undo_log WHERE branch_id = ? AND xid = ?
2020-05-22 09:27:53.346  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: DeleteStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.DeleteStatement@9b8689c, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@39477e77), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@39477e77)
2020-05-22 09:27:53.346  INFO 9580 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: DELETE FROM undo_log WHERE branch_id = ? AND xid = ? ::: [2012243547, 192.168.10.107:8091:2012243545]
2020-05-22 09:27:53.399  INFO 9580 --- [atch_RMROLE_1_8] i.s.r.d.undo.AbstractUndoLogManager      : xid 192.168.10.107:8091:2012243545 branch 2012243547, undo_log deleted with GlobalFinished
2020-05-22 09:27:53.399  INFO 9580 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked</code></pre>
<p style=""></p>
<h5 style="" id="6.2.4-orderservice-%E6%9C%8D%E5%8A%A1%E6%97%A5%E5%BF%97">6.2.4 OrderService 服务日志</h5>
<p style=""></p>
<pre><code>2020-05-22 09:27:52.615  INFO 6268 --- [:20880-thread-4] i.s.s.i.o.dubbo.OrderDubboServiceImpl    : 全局事务id ：192.168.10.107:8091:2012243545
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3be964] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@efc0713] will not be managed by Spring
==&gt;  Preparing: insert into t_order values(?,?,1,?,50,100.0) 
==&gt; Parameters: 1263642704673898497(String), 72ac94267b7f4f729b42ff72e641a0c4(String), C201901140001(String)
2020-05-22 09:27:52.799  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : Logic SQL: insert into t_order values(?,?,1,?,50,100.0)
2020-05-22 09:27:52.799  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@385e4984, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@ac38214), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@ac38214, columnNames=[id, order_no, user_id, commodity_code, count, amount], insertValueContexts=[InsertValueContext(parametersCount=3, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=27, stopIndex=27, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=29, stopIndex=29, parameterMarkerIndex=1), LiteralExpressionSegment(startIndex=31, stopIndex=31, literals=1), ParameterMarkerExpressionSegment(startIndex=33, stopIndex=33, parameterMarkerIndex=2), LiteralExpressionSegment(startIndex=35, stopIndex=36, literals=50), LiteralExpressionSegment(startIndex=38, stopIndex=42, literals=100.0)], parameters=[1263642704673898497, 72ac94267b7f4f729b42ff72e641a0c4, C201901140001])], generatedKeyContext=Optional.empty)
2020-05-22 09:27:52.799  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: insert into t_order1 values(?, ?, 1, ?, 50, 100.0) ::: [1263642704673898497, 72ac94267b7f4f729b42ff72e641a0c4, C201901140001]
2020-05-22 09:27:52.802  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : Logic SQL: SELECT * FROM t_order WHERE id in (?)
2020-05-22 09:27:52.802  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@64adbefc, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@68010a02), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@68010a02, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=7, distinctRow=false, projections=[ShorthandProjection(owner=Optional.empty, actualColumns=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=order_no, alias=Optional.empty), ColumnProjection(owner=null, name=user_id, alias=Optional.empty), ColumnProjection(owner=null, name=commodity_code, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty), ColumnProjection(owner=null, name=amount, alias=Optional.empty)])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@2feef267, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@4a8eb7b2, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@13b4c98b, containsSubquery=false)
2020-05-22 09:27:52.802  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM t_order0 WHERE id in (?) ::: [1263642704673898497]
2020-05-22 09:27:52.802  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM t_order1 WHERE id in (?) ::: [1263642704673898497]
2020-05-22 09:27:52.875  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : Logic SQL: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now())
2020-05-22 09:27:52.875  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@79890300, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@2160f297), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@2160f297, columnNames=[branch_id, xid, context, rollback_info, log_status, log_created, log_modified], insertValueContexts=[InsertValueContext(parametersCount=5, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=109, stopIndex=109, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=112, stopIndex=112, parameterMarkerIndex=1), ParameterMarkerExpressionSegment(startIndex=115, stopIndex=115, parameterMarkerIndex=2), ParameterMarkerExpressionSegment(startIndex=118, stopIndex=118, parameterMarkerIndex=3), ParameterMarkerExpressionSegment(startIndex=121, stopIndex=121, parameterMarkerIndex=4), org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@6d458949, org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment@3204530f], parameters=[2012243552, 192.168.10.107:8091:2012243545, serializer=jackson, javax.sql.rowset.serial.SerialBlob@582376a0, 0])], generatedKeyContext=Optional.empty)
2020-05-22 09:27:52.875  INFO 6268 --- [:20880-thread-4] ShardingSphere-SQL                       : Actual SQL: ds0 ::: INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified) VALUES (?, ?, ?, ?, ?, now(), now()) ::: [2012243552, 192.168.10.107:8091:2012243545, serializer=jackson, javax.sql.rowset.serial.SerialBlob@582376a0, 0]
&lt;==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3be964]
2020-05-22 09:27:52.980  INFO 6268 --- [atch_RMROLE_1_8] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=192.168.10.107:8091:2012243545,branchId=2012243552,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/ds1,applicationData=null
2020-05-22 09:27:52.980  INFO 6268 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.10.107:8091:2012243545 2012243552 jdbc:mysql://127.0.0.1:3306/ds1
2020-05-22 09:27:52.980  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: SELECT * FROM undo_log WHERE branch_id = ? AND xid = ? FOR UPDATE
2020-05-22 09:27:52.980  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@24965d4, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@1a720567), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@1a720567, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=7, distinctRow=false, projections=[ShorthandProjection(owner=Optional.empty, actualColumns=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=branch_id, alias=Optional.empty), ColumnProjection(owner=null, name=xid, alias=Optional.empty), ColumnProjection(owner=null, name=context, alias=Optional.empty), ColumnProjection(owner=null, name=rollback_info, alias=Optional.empty), ColumnProjection(owner=null, name=log_status, alias=Optional.empty), ColumnProjection(owner=null, name=log_created, alias=Optional.empty), ColumnProjection(owner=null, name=log_modified, alias=Optional.empty), ColumnProjection(owner=null, name=ext, alias=Optional.empty)])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@14b5e859, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@108a6e17, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@5588e262, containsSubquery=false)
2020-05-22 09:27:52.981  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM undo_log WHERE branch_id = ? AND xid = ? FOR UPDATE ::: [2012243552, 192.168.10.107:8091:2012243545]
2020-05-22 09:27:52.983  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: SELECT * FROM t_order WHERE id in (?)
2020-05-22 09:27:52.983  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@64adbefc, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@6497500b), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@6497500b, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=7, distinctRow=false, projections=[ShorthandProjection(owner=Optional.empty, actualColumns=[ColumnProjection(owner=null, name=id, alias=Optional.empty), ColumnProjection(owner=null, name=order_no, alias=Optional.empty), ColumnProjection(owner=null, name=user_id, alias=Optional.empty), ColumnProjection(owner=null, name=commodity_code, alias=Optional.empty), ColumnProjection(owner=null, name=count, alias=Optional.empty), ColumnProjection(owner=null, name=amount, alias=Optional.empty)])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@272e6058, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@35a71d2d, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@369b70d4, containsSubquery=false)
2020-05-22 09:27:52.983  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM t_order0 WHERE id in (?) ::: [1263642704673898497]
2020-05-22 09:27:52.983  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: SELECT * FROM t_order1 WHERE id in (?) ::: [1263642704673898497]
2020-05-22 09:27:52.985  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: DELETE FROM t_order WHERE id = ?
2020-05-22 09:27:52.985  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: DeleteStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.DeleteStatement@2f28ed8, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@2778c7cc), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@2778c7cc)
2020-05-22 09:27:52.985  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: DELETE FROM t_order0 WHERE id = ? ::: [1263642704673898497]
2020-05-22 09:27:52.985  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: DELETE FROM t_order1 WHERE id = ? ::: [1263642704673898497]
2020-05-22 09:27:53.022  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Logic SQL: DELETE FROM undo_log WHERE branch_id = ? AND xid = ?
2020-05-22 09:27:53.022  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : SQLStatement: DeleteStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.DeleteStatement@15c0ff43, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@4cfe761d), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@4cfe761d)
2020-05-22 09:27:53.022  INFO 6268 --- [atch_RMROLE_1_8] ShardingSphere-SQL                       : Actual SQL: ds0 ::: DELETE FROM undo_log WHERE branch_id = ? AND xid = ? ::: [2012243552, 192.168.10.107:8091:2012243545]
2020-05-22 09:27:53.127  INFO 6268 --- [atch_RMROLE_1_8] i.s.r.d.undo.AbstractUndoLogManager      : xid 192.168.10.107:8091:2012243545 branch 2012243552, undo_log deleted with GlobalFinished
2020-05-22 09:27:53.128  INFO 6268 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked</code></pre>
<p style=""></p>
<p style="">我们查看数据库数据，已经回滚，和上面的数据一致。</p>
<p style=""></p>
<p style="">到这里一个简单的<code>seata1.2.0</code>、<code>sharding-sphere4.1.0</code>和<code>dubbo2.7.5</code> 的整合案例基本就分析结束。感谢你的学习。如果想交流的可以私信我。</p>
<p style=""></p>
<p style=""></p>
<p style="">来源：Github-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://github.com/lidong1665/seata-spring-boot-dubbo-nacos-shardingsphere-examples">lidong1665</a></p>
<p style=""></p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/2KSidJ72YsovpJ94P1aK1g">https://github.com/lidong1665/seata-spring-boot-dubbo-nacos-shardingsphere-examples</a></p>]]></description><guid isPermaLink="false">/archives/shi-yong-seata-sharding-spherehe-dubboda-jian-kuang-jia</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fseata.webp&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Fri, 12 Jan 2024 13:18:00 GMT</pubDate></item><item><title><![CDATA[Spring @Valid 注解类型使用]]></title><link>https://xiaoming728.com/archives/spring-valid-zhu-jie-lei-xing-shi-yong</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Spring%20%40Valid%20%E6%B3%A8%E8%A7%A3%E7%B1%BB%E5%9E%8B%E4%BD%BF%E7%94%A8&amp;url=/archives/spring-valid-zhu-jie-lei-xing-shi-yong" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E4%B8%80%E3%80%81%E7%B3%BB%E7%BB%9F%E9%BB%98%E8%AE%A4%E6%B3%A8%E8%A7%A3">一、系统默认注解</h3>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FAFAFA">
    <p style="">限制</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FAFAFA">
    <p style="">说明</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Null</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制只能为null</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@NotNull</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须不为null</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@AssertFalse</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须为false</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@AssertTrue</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须为true</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@DecimalMax(value)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须为一个不大于指定值的数字</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@DecimalMin(value)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须为一个不小于指定值的数字</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Digits(integer,fraction)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须为一个小数，且整数部分的位数不能超过integer，小数部分的位数不能超过fraction</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Future</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须是一个将来的日期</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Max(value)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须为一个不大于指定值的数字</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Min(value)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须为一个不小于指定值的数字</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Past</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须是一个过去的日期</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Pattern(value)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制必须符合指定的正则表达式</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Size(max,min)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">限制字符长度必须在min到max之间</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Past</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">验证注解的元素值（日期类型）比当前时间早</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@NotEmpty</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">验证注解的元素值不为null且不为空（字符串长度不为0、集合大小不为0）</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@NotBlank</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">验证注解的元素值不为空（不为null、去除首位空格后长度为0），不同于@NotEmpty，@NotBlank只应用于字符串且在比较时会去除字符串的空格</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">@Email</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">验证注解的元素值是Email，也可以通过正则表达式和flag指定自定义的email格式</p></td>
  </tr>
 </tbody>
</table>
<p style="">注意，不要错用了异常类型，比如在int上不可用@size</p>
<p style=""></p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E4%BD%BF%E7%94%A8%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">二、使用正则表达式</h3>
<pre><code>@Pattern(regexp = "\\w+$")</code></pre>
<p style="">把这个注解加在entity的参数上，可以选择分类也可以默认；</p>
<p style="">关于注解中需要传的参数：一般默认就填入正则表达式即可，但是java中字符串需要转义，这个需要注意一下。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2020%2Fpng%2F550960%2F1587695562479-821c9d6e-f4fc-42f3-9578-bdb4861cf0f8.png&amp;size=m" width="618" style="display: inline-block"></p>
<pre><code>@Pattern(regexp = "\\w+$")
private String userName;</code></pre>
<p style="">在entity传参或者controller层中的添加<span fontsize="" color="rgb(187, 181, 41)" style="color: rgb(187, 181, 41)">@Valid</span>注解进行调用</p>
<pre><code>@PostMapping("/add")
public void addUser(@Valid @RequestBody User user){
    ...
}</code></pre>
<h3 style="" id="%E4%B8%89%E3%80%81%E5%B8%B8%E7%94%A8%E7%9A%84%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">三、常用的正则表达式</h3>
<pre><code>一、校验数字的表达式
1 数字：^[0-9]*$
2 n位的数字：^\d{n}$
3 至少n位的数字：^\d{n,}$
4 m-n位的数字：^\d{m,n}$
5 零和非零开头的数字：^(0|[1-9][0-9]*)$
6 非零开头的最多带两位小数的数字：^([1-9][0-9]*)+(.[0-9]{1,2})?$
7 带1-2位小数的正数或负数：^(\-)?\d+(\.\d{1,2})?$
8 正数、负数、和小数：^(\-|\+)?\d+(\.\d+)?$
9 有两位小数的正实数：^[0-9]+(.[0-9]{2})?$
10 有1~3位小数的正实数：^[0-9]+(.[0-9]{1,3})?$
11 非零的正整数：^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
12 非零的负整数：^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
13 非负整数：^\d+$ 或 ^[1-9]\d*|0$
14 非正整数：^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
15 非负浮点数：^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
16 非正浮点数：^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
17 正浮点数：^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
18 负浮点数：^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
19 浮点数：^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$

二、校验字符的表达式
1 汉字：^[\u4e00-\u9fa5]{0,}$
2 英文和数字：^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
3 长度为3-20的所有字符：^.{3,20}$
4 由26个英文字母组成的字符串：^[A-Za-z]+$
5 由26个大写英文字母组成的字符串：^[A-Z]+$
6 由26个小写英文字母组成的字符串：^[a-z]+$
7 由数字和26个英文字母组成的字符串：^[A-Za-z0-9]+$
8 由数字、26个英文字母或者下划线组成的字符串：^\w+$ 或 ^\w{3,20}$
9 中文、英文、数字包括下划线：^[\u4E00-\u9FA5A-Za-z0-9_]+$
10 中文、英文、数字但不包括下划线等符号：^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
11 可以输入含有^%&amp;',;=?$\"等字符：[^%&amp;',;=?$\x22]+
12 禁止输入含有~的字符：[^~\x22]+

三、特殊需求表达式
1 Email地址：^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
2 域名：[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
3 InternetURL：[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&amp;=]*)?$
4 手机号码：^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
5 电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX)：^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$ 
6 国内电话号码(0511-4405222、021-87888822)：\d{3}-\d{8}|\d{4}-\d{7}
7 身份证号：
  15或18位身份证：^\d{15}|\d{18}$
  15位身份证：^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$
  18位身份证：^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{4}$
8 短身份证号码(数字、字母x结尾)：^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$
9 帐号是否合法(字母开头，允许5-16字节，允许字母数字下划线)：^[a-zA-Z][a-zA-Z0-9_]{4,15}$
10 密码(以字母开头，长度在6~18之间，只能包含字母、数字和下划线)：^[a-zA-Z]\w{5,17}$
11 强密码(必须包含大小写字母和数字的组合，不能使用特殊字符，长度在8-10之间)：^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$ 
12 日期格式：^\d{4}-\d{1,2}-\d{1,2}
13 一年的12个月(01～09和1～12)：^(0?[1-9]|1[0-2])$
14 一个月的31天(01～09和1～31)：^((0?[1-9])|((1|2)[0-9])|30|31)$ 
15 钱的输入格式：
  1.有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000"：^[1-9][0-9]*$ 
  2.这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式：^(0|[1-9][0-9]*)$ 
  3.一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号：^(0|-?[1-9][0-9]*)$ 
  4.这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分：^[0-9]+(.[0-9]+)?$ 
  5.必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的：^[0-9]+(.[0-9]{2})?$ 
  6.这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样：^[0-9]+(.[0-9]{1,2})?$ 
  7.这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样：^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$ 
  8.1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须：^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$ 
	备注：这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
25 xml文件：^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
26 中文字符的正则表达式：[\u4e00-\u9fa5]
27 双字节字符：[^\x00-\xff] (包括汉字在内，可以用来计算字符串的长度(一个双字节字符长度计2，ASCII字符计1))
28 空白行的正则表达式：\n\s*\r (可以用来删除空白行)
29 HTML标记的正则表达式：&lt;(\S*?)[^&gt;]*&gt;.*?&lt;/\1&gt;|&lt;.*? /&gt; (网上流传的版本太糟糕，上面这个也仅仅能部分，对于复杂的嵌套标记依旧无能为力)
30 首尾空白字符的正则表达式：^\s*|\s*$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等)，非常有用的表达式)
31 腾讯QQ号：[1-9][0-9]{4,} (腾讯QQ号从10000开始)
32 中国邮政编码：[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
33 IP地址：\d+\.\d+\.\d+\.\d+ (提取IP地址时有用)</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/spring-valid-zhu-jie-lei-xing-shi-yong</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fspring%2520valid.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Fri, 12 Jan 2024 13:17:32 GMT</pubDate></item><item><title><![CDATA[SpringBoot @Validated 和 @Valid 的区别]]></title><link>https://xiaoming728.com/archives/springboot-validated-he-valid-de-qu-bie</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=SpringBoot%20%40Validated%20%E5%92%8C%20%40Valid%20%E7%9A%84%E5%8C%BA%E5%88%AB&amp;url=/archives/springboot-validated-he-valid-de-qu-bie" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：微信公众号-Java知音</p>
<p style="">日期：2022-06-02 08:45</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/vErmG-x8zSYJoNCWLovndg">https://mp.weixin.qq.com/s/vErmG-x8zSYJoNCWLovndg</a></p>
<h2 style="" id="%E6%A6%82%E8%BF%B0"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">概述</span></h2>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Valid</span><span fontsize="" color="black" style="color: black">是使用</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">Hibernate validation</span><span fontsize="" color="black" style="color: black">的时候使用</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Validated</span><span fontsize="" color="black" style="color: black">是只用</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">Spring Validator</span><span fontsize="" color="black" style="color: black">校验机制使用</span></p></li>
</ul>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">说明：java的JSR303声明了</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Valid</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">这类接口，而</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">Hibernate-validator</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">对其进行了实现</span></p>
<p style=""><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Validation</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">对</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Valid</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">进行了二次封装，在使用上并没有区别，但在分组、注解位置、嵌套验证等功能上有所不同，这里主要就这几种情况进行说明。</span></p>
<h2 style="" id="%E6%B3%A8%E8%A7%A3%E4%BD%8D%E7%BD%AE"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">注解位置</span></h2>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Validated</span><span fontsize="" color="black" style="color: black">：用在类型、方法和方法参数上。但不能用于成员属性（field）</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Valid</span><span fontsize="" color="black" style="color: black">：可以用在方法、构造函数、方法参数和成员属性（field）上</span></p></li>
</ul>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">如：</span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1655172495528-bb83a973-8df0-48ee-87da-acf22d395fdc.png&amp;size=m" width="265" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1655172506560-8029af39-0382-45cc-91cb-87c8a4df20e7.png&amp;size=m" width="289" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">如果</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Validated</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">注解在成员属性上，则会报不适用于field错误</span></p>
<h2 style="" id="%E5%88%86%E7%BB%84%E6%A0%A1%E9%AA%8C"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">分组校验</span></h2>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Validated</span><span fontsize="" color="black" style="color: black">：提供分组功能，可以在参数验证时，根据不同的分组采用不同的验证机制</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Valid</span><span fontsize="" color="black" style="color: black">：没有分组功能</span></p></li>
</ul>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">举例：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">定义分组接口：</span></p>
<pre><code>public interface IGroupA {
}
 
public interface IGroupB {
}</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">定义需要检验的参数bean：</span></p>
<pre><code>public class StudentBean implements Serializable{
    @NotBlank(message = "用户名不能为空")
    private String name;
    //只在分组为IGroupB的情况下进行验证
    @Min(value = 18, message = "年龄不能小于18岁", groups = {IGroupB.class})
    private Integer age;
    @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
    private String phoneNum;
    @Email(message = "邮箱格式错误")
    private String email;
    @MyConstraint
    private String className;</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">测试代码：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">检验分组为IGroupA的情况</span></p>
<pre><code>@RestController
public class CheckController {
    @PostMapping("stu")
    public String addStu(@Validated({IGroupA.class}) @RequestBody StudentBean studentBean){
        return "add student success";
    }
}</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">测试：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1655172568053-170eb078-85ed-4857-8e68-e67a376dabc4.png&amp;size=m" width="496" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">这里对分组IGroupB的就没检验了</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">如果把测试代码改成下面这样，看看测试结果</span></p>
<pre><code>@RestController
public class CheckController {
    @PostMapping("stu")
    public String addStu(@Validated({IGroupA.class, IGroupB.class}) @RequestBody StudentBean studentBean){
        return "add student success";
    }
}</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">说明：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">1、不分 配groups，默认每次都要进行验证</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">2、对一个参数需要多种验证方式时，也可通过分配不同的组达到目的。</span></p>
<h2 style="" id="%E7%BB%84%E5%BA%8F%E5%88%97"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">组序列</span></h2>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">默认情况下 不同级别的约束验证是无序的，但是在一些情况下，顺序验证却是很重要。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">一个组可以定义为其他组的序列，使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候，如果序列前边的组验证失败，则后面的组将不再给予验证。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">举例：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">定义组序列：</span></p>
<pre><code>@GroupSequence({Default.class, IGroupA.class, IGroupB.class})
public interface IGroup {
}</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">需要校验的Bean，分别定义IGroupA对age进行校验，IGroupB对className进行校验：</span></p>
<pre><code>public class StudentBean implements Serializable{
    @NotBlank(message = "用户名不能为空")
    private String name;
    @Min(value = 18, message = "年龄不能小于18岁", groups = IGroupA.class)
    private Integer age;
    @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
    private String phoneNum;
    @Email(message = "邮箱格式错误")
    private String email;
    @MyConstraint(groups = IGroupB.class)
    private String className;</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">测试代码：</span></p>
<pre><code>@RestController
public class CheckController {
    @PostMapping("stu")
    public String addStu(@Validated({IGroup.class}) @RequestBody StudentBean studentBean){
        return "add student success";
    }
}</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">测试发现，如果age出错，那么对组序列在IGroupA后的IGroupB不进行校验，即例子中的className不进行校验，结果如下：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1655172667712-61353036-e4ab-47c1-83e9-26e24117560b.png&amp;size=m" width="417" style="display: inline-block"></p>
<h2 style="" id="%E5%B5%8C%E5%A5%97%E6%A0%A1%E9%AA%8C"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">嵌套校验</span></h2>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">一个待验证的pojo类，其中还包含了待验证的对象，需要在待验证对象上注解</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Valid</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">，才能验证待验证对象中的成员属性，这里不能使用</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Validated</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">举例：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">需要约束校验的bean：</span></p>
<pre><code>public class TeacherBean {
    @NotEmpty(message = "老师姓名不能为空")
    private String teacherName;
    @Min(value = 1, message = "学科类型从1开始计算")
    private int type;</code></pre>
<pre><code>public class StudentBean implements Serializable{
    @NotBlank(message = "用户名不能为空")
    private String name;
    @Min(value = 18, message = "年龄不能小于18岁")
    private Integer age;
    @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
    private String phoneNum;
    @Email(message = "邮箱格式错误")
    private String email;
    @MyConstraint
    private String className;
 
    @NotNull(message = "任课老师不能为空")
    @Size(min = 1, message = "至少有一个老师")
    private List&lt;TeacherBean&gt; teacherBeans;</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">注意：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">这里对</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">teacherBeans</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">只校验了</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">NotNull</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">, 和 Size，并没有对teacher信息里面的字段进行校验，具体测试如下：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1655172701534-b71bb382-f2c3-4cfc-965f-b7222a60fce3.png&amp;size=m" width="511" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">这里teacher中的type明显是不符合约束要求的，但是能检测通过，是因为在student中并没有做 嵌套校验</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">可以在</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">teacherBeans</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">中加上 </span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">@Valid</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">，具体如下：</span></p>
<pre><code>@Valid
@NotNull(message = "任课老师不能为空")
@Size(min = 1, message = "至少有一个老师")
private List&lt;TeacherBean&gt; teacherBeans;</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">这里再来测试，会发现如下结果：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1655172735801-ab608124-39ee-4c89-8858-d2b14aa4a9ac.png&amp;size=m" width="454" style="display: inline-block"></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/springboot-validated-he-valid-de-qu-bie</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fspring%2520valid.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Fri, 12 Jan 2024 13:17:16 GMT</pubDate></item><item><title><![CDATA[Spring @Validated和@Valid区别]]></title><link>https://xiaoming728.com/archives/spring-validatedhe-validqu-bie</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Spring%20%40Validated%E5%92%8C%40Valid%E5%8C%BA%E5%88%AB&amp;url=/archives/spring-validatedhe-validqu-bie" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px">Spring validation验证框架对入参实体进行嵌套验证必须在相应属性（字段）加上@Valid而不是@Validated</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">Spring Validation验证框架对参数的验证机制提供了@Validated（Spring's JSR-303规范，是标准JSR-303的一个变种），javax提供了@Valid（标准JSR-303规范），配合BindingResult可以直接提供参数验证结果。其中对于字段的特定验证注解比如@NotNull等网上到处都有，这里不详述。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">在检验Controller的入参是否符合规范时，使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同：</span></p>
<p style=""></p>
<h3 style="" id="1.-%E5%88%86%E7%BB%84">1. 分组</h3>
<p style=""><span style="font-size: 15px">@Validated：提供了一个分组功能，可以在入参验证时，根据不同的分组采用不同的验证机制，这个网上也有资料，不详述。@Valid：作为标准JSR-303规范，还没有吸收分组的功能。</span></p>
<p style=""></p>
<h3 style="" id="2.-%E6%B3%A8%E8%A7%A3%E5%9C%B0%E6%96%B9">2. 注解地方</h3>
<p style=""><span style="font-size: 15px">@Validated：可以用在类型、方法和方法参数上。但是不能用在成员属性（字段）上</span></p>
<p style=""><span style="font-size: 15px">@Valid：可以用在方法、构造函数、方法参数和成员属性（字段）上</span></p>
<p style=""><span style="font-size: 15px">两者是否能用于成员属性（字段）上直接影响能否提供嵌套验证的功能。</span></p>
<p style=""></p>
<h3 style="" id="3.-%E5%B5%8C%E5%A5%97%E9%AA%8C%E8%AF%81">3. 嵌套验证</h3>
<p style=""><span style="font-size: 15px">在比较两者嵌套验证时，先说明下什么叫做嵌套验证。比如我们现在有个实体叫做Item：</span></p>
<pre><code>public class Item {
    @NotNull(message = "id不能为空")
    @Min(value = 1, message = "id必须为正整数")
    private Long id;

    @NotNull(message = "props不能为空")
    @Size(min = 1, message = "至少要有一个属性")
    private List&lt;Prop&gt; props;
}</code></pre>
<p style=""><span style="font-size: 15px">Item带有很多属性，属性里面有属性id，属性值id，属性名和属性值，如下所示：</span></p>
<pre><code>public class Prop {
    @NotNull(message = "pid不能为空")
    @Min(value = 1, message = "pid必须为正整数")
    private Long pid;

    @NotNull(message = "vid不能为空")
    @Min(value = 1, message = "vid必须为正整数")
    private Long vid;

    @NotBlank(message = "pidName不能为空")
    private String pidName;

    @NotBlank(message = "vidName不能为空")
    private String vidName;
}</code></pre>
<p style=""><span style="font-size: 15px">属性这个实体也有自己的验证机制，比如属性和属性值id不能为空，属性名和属性值不能为空等。</span></p>
<p style=""><span style="font-size: 15px">现在我们有个ItemController接受一个Item的入参，想要对Item进行验证，如下所示：</span></p>
<pre><code>@RestController
public class ItemController {

    @RequestMapping("/item/add")
    public void addItem(@Validated Item item, BindingResult bindingResult) {
        doSomething();
    }
}</code></pre>
<p style=""><span style="font-size: 15px">在上图中，如果Item实体的props属性不额外加注释，只有@NotNull和@Size，无论入参采用@Validated还是@Valid验证，Spring Validation框架只会对Item的id和props做非空和数量验证，不会对props字段里的Prop实体进行字段验证，也就是@Validated和@Valid加在方法参数前，都不会自动对参数进行嵌套验证。也就是说如果传的List&lt;Prop&gt;中有Prop的pid为空或者是负数，入参验证不会检测出来。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">为了能够进行嵌套验证，必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性（字段）上，但是@Valid能加在成员属性（字段）上，而且@Valid类注解上也说明了它支持嵌套验证功能，那么我们能够推断出：@Valid加在方法参数时并不能够自动进行嵌套验证，而是用在需要嵌套验证类的相应字段上，来配合方法参数上@Validated或@Valid来进行嵌套验证。</span></p>
<p style=""><span style="font-size: 15px">我们修改Item类如下所示：</span></p>
<pre><code>public class Item {
    @NotNull(message = "id不能为空")
    @Min(value = 1, message = "id必须为正整数")
    private Long id;

    @Valid // 嵌套验证必须用@Valid
    @NotNull(message = "props不能为空")
    @Size(min = 1, message = "props至少要有一个自定义属性")
    private List&lt;Prop&gt; props;
}</code></pre>
<p style=""><span style="font-size: 15px">然后我们在ItemController的addItem函数上再使用@Validated或者@Valid，就能对Item的入参进行嵌套验证。此时Item里面的props如果含有Prop的相应字段为空的情况，Spring Validation框架就会检测出来，bindingResult就会记录相应的错误。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">总结一下@Validated和@Valid在嵌套验证功能上的区别：</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">@Validated：用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性（字段）上，也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">@Valid：用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性（字段）上，提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。</span></p>]]></description><guid isPermaLink="false">/archives/spring-validatedhe-validqu-bie</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fspring%2520valid.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Fri, 12 Jan 2024 13:17:01 GMT</pubDate></item><item><title><![CDATA[SpringBoot 内置 Tomcat 线程数优化配置]]></title><link>https://xiaoming728.com/archives/springboot-nei-zhi-tomcat-xian-cheng-shu-you-hua-pei-zhi</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=SpringBoot%20%E5%86%85%E7%BD%AE%20Tomcat%20%E7%BA%BF%E7%A8%8B%E6%95%B0%E4%BC%98%E5%8C%96%E9%85%8D%E7%BD%AE&amp;url=/archives/springboot-nei-zhi-tomcat-xian-cheng-shu-you-hua-pei-zhi" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：微信公众号-Java学习者社区</p>
<p style="">日期：2022-05-21 11:45</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/5kybQx3VToNrv9_S3ORReQ">https://mp.weixin.qq.com/s/5kybQx3VToNrv9_S3ORReQ</a></p>
<p style="">标题：深度理解Tomcat的acceptCount、maxConnections、maxThreads</p>
<p style="">日期：2019-03-22 14:22:40</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/5kybQx3VToNrv9_S3ORReQ">https://blog.csdn.net/zzzgd_666/article/details/88740198</a></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">本文解析springboot内置tomcat调优并发线程数的一些参数，并结合源码进行分析</span></p>
<h3 style="" id="%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参数配置</span></h3>
<h4 style="" id="%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%A0%B8%E5%BF%83%E7%BA%BF%E7%A8%8B%E6%95%B0"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">线程池核心线程数</span></h4>
<p style=""><code>server.tomcat.threads.min-spare: 10</code></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">该参数为tomcat处理业务的核心线程数大小，默认值为10，一般跟随CPU核心数</span></p>
<h4 style="" id="%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%9C%80%E5%A4%A7%E7%BA%BF%E7%A8%8B%E6%95%B0"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">线程池最大线程数</span></h4>
<p style=""><code>server.tomcat.threads.max: 200</code></p>
<p style="">请求处理线程的最大数量。默认值是200（Tomcat7和8都是的）。如果该Connector绑定了Executor，这个值会被忽略，因为该Connector将使用绑定的Executor，而不是内置的线程池来执行任务。</p>
<p style="">maxThreads规定的是最大的线程数目，并不是实际running的CPU数量；实际上，maxThreads的大小比CPU核心数量要大得多。这是因为，处理请求的线程真正用于计算的时间可能很少，大多数时间可能在阻塞，如等待数据库返回数据、等待硬盘读写数据等。因此，在某一时刻，只有少数的线程真正的在使用物理CPU，大多数线程都在等待；因此线程数远大于物理核心数才是合理的。</p>
<p style="">换句话说，<strong>Tomcat通过使用比CPU核心数量多得多的线程数，可以使CPU忙碌起来，大大提高CPU的利用率</strong>。</p>
<h4 style="" id="%E8%AF%B7%E6%B1%82%E6%9C%80%E5%A4%A7%E8%BF%9E%E6%8E%A5%E6%95%B0"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">请求最大连接数</span></h4>
<p style=""><code>server.tomcat.max-connections: 8192</code></p>
<p style="">Tomcat在任意时刻接收和处理的最大连接数。当Tomcat接收的连接数达到maxConnections时，Acceptor线程不会读取accept队列中的连接；这时accept队列中的线程会一直阻塞着，直到Tomcat接收的连接数小于maxConnections。如果设置为-1，则连接数不受限制。</p>
<p style="">默认值与连接器使用的协议有关：NIO的默认值是10000，APR/native的默认值是8192，而BIO的默认值为maxThreads（如果配置了Executor，则默认值是Executor的maxThreads）。</p>
<p style="">在windows下，APR/native的maxConnections值会自动调整为设置值以下最大的1024的整数倍；如设置为2000，则最大值实际是1024。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1653276163856-d82c36ed-ab89-42a7-83c7-badc88a22127.png&amp;size=m" width="477" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1653276252065-7ec1b926-0102-4df3-a398-63e07c05beb1.png&amp;size=m" width="585" style="display: inline-block"></p>
<h4 style="" id="%E8%AF%B7%E6%B1%82%E6%9C%80%E5%A4%A7%E9%98%9F%E5%88%97%E6%95%B0">请求最大队列数</h4>
<p style=""><code>server.tomcat.accept-count: 100</code></p>
<p style="">accept队列的长度；当accept队列中连接的个数达到acceptCount时，队列满，进来的请求一律被拒绝。默认值是100。<span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">这个参数实际上和tomcat没有太大关系，它是服务端创建</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">ServerSocket</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">时操作系统控制同时连接的最大数量的，服务端接收连接是通过</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">accept()</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">来的，</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">accept()是</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">非常快的，所以</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">accept-count</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">的不需要太大，正常保持默认值100即可了，</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">acceptCount</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">这个参数和线程池无关，会被映射为backlog参数，是socket的参数，在源码的使用是在</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">NioEndpoint</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">类的</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">initServerSocket</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">方法，在tomcat中的名字是</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">backlog</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">在springboot内置tomcat中名字没有使用</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">backlog</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">而是使用</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">acceptCount</span></p>
<h3 style="" id="tomcat%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%A4%84%E7%90%86%E6%9C%BA%E5%88%B6"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">Tomcat线程池处理机制</span></h3>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">tomcat最终使用线程池来处理业务逻辑，java默认的线程池的规则：</span></p>
<p style="">核心线程数满了则优先放入队列，当队列满了之后才会创建非核心线程来处理</p>
<p style="">那么tomcat是这样做的吗？</p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">首先如果tomcat这样做，那么当达到核心线程后后续任务就需要等待了，这显然是不合理的，我们通过源码来看下tomcat是如何处理的</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">在</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">AbstractEndpoint</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">的</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">createExecutor</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">创建了处理业务数据的线程池</span></p>
<pre><code>public void createExecutor() {
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">主要是使用了</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">TaskQueue</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">队列，</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">ThreadPoolExecutor</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">并不是jdk的，而是tomcat重写的。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">我们从线程池的处理方法</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">execute</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">看起</span></p>
<pre><code>public void execute(Runnable command) {
    execute(command,0,TimeUnit.MILLISECONDS);
}</code></pre>
<pre><code>public void execute(Runnable command, long timeout, TimeUnit unit) {
    submittedCount.incrementAndGet();
    try {
        // 核心代码
        super.execute(command);
    } catch (RejectedExecutionException rx) {
        if (super.getQueue() instanceof TaskQueue) {
            final TaskQueue queue = (TaskQueue)super.getQueue();
            try {
                if (!queue.force(command, timeout, unit)) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException("Queue capacity is full.");
                }
            } catch (InterruptedException x) {
                submittedCount.decrementAndGet();
                throw new RejectedExecutionException(x);
            }
        } else {
            submittedCount.decrementAndGet();
            throw rx;
        }

    }
}</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">又调用会jdk的</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">execute</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">了</span></p>
<pre><code>public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    // 1、工作线程数小于核心线程数则添加任务，核心线程会处理
    if (workerCountOf(c) &lt; corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2、工作线程不小于核心线程数，则放到workQueue队列中
    if (isRunning(c) &amp;&amp; workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) &amp;&amp; remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 3、否则添加任务，addWorker会进行创建线程
    else if (!addWorker(command, false))
        reject(command);
}</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">从这里可以看到jdk线程池的机制，tomcat使用了自己的</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">TaskQueue</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">队列，所以我们看代码2处当核心线程用完了会调用队列的offer方法</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">我们看</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">TaskQueue</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">的offer</span></p>
<pre><code>public boolean offer(Runnable o) {
    //we can't do any checks
    // parent就是指线程池，没有线程池则添加到队列
    if (parent==null) return super.offer(o);
    //we are maxed out on threads, simply queue the object
    // 线程数量已经达到了最大线程数，那么只能添加到队列
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
    //we have idle threads, just add it to the queue
    // 如果当前处理的任务数量小于当前线程池中线程的数量，那么任务放到线程池，即相当于马上会有空闲线程来处理
    if (parent.getSubmittedCount()&lt;=(parent.getPoolSize())) return super.offer(o);
    //if we have less threads than maximum force creation of a new thread
    // TODO 核心代码，如果当前线程数量还没有达到线程池最大线程池的数量，那么就直接创建线程，这里返回false
    if (parent.getPoolSize()&lt;parent.getMaximumPoolSize()) return false;
    //if we reached here, we need to add it to the queue
    // 最后的策略，放到队列
    return super.offer(o);
}</code></pre>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">可以看到当执行offer时，不是直接放到队列的，当线程池总线程数量还没达到线程池最大线程数时会返回false，返回false时就会执行线程池execute的代码3处，执行</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">addWorker(command, false)</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">，也就开始创建新的线程来处理当前任务了</span></p>
<h3 style="" id="%E6%80%BB%E7%BB%93"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">总结</span></h3>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">tomcat主要通过使用自己的</span><span fontsize="" color="rgb(30, 107, 184)" style="color: rgb(30, 107, 184)">TaskQueue</span><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">队列来对线程池做出了不同的策略，也就是tomcat当线程数大于核心数时就会直接创建新的线程来处理，而不是放到队列</span></p>]]></description><guid isPermaLink="false">/archives/springboot-nei-zhi-tomcat-xian-cheng-shu-you-hua-pei-zhi</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fspringboot%2520tomcat.png&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Fri, 12 Jan 2024 13:17:00 GMT</pubDate></item><item><title><![CDATA[docker容器中没有vi编辑器的解决办法]]></title><link>https://xiaoming728.com/archives/dockerrong-qi-zhong-mei-you-vibian-ji-qi-de-jie-jue-ban-fa</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=docker%E5%AE%B9%E5%99%A8%E4%B8%AD%E6%B2%A1%E6%9C%89vi%E7%BC%96%E8%BE%91%E5%99%A8%E7%9A%84%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95&amp;url=/archives/dockerrong-qi-zhong-mei-you-vibian-ji-qi-de-jie-jue-ban-fa" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px">来源：博客-</span><span style="font-size: 12px; color: rgb(103, 194, 58)"> </span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://www.jiajiajia.club/blog/user/4ba0f663/1">硅谷探秘者</a></p>
<p style=""><span style="font-size: 15px">日期：2021年3月2日</span></p>
<p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://www.jiajiajia.club/blog/user/4ba0f663/1">http://www.jiajiajia.club/blog/artical/89ud1wmkn9i6/439</a></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<ol>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">先执行命令：apt-get update</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">然后执行：apt-get install vim</span></p></li>
</ol>
<p style=""></p>
<p style=""></p>
<h3 style="" id="1.%E6%B2%A1%E6%9C%89vim%E5%91%BD%E4%BB%A4"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">1.没有vim命令</span></h3>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F21432006%2F1638412081165-5da72759-e1cd-4d1f-b43d-d24e639dfc2f.png&amp;size=m" width="415" style="display: inline-block"></p>
<h3 style="" id="2.%E4%BD%BF%E7%94%A8apt-get%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">2.使用apt-get命令安装</span></h3>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">命令如下：apt-get install vim</span></p>
<h3 style="" id="3.%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E5%8F%AF%E8%83%BD%E4%BC%9A%E6%8A%A5%E9%94%99%E5%A6%82%E4%B8%8B%EF%BC%9A"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">3.执行过程可能会报错如下：</span></h3>
<h4 style="" id="1.%E5%A6%82%E6%9E%9C%E8%BF%9B%E5%85%A5%E5%AE%B9%E5%99%A8%E6%97%B6%E6%B2%A1%E6%9C%89%E6%8C%87%E5%AE%9Aroot%E7%94%A8%E6%88%B7%EF%BC%8C%E5%88%99%E5%8F%AF%E8%83%BD%E4%BC%9A%E6%8A%A5%E9%94%99"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">1.如果进入容器时没有指定root用户，则可能会报错</span></h4>
<p style="">E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied) E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F21432006%2F1638412099042-0429199c-2a18-40c3-a932-963987fd1bcd.png&amp;size=m" width="676" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">解决：</span></p>
<ol>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">exit退出容器，指定root用户进入容器</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">命令如 ： docker exec -it --user root 1837cf8cd4b9 /bin/bash</span></p></li>
</ol>
<h4 style="" id="2.%E5%A6%82%E6%9E%9C%E4%B9%8B%E5%89%8D%E6%B2%A1%E6%9C%89%E6%89%A7%E8%A1%8Capt-get-update%E8%BF%99%E4%B8%AA%E5%91%BD%E4%BB%A4%EF%BC%8C%E5%8F%AF%E8%83%BD%E4%BC%9A%E6%8A%A5%E9%94%99"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">2.如果之前没有执行apt-get update这个命令，可能会报错</span></h4>
<p style="">Reading package lists... Done Building dependency tree Reading state information... Done E: Unable to locate package vim</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F21432006%2F1638412114027-2a4b1c1a-00d2-4407-80cf-73686d2ed29e.png&amp;size=m" width="435" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">解决：</span></p>
<ol>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">先执行命令：apt-get update</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">然后执行：apt-get install vim</span></p></li>
</ol>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/dockerrong-qi-zhong-mei-you-vibian-ji-qi-de-jie-jue-ban-fa</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Fri, 12 Jan 2024 13:16:00 GMT</pubDate></item><item><title><![CDATA[MySQL单表超过2000w就要分库分表]]></title><link>https://xiaoming728.com/archives/mysqldan-biao-chao-guo-2000wjiu-yao-fen-ku-fen-biao</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=MySQL%E5%8D%95%E8%A1%A8%E8%B6%85%E8%BF%872000w%E5%B0%B1%E8%A6%81%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8&amp;url=/archives/mysqldan-biao-chao-guo-2000wjiu-yao-fen-ku-fen-biao" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：微信公众号-石杉的架构笔记</p>
<p style="">日期：2022-12-01 07:50</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/A6WS1CSjF7wvBE_gKLyp8w">https://mp.weixin.qq.com/s/A6WS1CSjF7wvBE_gKLyp8w</a></p>
<h2 style="" id="%E4%B8%80%E3%80%81%E5%89%8D%E8%A8%80"><span fontsize="" color="black" style="color: black">一、前言</span></h2>
<p style=""><span fontsize="" color="black" style="color: black">不急于上手实战ShardingSphere框架，先来复习下分库分表的基础概念，技术名词大多晦涩难懂，不要死记硬背理解最重要，当你捅破那层窗户纸，发现其实它也就那么回事。</span></p>
<h2 style="" id="%E4%BA%8C%E3%80%81%E4%BB%80%E4%B9%88%E6%98%AF%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8">二、什么是分库分表</h2>
<p style=""><span fontsize="" color="black" style="color: black">分库分表是在海量数据下，由于单库、表数据量过大，导致数据库性能持续下降的问题，演变出的技术方案。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">分库分表是由</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">分库</span><span fontsize="" color="black" style="color: black">和</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">分表</span><span fontsize="" color="black" style="color: black">这两个独立概念组成的，只不过通常分库与分表的操作会同时进行，以至于我们习惯性的将它们合在一起叫做分库分表。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466817091-59023d09-9b14-4058-b69f-0121a9f59115.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">通过一定的规则，将原本数据量大的数据库拆分成多个单独的数据库，将原本数据量大的表拆分成若干个数据表，使得单一的库、表性能达到最优的效果（响应速度快），以此提升整体数据库性能。</span></p>
<h2 style="" id="%E4%B8%89%E3%80%81%E4%B8%BA%E4%BB%80%E4%B9%88%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8">三、为什么分库分表</h2>
<p style=""><span fontsize="" color="black" style="color: black">单机数据库的存储能力、连接数是有限的，它自身就很容易会成为系统的瓶颈。当单表数据量在百万以里时，我们还可以通过添加从库、优化索引提升性能。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">一旦数据量朝着千万以上趋势增长，再怎么优化数据库，很多操作性能仍下降严重。为了减少数据库的负担，提升数据库响应速度，缩短查询时间，这时候就需要进行分库分表。</span></p>
<h3 style="" id="%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E5%88%86%E5%BA%93%EF%BC%9F"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">为什么需要分库？</span></h3>
<p style=""><strong><span fontsize="" color="black" style="color: black">容量</span></strong></p>
<p style=""><span fontsize="" color="black" style="color: black">我们给数据库实例分配的磁盘容量是固定的，数据量持续的大幅增长，用不了多久单机的容量就会承载不了这么多数据，解决办法简单粗暴，加容量！</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">连接数</span></strong></p>
<p style=""><span fontsize="" color="black" style="color: black">单机的容量可以随意扩展，但数据库的连接数却是有限的，在高并发场景下多个业务同时对一个数据库操作，很容易将连接数耗尽导致</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">too many connections</span><span fontsize="" color="black" style="color: black">报错，导致后续数据库无法正常访问。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">可以通过</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">max_connections</span><span fontsize="" color="black" style="color: black">查看MySQL最大连接数。</span></p>
<p style=""><code>show variables like '%max_connections%'</code></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466817240-2ec45fcc-1118-47aa-bd3a-87843f5f21f3.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">将原本单数据库按不同业务拆分成订单库、物流库、积分库等不仅可以有效分摊数据库读写压力，也提高了系统容错性。</span></p>
<h3 style="" id="%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E5%88%86%E8%A1%A8%EF%BC%9F"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">为什么需要分表？</span></h3>
<p style=""><span fontsize="" color="black" style="color: black">做过报表业务的同学应该都体验过，一条SQL执行时间超过几十秒的场景。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">导致数据库查询慢的原因有很多，SQL没命中索引、like扫全表、用了函数计算，这些都可以通过优化手段解决，可唯独数据量大是MySQL无法通过自身优化解决的。慢的根本原因是</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">InnoDB</span><span fontsize="" color="black" style="color: black">存储引擎，聚簇索引结构的 B+tree 层级变高，磁盘IO变多查询性能变慢，详细原理自行查找一下，这里不用过多篇幅说明。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">阿里的开发手册中有条建议，单表行数超500万行或者单表容量超过2GB，就推荐分库分表，然而理想和实现总是有差距的，阿里这种体量的公司不差钱当然可以这么用，实际上很多公司单表数据几千万、亿级别仍然不选择分库分表。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466817129-a3fbc29e-365c-4628-9172-6a10b0730068.png&amp;size=m" width="1080" style="display: inline-block"></p>
<h2 style="" id="%E5%9B%9B%E3%80%81%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8">四、什么时候分库分表</h2>
<p style=""><span fontsize="" color="black" style="color: black">经常会有小伙伴问，到底什么情况下会用分库分表呢？</span></p>
<p style=""><span fontsize="" color="black" style="color: black">分库分表要解决的是</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">现存海量数据</span><span fontsize="" color="black" style="color: black">访问的性能瓶颈，对</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">持续激增</span><span fontsize="" color="black" style="color: black">的数据量所做出的架构预见性。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">是否分库分表的关键指标是数据量</span></strong><span fontsize="" color="black" style="color: black">，</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/A6WS1CSjF7wvBE_gKLyp8w"><span fontsize="" color="black" style="color: black">我们以</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">fire100.top</span></a><span fontsize="" color="black" style="color: black">这个网站的资源表</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_resource</span><span fontsize="" color="black" style="color: black">为例，系统在运行初始的时候，每天只有可怜的几十个资源上传，这时使用单库、单表的方式足以支持系统的存储，数据量小几乎没什么数据库性能瓶颈。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">但某天开始一股神秘的流量进入，系统每日产生的资源数据量暴增至十万甚至上百万级别，这时资源表数据量到达千万级，查询响应变得缓慢，数据库的性能瓶颈逐渐显现。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">以MySQL数据库为例，单表的数据量在达到亿条级别，通过加索引、SQL调优等传统优化策略，性能提升依旧微乎其微时，就可以考虑做分库分表了。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">既然MySQL存储海量数据时会出现性能瓶颈，那么我们是不是可以考虑用其他方案替代它？比如高性能的非关系型数据库</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">MongoDB</span><span fontsize="" color="black" style="color: black">？</span></p>
<p style=""><span fontsize="" color="black" style="color: black">可以，但要看存储的数据类型！</span></p>
<p style=""><span fontsize="" color="black" style="color: black">现在互联网上大部分公司的核心数据几乎是存储在关系型数据库（MySQL、Oracle等），因为它们有着</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">NoSQL</span><span fontsize="" color="black" style="color: black">如法比拟的稳定性和可靠性，产品成熟生态系统完善，还有核心的事务功能特性，也是其他存储工具不具备的，而评论、点赞这些非核心数据还是可以考虑用</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">MongoDB</span><span fontsize="" color="black" style="color: black">的。</span></p>
<h2 style="" id="%E4%BA%94%E3%80%81%E5%A6%82%E4%BD%95%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8">五、如何分库分表</h2>
<p style=""><span fontsize="" color="black" style="color: black">分库分表的核心就是对数据的分片（</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">Sharding</span><span fontsize="" color="black" style="color: black">）并相对均匀的路由在不同的库、表中，以及分片后对数据的快速定位与检索结果的整合。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">分库与分表可以从：垂直（纵向）和 水平（横向）两种纬度进行拆分。下边我们以经典的订单业务举例，看看如何拆分。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466817095-b9cc5b95-63aa-4ab4-87ff-41b34439344d.png&amp;size=m" width="1080" style="display: inline-block"></p>
<h3 style="" id="%E5%9E%82%E7%9B%B4%E6%8B%86%E5%88%86"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">垂直拆分</span></h3>
<p style=""><strong><span fontsize="" color="black" style="color: black">（1）垂直分库</span></strong></p>
<p style=""><span fontsize="" color="black" style="color: black">垂直分库一般来说按照业务和功能的维度进行拆分，将不同业务数据分别放到不同的数据库中，核心理念</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">专库专用</span><span fontsize="" color="black" style="color: black">。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">按业务类型对数据分离，剥离为多个数据库，像订单、支付、会员、积分相关等表放在对应的订单库、支付库、会员库、积分库。不同业务禁止跨库直连，获取对方业务数据一律通过</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">API</span><span fontsize="" color="black" style="color: black">接口交互，这也是微服务拆分的一个重要依据。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466817781-dd8fbbd1-036f-4f83-90a6-40a8f88eef14.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(136, 136, 136)" style="color: rgb(136, 136, 136)">垂直分库</span></p>
<p style=""><span fontsize="" color="black" style="color: black">垂直分库很大程度上取决于业务的划分，但有时候业务间的划分并不是那么清晰，比如：电商中订单数据的拆分，其他很多业务都依赖于订单数据，有时候界线不是很好划分。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">垂直分库把一个库的压力分摊到多个库，提升了一些数据库性能，但并没有解决由于单表数据量过大导致的性能问题，所以就需要配合后边的分表来解决。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">（2）垂直分表</span></strong></p>
<p style=""><span fontsize="" color="black" style="color: black">垂直分表针对业务上字段比较多的大表进行的，一般是把业务宽表中比较独立的字段，或者不常用的字段拆分到单独的数据表中，是一种大表拆小表的模式。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">例如：一张</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order</span><span fontsize="" color="black" style="color: black">订单表上有几十个字段，其中订单金额相关字段计算频繁，为了不影响订单表</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order</span><span fontsize="" color="black" style="color: black">的性能，就可以把订单金额相关字段拆出来单独维护一个</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order_price_expansion</span><span fontsize="" color="black" style="color: black">扩展表，这样每张表只存储原表的一部分字段，通过订单号</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">order_no</span><span fontsize="" color="black" style="color: black">做关联，再将拆分出来的表路由到不同的库中。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466817757-b821acf7-4751-4301-8dec-42f8aec58591.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">数据库它是以行为单位将数据加载到内存中，这样拆分以后核心表大多是访问频率较高的字段，而且字段长度也都较短，因而可以加载更多数据到内存中，减少磁盘IO，增加索引查询的命中率，进一步提升数据库性能。</span></p>
<h3 style="" id="%E6%B0%B4%E5%B9%B3%E6%8B%86%E5%88%86"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">水平拆分</span></h3>
<p style=""><span fontsize="" color="black" style="color: black">上边垂直分库、垂直分表后还是会存在单库、表数据量过大的问题，当我们的应用已经无法在细粒度的垂直切分时，依旧存在单库读写、存储性能瓶颈，这时就要配合水平分库、水平分表一起了。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">（1）水平分库</span></strong></p>
<p style=""><span fontsize="" color="black" style="color: black">水平分库是把同一个表按一定规则拆分到不同的数据库中，每个库可以位于不同的服务器上，以此实现水平扩展，是一种常见的提升数据库性能的方式。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466817834-8b35fc57-ae91-4016-a584-810640c912be.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">例如：</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">db_orde_1</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">db_order_2</span><span fontsize="" color="black" style="color: black">两个数据库内有完全相同的</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order</span><span fontsize="" color="black" style="color: black">表，我们在访问某一笔订单时可以通过对订单的订单编号取模的方式</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">订单编号 mod 2 （数据库实例数）</span><span fontsize="" color="black" style="color: black">，指定该订单应该在哪个数据库中操作。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">这种方案往往能解决单库存储量及性能瓶颈问题，但由于同一个表被分配在不同的数据库中，数据的访问需要额外的路由工作，因此系统的复杂度也被提升了。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">（2）水平分表</span></strong></p>
<p style=""><span fontsize="" color="black" style="color: black">水平分表是在</span><strong><span fontsize="" color="black" style="color: black">同一个数据库内</span></strong><span fontsize="" color="black" style="color: black">，把一张大数据量的表按一定规则，切分成多个结构完全相同表，而每个表只存原表的一部分数据。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">例如：一张</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order</span><span fontsize="" color="black" style="color: black">订单表有900万数据，经过水平拆分出来三个表，</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order_1</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order_2</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order_3</span><span fontsize="" color="black" style="color: black">，每张表存有数据300万，以此类推。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466818104-3d5b5349-86f8-48ec-a938-144576addb52.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">水平分表尽管拆分了表，但子表都还是在同一个数据库实例中，只是解决了单一表数据量过大的问题，并没有将拆分后的表分散到不同的机器上，还在竞争同一个物理机的CPU、内存、网络IO等。要想进一步提升性能，就需要将拆分后的表分散到不同的数据库中，达到分布式的效果。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466818148-0b0ac5a6-bf95-48e9-aaa2-efd60b933337.png&amp;size=m" width="1080" style="display: inline-block"></p>
<h2 style="" id="%E5%85%AD%E3%80%81%E6%95%B0%E6%8D%AE%E5%AD%98%E5%9C%A8%E5%93%AA%E4%B8%AA%E5%BA%93%E7%9A%84%E8%A1%A8">六、数据存在哪个库的表</h2>
<p style=""><span fontsize="" color="black" style="color: black">分库分表以后会出现一个问题，一张表会出现在多个数据库里，到底该往哪个库的哪个表里存呢？</span></p>
<p style=""><span fontsize="" color="black" style="color: black">上边我们多次提到过</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">一定规则</span><span fontsize="" color="black" style="color: black">，其实这个规则它是一种路由算法，决定了一条数据具体应该存在哪个数据库的哪张表里。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">常见的有</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">取模算法</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">范围限定算法</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">范围+取模算法</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">预定义算法</span></p>
<h3 style="" id="1%E3%80%81%E5%8F%96%E6%A8%A1%E7%AE%97%E6%B3%95"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">1、取模算法</span></h3>
<p style=""><span fontsize="" color="black" style="color: black">关键字段取模（对hash结果取余数 hash(XXX) mod N)，N为数据库实例数或子表数量）是最为常见的一种路由方式。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">以</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order</span><span fontsize="" color="black" style="color: black">订单表为例，先给数据库从 0 到 N-1进行编号，对</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order</span><span fontsize="" color="black" style="color: black">订单表中</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">order_no</span><span fontsize="" color="black" style="color: black">订单编号字段进行取模</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">hash(order_no) mod N</span><span fontsize="" color="black" style="color: black">，得到余数</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">i</span><span fontsize="" color="black" style="color: black">。</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">i=0</span><span fontsize="" color="black" style="color: black">存第一个库，</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">i=1</span><span fontsize="" color="black" style="color: black">存第二个库，</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">i=2</span><span fontsize="" color="black" style="color: black">存第三个库，以此类推。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466818477-45e1a9cb-afef-463e-9eb7-a0d203f0ec01.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">同一笔订单数据会落在同一个库、表里，查询时用相同的规则，用</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order</span><span fontsize="" color="black" style="color: black">订单编号作为查询条件，就能快速的定位到数据。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">优点</span></strong></p>
<p style=""><span fontsize="" color="black" style="color: black">实现简单，数据分布相对比较均匀，不易出现请求都打到一个库上的情况。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">缺点</span></strong></p>
<p style=""><span fontsize="" color="black" style="color: black">取模算法对集群的伸缩支持不太友好，集群中有N个数据库实</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">·hash(user_id) mod N</span><span fontsize="" color="black" style="color: black">，当某一台机器宕机，本应该落在该数据库的请求就无法得到处理，这时宕掉的实例会被踢出集群。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">此时机器数减少算法发生变化</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">hash(user_id) mod N-1</span><span fontsize="" color="black" style="color: black">，同一用户数据落在了在不同数据库中，等这台机器恢复，用</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">user_id</span><span fontsize="" color="black" style="color: black">作为条件查询用户数据就会少一部分。</span></p>
<h3 style="" id="2%E3%80%81%E8%8C%83%E5%9B%B4%E9%99%90%E5%AE%9A%E7%AE%97%E6%B3%95"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">2、范围限定算法</span></h3>
<p style=""><span fontsize="" color="black" style="color: black">范围限定算法以某些范围字段，如</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">时间</span><span fontsize="" color="black" style="color: black">或</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">ID区</span><span fontsize="" color="black" style="color: black">拆分。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">用户表</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user</span><span fontsize="" color="black" style="color: black">被拆分成</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user_1</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user_2</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user_3</span><span fontsize="" color="black" style="color: black">三张表，后续将</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">user_id</span><span fontsize="" color="black" style="color: black">范围为1 ~ 1000w的用户数据放入</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user_1</span><span fontsize="" color="black" style="color: black">，1000~ 2000w放入</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user_2</span><span fontsize="" color="black" style="color: black">，2000~3000w放入</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user_3</span><span fontsize="" color="black" style="color: black">，以此类推。按日期范围划分同理。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466818545-73ec1212-522f-47bf-aef4-074f7c782b85.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">优点</span></strong></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">单表数据量是可控的</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">水平扩展简单只需增加节点即可，无需对其他分片的数据进行迁移</span></p></li>
</ul>
<p style=""><strong><span fontsize="" color="black" style="color: black">缺点</span></strong></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">由于连续分片可能存在</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">数据热点</span><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">，比如按时间字段分片时，如果某一段时间（双11等大促）订单骤增，存11月数据的表可能会被频繁的读写，其他分片表存储的历史数据则很少被查询，导致数据倾斜，数据库压力分摊不均匀。</span></p></li>
</ul>
<h3 style="" id="3%E3%80%81%E8%8C%83%E5%9B%B4-%2B-%E5%8F%96%E6%A8%A1%E7%AE%97%E6%B3%95"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">3、范围 + 取模算法</span></h3>
<p style=""><span fontsize="" color="black" style="color: black">为了避免热点数据的问题，我们可以对上范围算法优化一下</span></p>
<p style=""><span fontsize="" color="black" style="color: black">这次我们先通过范围算法定义每个库的用户表</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user</span><span fontsize="" color="black" style="color: black">只存1000w数据，第一个</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">db_order_1</span><span fontsize="" color="black" style="color: black">库存放</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">userId</span><span fontsize="" color="black" style="color: black">从1 ~ 1000w，第二个库1000~2000w，第三个库2000~3000w，以此类推。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466818951-450757bc-1448-42ce-b065-950134b060ce.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">每个库里再把用户表</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user</span><span fontsize="" color="black" style="color: black">拆分成</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user_1</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user_2</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_user_3</span><span fontsize="" color="black" style="color: black">等，对</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">userd</span><span fontsize="" color="black" style="color: black">进行取模路由到对应的表中。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">有效的避免数据分布不均匀的问题，数据库水平扩展也简单，直接添加实例无需迁移历史数据。</span></p>
<h3 style="" id="4%E3%80%81%E5%9C%B0%E7%90%86%E4%BD%8D%E7%BD%AE%E5%88%86%E7%89%87"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">4、地理位置分片</span></h3>
<p style=""><span fontsize="" color="black" style="color: black">地理位置分片其实是一个更大的范围，按城市或者地域划分，比如华东、华北数据放在不同的分片库、表。</span></p>
<h3 style="" id="5%E3%80%81%E9%A2%84%E5%AE%9A%E4%B9%89%E7%AE%97%E6%B3%95"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">5、预定义算法</span></h3>
<p style=""><span fontsize="" color="black" style="color: black">预定义算法是事先已经明确知道分库和分表的数量，可以直接将某类数据路由到指定库或表中，查询的时候亦是如此。</span></p>
<h2 style="" id="%E4%B8%83%E3%80%81%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E5%87%BA%E6%9D%A5%E7%9A%84%E9%97%AE%E9%A2%98">七、分库分表出来的问题</h2>
<p style=""><span fontsize="" color="black" style="color: black">了解了上边分库分表的拆分方式不难发现，相比于拆分前的单库单表，系统的数据存储架构演变到现在已经变得非常复杂。看几个具有代表性的问题，比如：</span></p>
<h3 style="" id="%E5%88%86%E9%A1%B5%E3%80%81%E6%8E%92%E5%BA%8F%E3%80%81%E8%B7%A8%E8%8A%82%E7%82%B9%E8%81%94%E5%90%88%E6%9F%A5%E8%AF%A2"><strong><span fontsize="" color="black" style="color: black">分页、排序、跨节点联合查询</span></strong></h3>
<p style=""><span fontsize="" color="black" style="color: black">分页、排序、联合查询，这些看似普通，开发中使用频率较高的操作，在分库分表后却是让人非常头疼的问题。把分散在不同库中表的数据查询出来，再将所有结果进行汇总合并整理后提供给用户。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">比如：我们要查询11、12月的订单数据，如果两个月的数据是分散到了不同的数据库实例，则要查询两个数据库相关的数据，在对数据合并排序、分页，过程繁琐复杂。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466819164-01aee886-40e2-4f68-b37f-2f6266b35a7e.png&amp;size=m" width="1080" style="display: inline-block"></p>
<h3 style="" id="%E4%BA%8B%E5%8A%A1%E4%B8%80%E8%87%B4%E6%80%A7"><strong><span fontsize="" color="black" style="color: black">事务一致性</span></strong></h3>
<p style=""><span fontsize="" color="black" style="color: black">分库分表后由于表分布在不同库中，不可避免会带来跨库事务问题。后续会分别以阿里的</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">Seata</span><span fontsize="" color="black" style="color: black">和MySQL的</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">XA</span><span fontsize="" color="black" style="color: black">协议实现分布式事务，用来比较各自的优势与不足。</span></p>
<h3 style="" id="%E5%85%A8%E5%B1%80%E5%94%AF%E4%B8%80%E7%9A%84%E4%B8%BB%E9%94%AE"><strong><span fontsize="" color="black" style="color: black">全局唯一的主键</span></strong></h3>
<p style=""><span fontsize="" color="black" style="color: black">分库分表后数据库表的主键ID业务意义就不大了，因为无法在标识唯一一条记录，例如：多张表</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order_1</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order_2</span><span fontsize="" color="black" style="color: black">的主键ID全部从1开始会重复，此时我们需要主动为一条记录分配一个ID，这个全局唯一的ID就叫</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">分布式ID</span><span fontsize="" color="black" style="color: black">，发放这个ID的系统通常被叫发号器。</span></p>
<h3 style="" id="%E5%A4%9A%E6%95%B0%E6%8D%AE%E5%BA%93%E9%AB%98%E6%95%88%E6%B2%BB%E7%90%86"><strong><span fontsize="" color="black" style="color: black">多数据库高效治理</span></strong></h3>
<p style=""><span fontsize="" color="black" style="color: black">对多个数据库以及库内大量分片表的高效治理，是非常有必要，因为像某宝这种大厂一次大促下来，订单表可能会被拆分成成千上万个</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">t_order_n</span><span fontsize="" color="black" style="color: black">表，如果没有高效的管理方案，手动建表、排查问题是一件很恐怖的事。</span></p>
<h3 style="" id="%E5%8E%86%E5%8F%B2%E6%95%B0%E6%8D%AE%E8%BF%81%E7%A7%BB"><strong><span fontsize="" color="black" style="color: black">历史数据迁移</span></strong></h3>
<p style=""><span fontsize="" color="black" style="color: black">分库分表架构落地以后，首要的问题就是如何平滑的迁移历史数据，增量数据和全量数据迁移，这又是一个比较麻烦的事情，后边详细讲。</span></p>
<h2 style="" id="%E5%85%AB%E3%80%81%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%BC%8F">八、分库分表架构模式</h2>
<p style=""><span fontsize="" color="black" style="color: black">分库分表架构主要有两种模式：</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">client</span><span fontsize="" color="black" style="color: black">客户端模式和</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">proxy</span><span fontsize="" color="black" style="color: black">代理模式</span></p>
<h3 style="" id="%E5%AE%A2%E6%88%B7%E6%A8%A1%E5%BC%8F"><strong><span fontsize="" color="black" style="color: black">客户模式</span></strong></h3>
<p style=""><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">client</span><span fontsize="" color="black" style="color: black">模式指分库分表的逻辑都在你的系统应用内部进行控制，应用会将拆分后的SQL直连多个数据库进行操作，然后本地进行数据的合并汇总等操作。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466819455-5e44017e-2c37-41c7-b579-52f8e81bde77.png&amp;size=m" width="1080" style="display: inline-block"></p>
<h3 style="" id="%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F"><strong><span fontsize="" color="black" style="color: black">代理模式</span></strong></h3>
<p style=""><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">proxy</span><span fontsize="" color="black" style="color: black">代理模式将应用程序与MySQL数据库隔离，业务方的应用不在需要直连数据库，而是连接proxy代理服务，代理服务实现了MySQL的协议，对业务方来说代理服务就是数据库，它会将SQL分发到具体的数据库进行执行，并返回结果。该服务内有分库分表的配置，根据配置自动创建分片表。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670466819438-619531b2-4ab0-4ad1-8ae6-815ccd0e8fe1.png&amp;size=m" width="1080" style="display: inline-block"></p>
<h3 style="" id="%E5%A6%82%E4%BD%95%E6%8A%89%E6%8B%A9"><strong><span fontsize="" color="black" style="color: black">如何抉择</span></strong></h3>
<p style=""><span fontsize="" color="black" style="color: black">如何选择</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">client</span><span fontsize="" color="black" style="color: black">模式和</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">proxy</span><span fontsize="" color="black" style="color: black">模式，我们可以从以下几个方面来简单做下比较。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">（1）性能</span></strong></p>
<p style=""><span fontsize="" color="black" style="color: black">性能方面</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">client</span><span fontsize="" color="black" style="color: black">模式表现的稍好一些，它是直接连接MySQL执行命令；</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">proxy</span><span fontsize="" color="black" style="color: black">代理服务则将整个执行链路延长了，应用-&gt;代理服务-&gt;MySQL，可能导致性能有一些损耗，但两者差距并不是非常大。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">（2）复杂度</span></strong></p>
<p style=""><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">client</span><span fontsize="" color="black" style="color: black">模式在开发使用通常引入一个jar可以；</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">proxy</span><span fontsize="" color="black" style="color: black">代理模式则需要搭建单独的服务，有一定的维护成本，既然是服务那么就要考虑高可用，毕竟应用的所有SQL都要通过它转发至MySQL。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">（3）升级</span></strong></p>
<p style=""><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">client</span><span fontsize="" color="black" style="color: black">模式分库分表一般是依赖基础架构团队的Jar包，一旦有版本升级或者Bug修改，所有应用到的项目都要跟着升级。小规模的团队服务少升级问题不大，如果是大公司服务规模大，且涉及到跨多部门，那么升级一次成本就比较高；</span></p>
<p style=""><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">proxy</span><span fontsize="" color="black" style="color: black">模式在升级方面优势很明显，发布新功能或者修复Bug，只要重新部署代理服务集群即可，业务方是无感知的，但要保证发布过程中服务的可用性。</span></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">（4）治理、监控</span></strong></p>
<p style=""><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">client</span><span fontsize="" color="black" style="color: black">模式由于是内嵌在应用内，应用集群部署不太方便统一处理；</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">proxy</span><span fontsize="" color="black" style="color: black">模式在对SQL限流、读写权限控制、监控、告警等服务治理方面更优雅一些。</span><br></p>]]></description><guid isPermaLink="false">/archives/mysqldan-biao-chao-guo-2000wjiu-yao-fen-ku-fen-biao</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Fri, 12 Jan 2024 13:15:08 GMT</pubDate></item><item><title><![CDATA[MyBatis动态 SQL 大全]]></title><link>https://xiaoming728.com/archives/mybatisdong-tai-sql-da-quan</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=MyBatis%E5%8A%A8%E6%80%81%20SQL%20%E5%A4%A7%E5%85%A8&amp;url=/archives/mybatisdong-tai-sql-da-quan" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：微信公众账号-架构师专栏</p>
<p style="">日期： 2022-05-31 09:01</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/ouk3n0pY2uK9mXKhIQBekw">https://mp.weixin.qq.com/s/ouk3n0pY2uK9mXKhIQBekw</a></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">Mybatis动态SQL语句大全，Mybatis中如何定义变量，Mybatis中如何提取公共的SQL片段</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">读完这篇文章里你能收获到</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">Mybatis动态SQL语句大全</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">Mybatis中如何定义变量</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">Mybatis中如何提取公共的SQL片段</span></p></li>
</ul>
<h2 style="" id="1.-if-%E8%AF%AD%E5%8F%A5"><span fontsize="" color="rgb(255, 169, 0)" style="color: rgb(255, 169, 0)">1. If 语句</span></h2>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">需求：根据作者名字和博客名字来查询博客！如果作者名字为空，那么只根据博客名字查询，反之，则</span><br><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">根据作者名来查询</span></p>
<pre><code>&lt;!--需求1：
根据作者名字和博客名字来查询博客！
如果作者名字为空，那么只根据博客名字查询，反之，则根据作者名来查询
select * from blog where title = #{title} and author = #{author}
--&gt;
&lt;select id="queryBlogIf" parameterType="map" resultType="blog"&gt;
    select * from blog where
    &lt;if test="title != null"&gt;
        title = #{title}
    &lt;/if&gt;
    &lt;if test="author != null"&gt;
        and author = #{author}
    &lt;/if&gt;
&lt;/select&gt;</code></pre>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">这样写我们可以看到，如果 author 等于 null，那么查询语句为 select </span><em><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)"> from user where title=#{title}, 但是如果title为空呢？那么查询语句为 select </span></em><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)"> from user where and author=#{author}，这是错误的 SQL 语句，如何解决呢？请看下面的 where 语句！</span></p>
<h2 style="" id="2.-where%E8%AF%AD%E5%8F%A5"><span fontsize="" color="rgb(255, 169, 0)" style="color: rgb(255, 169, 0)">2. Where语句</span></h2>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">修改上面的SQL语句：</span></p>
<pre><code>&lt;select id="queryBlogIf" parameterType="map" resultType="blog"&gt;
    select * from blog
    &lt;where&gt;
        &lt;if test="title != null"&gt;
            title = #{title}
        &lt;/if&gt;
        &lt;if test="author != null"&gt;
            and author = #{author}
        &lt;/if&gt;
    &lt;/where&gt;
&lt;/select&gt;</code></pre>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且，若子句的开头为 “AND” 或 “OR”，where 元素也会将它们去除。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">如果 where 元素与你期望的不太一样，你也可以通过自定义 trim 元素来定制 where 元素的功能。</span></p>
<h3 style="" id="2.1-%E5%92%8C-where-%E5%85%83%E7%B4%A0%E7%AD%89%E4%BB%B7%E7%9A%84%E8%87%AA%E5%AE%9A%E4%B9%89-trim-%E5%85%83%E7%B4%A0"><span fontsize="" color="rgb(255, 169, 0)" style="color: rgb(255, 169, 0)">2.1 和 where 元素等价的自定义 trim 元素</span></h3>
<pre><code>&lt;trim prefix="WHERE" prefixOverrides="AND |OR "&gt;
  ...
&lt;/trim&gt;</code></pre>
<h2 style="" id="3.-set%E8%AF%AD%E5%8F%A5"><span fontsize="" color="rgb(255, 169, 0)" style="color: rgb(255, 169, 0)">3. Set语句</span></h2>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">同理，上面的对于查询 SQL 语句包含 where 关键字，如果在进行更新操作的时候，含有 set 关键词，</span><br><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">我们怎么处理呢？</span></p>
<pre><code>&lt;!--注意set是用的逗号隔开--&gt;
&lt;update id="updateBlog" parameterType="map"&gt;
    update blog
    &lt;set&gt;
        &lt;if test="title != null"&gt;
            title = #{title},
        &lt;/if&gt;
        &lt;if test="author != null"&gt;
            author = #{author}
        &lt;/if&gt;
    &lt;/set&gt;
    where id = #{id};
&lt;/update&gt;</code></pre>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">这个例子中，set 元素会动态地在行首插入 SET 关键字，并会删掉额外的逗号（这些逗号是在使用条件语句给列赋值时引入的）</span></p>
<h3 style="" id="3.1-%E4%B8%8E-set-%E5%85%83%E7%B4%A0%E7%AD%89%E4%BB%B7%E7%9A%84%E8%87%AA%E5%AE%9A%E4%B9%89-trim-%E5%85%83%E7%B4%A0"><span fontsize="" color="rgb(255, 169, 0)" style="color: rgb(255, 169, 0)">3.1 与 set 元素等价的自定义 trim 元素</span></h3>
<pre><code>&lt;trim prefix="SET" suffixOverrides=","&gt;
  ...
&lt;/trim&gt;</code></pre>
<h2 style="" id="4.-choose%E8%AF%AD%E5%8F%A5"><span fontsize="" color="rgb(255, 169, 0)" style="color: rgb(255, 169, 0)">4. Choose语句</span></h2>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">有时候，我们不想用到所有的查询条件，只想选择其中的一个，查询条件有一个满足即可，使用 choose</span><br><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">标签可以解决此类问题，类似于 Java 的 switch 语句</span></p>
<pre><code>&lt;select id="queryBlogChoose" parameterType="map" resultType="blog"&gt;
    select * from blog
    &lt;where&gt;
        &lt;choose&gt;
            &lt;when test="title != null"&gt;
                title = #{title}
            &lt;/when&gt;
            &lt;when test="author != null"&gt;
                and author = #{author}
            &lt;/when&gt;
            &lt;otherwise&gt;
                and views = #{views}
            &lt;/otherwise&gt;
        &lt;/choose&gt;
    &lt;/where&gt;
&lt;/select&gt;</code></pre>
<h2 style="" id="5.-foreach%E8%AF%AD%E5%8F%A5"><span fontsize="" color="rgb(255, 169, 0)" style="color: rgb(255, 169, 0)">5. Foreach语句</span></h2>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">将数据库中前三个数据的id修改为1,2,3；</span><br><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">需求：我们需要查询 blog 表中 id 分别为1,2,3的博客信息</span></p>
<pre><code>&lt;select id="queryBlogForeach" parameterType="map" resultType="blog"&gt;
    select * from blog
    &lt;where&gt;
        &lt;!--
        collection:指定输入对象中的集合属性
        item:每次遍历生成的对象
        open:开始遍历时的拼接字符串
        close:结束时拼接的字符串
        separator:遍历对象之间需要拼接的字符串
        select * from blog where 1=1 and (id=1 or id=2 or id=3)
        --&gt;
        &lt;foreach collection="ids" item="id" open="and (" close=")"
        separator="or"&gt;
            id=#{id}
        &lt;/foreach&gt;
    &lt;/where&gt;
&lt;/select&gt;</code></pre>
<h2 style="" id="6.-sql%E7%89%87%E6%AE%B5"><span fontsize="" color="rgb(255, 169, 0)" style="color: rgb(255, 169, 0)">6. SQL片段</span></h2>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">有时候可能某个 sql 语句我们用的特别多，为了增加代码的重用性，简化代码，我们需要将这些代码抽<br>
  取出来，然后使用时直接调用。<br>
  提取SQL片段：</span></p>
<pre><code>&lt;sql id="if-title-author"&gt;
    &lt;if test="title != null"&gt;
        title = #{title}
    &lt;/if&gt;
    &lt;if test="author != null"&gt;
        and author = #{author}
    &lt;/if&gt;
&lt;/sql&gt;</code></pre>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">引用SQL片段：</span></p>
<pre><code>&lt;select id="queryBlogIf" parameterType="map" resultType="blog"&gt;
    select * from blog
    &lt;where&gt;
        &lt;!-- 引用 sql 片段，如果refid 指定的不在本文件中，那么需要在前面加上 namespace--&gt;
        &lt;include refid="if-title-author"&gt;&lt;/include&gt;
        &lt;!-- 在这里还可以引用其他的 sql 片段 --&gt;
    &lt;/where&gt;
&lt;/select&gt;</code></pre>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">注意：</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">1、最好基于 单表来定义 sql 片段，提高片段的可重用性<br>
  2、在 sql 片段中不要包括 where</span></p>
<h2 style="" id="7.-bind%E5%85%83%E7%B4%A0"><span fontsize="" color="rgb(255, 169, 0)" style="color: rgb(255, 169, 0)">7. Bind元素</span></h2>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">bind 元素允许你在 OGNL 表达式以外创建一个变量，并将其绑定到当前的上下文。比如：</span></p>
<pre><code>&lt;select id="selectBlogsLike" resultType="Blog"&gt;
  &lt;bind name="pattern" value="'%' + _parameter.getTitle() + '%'" /&gt;
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
&lt;/select&gt;</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/mybatisdong-tai-sql-da-quan</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmybatis.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Fri, 12 Jan 2024 13:15:00 GMT</pubDate></item><item><title><![CDATA[MySQL单表行数为什么不要超过2000w]]></title><link>https://xiaoming728.com/archives/mysqldan-biao-xing-shu-wei-shi-me-bu-yao-chao-guo-2000w</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=MySQL%E5%8D%95%E8%A1%A8%E8%A1%8C%E6%95%B0%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%A6%81%E8%B6%85%E8%BF%872000w&amp;url=/archives/mysqldan-biao-xing-shu-wei-shi-me-bu-yao-chao-guo-2000w" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：开源博客-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://my.oschina.net/u/4090830">京东云开发者</a></p>
<p style="">日期： 2022/07/27 17:24</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://my.oschina.net/u/4090830">https://my.oschina.net/u/4090830/blog/5559454</a></p>
<h2 style="" id="%E4%B8%80%E3%80%81%E8%83%8C%E6%99%AF">一、背景</h2>
<p style="">作为在后端圈开车的多年老司机，是不是经常听到过，“mysql 单表最好不要超过 2000w”,“单表超过 2000w 就要考虑数据迁移了”，“你这个表数据都马上要到 2000w 了，难怪查询速度慢”</p>
<p style="">这些名言民语就和 “群里只讨论技术，不开车，开车速度不要超过 120 码，否则自动踢群”，只听过，没试过，哈哈。</p>
<p style="">下面我们就把车速踩到底，干到 180 码试试…….</p>
<h2 style="" id="%E4%BA%8C%E3%80%81%E5%AE%9E%E9%AA%8C">二、实验</h2>
<h3 style="" id="1%E3%80%81%E6%B7%BB%E5%8A%A0%E6%95%B0%E6%8D%AE">1、添加数据</h3>
<p style="">建一张表</p>
<pre><code>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 '人员信息表';</code></pre>
<p style="">插入一条数据</p>
<pre><code>insert into person values(1,1,'user_1', NOW(), now());</code></pre>
<p style="">利用 mysql 伪列 rownum 设置伪列起始点为 1</p>
<pre><code>select (@i:=@i+1) as rownum, person_name from person, (select @i:=100) as init;
set @i=1;</code></pre>
<p style="">运行下面的 sql，连续执行 20 次，就是 2 的 20 次方约等于 100w 的数据；执行 23 次就是 2 的 23 次方约等于 800w , 如此下去即可实现千万测试数据的插入，如果不想翻倍翻倍的增加数据，而是想少量，少量的增加，有个技巧，就是在 SQL 的后面增加 where 条件，如 id &gt; 某一个值去控制增加的数据量即可。</p>
<pre><code>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;</code></pre>
<p style="">此处需要注意的是，也许你在执行到近 800w 或者 1000w 数据的时候，会报错：The total number of locks exceeds the lock table size，这是由于你的临时表内存设置的不够大，只需要扩大一下设置参数即可。</p>
<pre><code>SET GLOBAL tmp_table_size =512*1024*1024; （512M）
SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);</code></pre>
<hr>
<h3 style="" id="2%E3%80%81%E6%B5%8B%E8%AF%95%E7%9C%8B%E7%9C%8B">2、测试看看</h3>
<p style="">先来看一组测试数据，这组数据是在 mysql8.0 的版本，并且是在我本机上，由于本机还跑着 idea , 浏览器等各种工具，所以并不是机器配置就是用于数据库配置，所以测试数据只限于参考。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660186767837-962efa82-94f5-4988-a2e8-17575020609a.png&amp;size=m" width="1080" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660186774040-c3b70e64-f3f9-4a05-a742-450f2e1ff2b2.png&amp;size=m" width="951" style="display: inline-block"></p>
<p style="">看到这组数据似乎好像真的和标题对应，当数据达到 2000w 以后，查询时长急剧上升；难道这就是铁律吗？</p>
<h2 style="" id="%E4%B8%89%E3%80%81%E5%8D%95%E8%A1%A8%E6%95%B0%E9%87%8F%E9%99%90%E5%88%B6">三、单表数量限制</h2>
<p style="">首先我们先想想数据库单表行数最大多大？</p>
<pre><code>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 '人员信息表';</code></pre>
<p style="">看看上面的建表 sql，id 是主键，本身就是唯一的，也就是说主键的大小可以限制表的上限，如果主键声明 int 大小，也就是 32 位，那么支持 2^32-1 ~~21 亿；如果是 bigint，那就是 2^62-1 ？（36893488147419103232），难以想象这个的多大了，一般还没有到这个限制之前，可能数据库已经爆满了！！</p>
<p style="">有人统计过，如果建表的时候，自增字段选择无符号的 bigint , 那么自增长最大值是 18446744073709551615，按照一秒新增一条记录的速度，大约什么时候能用完？</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660186868168-c2cd5492-0a13-4adc-a06f-13f6fa0626b6.png&amp;size=m" width="649" style="display: inline-block"></p>
<h2 style="" id="%E5%9B%9B%E3%80%81%E8%A1%A8%E7%A9%BA%E9%97%B4">四、表空间</h2>
<p style="">下面我们再来看看索引的结构，对了，我们下面讲内容都是基于 Innodb 引擎的，大家都知道 Innodb 的索引内部用的是 B+ 树<img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660186903715-0a86ec09-c78c-42ee-9972-e6edcb7bdbe7.png&amp;size=m" width="833" style="display: inline-block"></p>
<p style="">这张表数据，在硬盘上存储也是类似如此的，它实际是放在一个叫 person.ibd （innodb data）的文件中，也叫做表空间；虽然数据表中，他们看起来是一条连着一条，但是实际上在文件中它被分成很多小份的数据页，而且每一份都是 16K。</p>
<p style="">大概就像下面这样，当然这只是我们抽象出来的，在表空间中还有段、区、组等很多概念，但是我们需要跳出来看。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660186922508-8f1f6f51-4a79-4bd2-b788-9ba27149d09e.png&amp;size=m" width="1017" style="display: inline-block"></p>
<h2 style="" id="%E4%BA%94%E3%80%81%E9%A1%B5%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84">五、页的数据结构</h2>
<p style="">因为每个页只有 16K 的大小，但是如果数据很多，那一页肯定就放不下这些数据，那数据肯定就会被分到其他的页中，所以为了把这些页关联起来，肯定就会有记录前后页地址，方便找到对应页；同时每页都是唯一的，那就会需要有一个唯一标志来标记页，就是页号；</p>
<p style="">页中会记录数据所以会存在读写操作，读写操作会存在中断或者其他异常导致数据不全等，那就会需要有校验机制，所以里面还有会校验码，而读操作最重要的就是效率问题，如果按照记录一个个进行遍历，那肯定是很费劲的，所以这里面还会为数据生成对应的页目录（Page Directory）; 所以实际页的内部结构像是下面这样的。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660186944681-07a02c51-6cba-4734-bbfc-06db9735d171.png&amp;size=m" width="947" style="display: inline-block"></p>
<p style="">从图中可以看出，一个 InnoDB 数据页的存储空间大致被划分成了 7 个部分，有的部分占用的字节数是确定的，有的部分占用的字节数是不确定的。</p>
<p style="">在页的 7 个组成部分中，我们自己存储的记录会按照我们指定的行格式存储到 User Records 部分。</p>
<p style="">但是在一开始生成页的时候，其实并没有 User Records 这个部分，每当我们插入一条记录，都会从 Free Space 部分，也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分，当 Free Space 部分的空间全部被 User Records 部分替代掉之后，也就意味着这个页使用完了，如果还有新的记录插入的话，就需要去申请新的页了。这个过程的图示如下。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660186986486-4cd37434-50f1-44ec-9a39-51613ee26a3e.png&amp;size=m" width="1023" style="display: inline-block"></p>
<p style="">刚刚上面说到了数据的新增的过程。</p>
<p style="">那下面就来说说，数据的查找过程，假如我们需要查找一条记录，我们可以把表空间中的每一页都加载到内存中，然后对记录挨个判断是不是我们想要的，在数据量小的时候，没啥问题，内存也可以撑；但是现实就是这么残酷，不会给你这个局面；为了解决这问题，mysql 中就有了索引的概念；大家都知道索引能够加快数据的查询，那到底是怎么个回事呢？下面我就来看看。</p>
<h2 style="" id="%E5%85%AD%E3%80%81%E7%B4%A2%E5%BC%95%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84">六、索引的数据结构</h2>
<p style="">在 mysql 中索引的数据结构和刚刚描述的页几乎是一模一样的，而且大小也是 16K, 但是在索引页中记录的是页 (数据页，索引页) 的最小主键 id 和页号，以及在索引页中增加了层级的信息，从 0 开始往上算，所以页与页之间就有了上下层级的概念。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660187030618-ab26a704-38fe-4a84-8b88-211c5e00a65d.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">看到这个图之后，是不是有点似曾相似的感觉，是不是像一棵二叉树啊，对，没错！它就是一棵树，只不过我们在这里只是简单画了三个节点，2 层结构的而已，如果数据多了，可能就会扩展到 3 层的树，这个就是我们常说的 B+ 树，最下面那一层的 page level =0, 也就是叶子节点，其余都是非叶子节点。</p>
<p style="text-align: center; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660187039025-c256343d-572c-478f-b227-39e2daa64bc9.png&amp;size=m" width="405" style="display: inline-block"></p>
<p style="">看上图中，我们是单拿一个节点来看，首先它是一个非叶子节点（索引页），在它的内容区中有 id 和 页号地址两部分，这个 id 是对应页中记录的最小记录 id 值，页号地址是指向对应页的指针；而数据页与此几乎大同小异，区别在于数据页记录的是真实的行数据而不是页地址，而且 id 的也是顺序的。</p>
<h2 style="" id="%E4%B8%83%E3%80%81%E5%8D%95%E8%A1%A8%E5%BB%BA%E8%AE%AE%E5%80%BC">七、单表建议值</h2>
<p style=""><strong>下面我们就以 3 层，2 分叉（实际中是 M 分叉）的图例来说明一下查找一个行数据的过程。</strong></p>
<p style=""><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">比如说我们需要查找一个 id=6 的行数据，因为在非叶子节点中存放的是页号和该页最小的 id，所以我们从顶层开始对比，首先看页号 10 中的目录，有 [id=1, 页号 = 20],[id=5, 页号 = 30], 说明左侧节点最小 id 为 1，右侧节点最小 id 是 5；6&gt;5, 那按照二分法查找的规则，肯定就往右侧节点继续查找，找到页号 30 的节点后，发现这个节点还有子节点（非叶子节点），那就继续比对，同理，6&gt;5&amp;&amp;6&lt;7, 所以找到了页号 60，找到页号 60 之后，发现此节点为叶子节点（数据节点），于是将此页数据加载至内存进行一一对比，结果找到了 id=6 的数据行。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">从上述的过程中发现，我们为了查找 id=6 的数据，总共查询了三个页，如果三个页都在磁盘中（未提前加载至内存），那么最多需要经历三次的磁盘 IO。</span><br><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">需要注意的是，图中的页号只是个示例，实际情况下并不是连续的，在磁盘中存储也不一定是顺序的。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660187090008-dbe763af-57de-4e9c-83f4-5273666d3fd1.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">至此，我们大概已经了解了表的数据是怎么个结构了，也大概知道查询数据是个怎么的过程了，这样我们也就能大概估算这样的结构能存放多少数据了。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">从上面的图解我们知道 B+ 数的叶子节点才是存在数据的，而非叶子节点是用来存放索引数据的。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">所以，同样一个 16K 的页，非叶子节点里的每条数据都指向新的页，而新的页有</span><strong><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">两种可能</span></strong></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">如果是叶子节点，那么里面就是一行行的数据</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">如果是非叶子节点的话，那么就会继续指向新的页</span></p></li>
</ul>
<p style=""><strong><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">假设</span></strong></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">非叶子节点内指向其他页的数量为 x</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">叶子节点内能容纳的数据行数为 y</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">B+ 数的层数为 z</span></p></li>
</ul>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">如下图中所示<br>
  Total =x^(z-1) *y 也就是说总数会等于 x 的 z-1 次方 与 Y 的乘积。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1660187116539-cafbd771-d8c8-4b9f-a91d-705b2d4826de.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">X =？</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">在文章的开头已经介绍了页的结构，索引也也不例外，都会有 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 行。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">Y=？</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">叶子节点和非叶子节点的结构是一样的，同理，能放数据的空间也是 15k；但是叶子节点中存放的是真正的行数据，这个影响的因素就会多很多，比如，字段的类型，字段的数量；每行数据占用空间越大，页中所放的行数量就会越少；这边我们暂时按一条行数据 1k 来算，那一页就能存下 15 条，Y≈15。</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">算到这边了，是不是心里已经有谱了啊<br>
  根据上述的公式，Total =x^(z-1) y，已知 x=1280,y=15<br>
  假设 B+ 树是两层，那就是 Z =2， Total = （1280 ^1 ）15 = 19200<br>
  假设 B+ 树是三层，那就是 Z =3， Total = （1280 ^2） *15 = 24576000 （约 2.45kw）</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">哎呀，妈呀！这不是正好就是文章开头说的最大行数建议值 2000w 嘛！对的，一般 B+ 数的层级最多也就是 3 层，你试想一下，如果是 4 层，除了查询的时候磁盘 IO 次数会增加，而且这个 Total 值会是多少，大概应该是 3 百多亿吧，也不太合理，所以，3 层应该是比较合理的一个值。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">到这里难道就完了？</span></p>
<p style=""></p>
<p style=""><strong><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">不</span></strong><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)"><br>
  我们刚刚在说 Y 的值时候假设的是 1K ，那比如我实际当行的数据占用空间不是 1K , 而是 5K, 那么单个数据页最多只能放下 3 条数据<br>
  同样，还是按照 Z=3 的值来计算，那 Total = （1280 ^2） *3 = 4915200 （近 500w）</span></p>
<p style=""><strong><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">所以</span></strong><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">，在保持相同的层级（相似查询性能）的情况下，在行数据大小不同的情况下，其实这个最大建议值也是不同的，而且影响查询性能的还有很多其他因素，比如，数据库版本，服务器配置，sql 的编写等等，MySQL 为了提高性能，会将表的索引装载到内存中。在 InnoDB buffer size 足够的情况下，其能完成全加载进内存，查询不会有问题。但是，当单表数据库到达某个量级的上限时，导致内存无法存储其索引，使得之后的 SQL 查询会产生磁盘 IO，从而导致性能下降，所以增加硬件配置（比如把内存当磁盘使），可能会带来立竿见影的性能提升哈。</span></p>
<h2 style="" id="%E5%85%AB%E3%80%81%E6%80%BB%E7%BB%93"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">八、总结</span></h2>
<p style="">1. Mysql 的表数据是以页的形式存放的，页在磁盘中不一定是连续的。</p>
<p style="">2. 页的空间是 16K, 并不是所有的空间都是用来存放数据的，会有一些固定的信息，如，页头，页尾，页码，校验码等等。</p>
<p style="">3. 在 B+ 树中，叶子节点和非叶子节点的数据结构是一样的，区别在于，叶子节点存放的是实际的行数据，而非叶子节点存放的是主键和页号。</p>
<p style="">4. 索引结构不会影响单表最大行数，2kw 也只是推荐值，超过了这个值可能会导致 B + 树层级更高，影响查询性能。</p>
<h2 style="" id="%E4%B9%9D%E3%80%81%E5%8F%82%E8%80%83">九、参考</h2>
<ul>
 <li>
  <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://my.oschina.net/u/4090830">https://www.jianshu.com/p/cf5d381ef637</a></p></li>
 <li>
  <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://my.oschina.net/u/4090830">https://www.modb.pro/db/139052</a></p></li>
 <li>
  <p style="">《MYSQL 内核：INNODB 存储引擎 卷 1》</p></li>
</ul>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/mysqldan-biao-xing-shu-wei-shi-me-bu-yao-chao-guo-2000w</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Fri, 12 Jan 2024 13:15:00 GMT</pubDate></item><item><title><![CDATA[程序员必备的画图工具]]></title><link>https://xiaoming728.com/archives/cheng-xu-yuan-bi-bei-de-hua-tu-gong-ju</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%E7%9A%84%E7%94%BB%E5%9B%BE%E5%B7%A5%E5%85%B7&amp;url=/archives/cheng-xu-yuan-bi-bei-de-hua-tu-gong-ju" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span fontsize="" color="black" style="color: black">最近经常有小伙伴问文章中的图是用什么软件画的，下面就来梳理一下常用的画图工具：</span></p>
<ul>
 <li>
  <p style=""><strong><span fontsize="" color="black" style="color: black">常规画图：</span></strong><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">Excalidraw、</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/"><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">draw.io</span></a><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">、语雀</span></p></li>
 <li>
  <p style=""><strong><span fontsize="" color="black" style="color: black">思维导图：</span></strong><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">Xmind</span></p></li>
 <li>
  <p style=""><strong><span fontsize="" color="black" style="color: black">代码截图：</span></strong><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">Carbon</span></p></li>
 <li>
  <p style=""><strong><span fontsize="" color="black" style="color: black">画图模板：</span></strong><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">ProcessOn</span></p></li>
</ul>
<h2 style="" id="%E4%B8%80%E3%80%81excalidraw">一、Excalidraw</h2>
<p style=""><span fontsize="" color="black" style="color: black">Excalidraw 是一款轻量、开源的手绘风格电子白板和画图应用，可以快速画出漂亮的流程图、UML图甚至是图表。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483021034-6858a398-4b34-444d-ab64-2399eaf6d8de.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">Excalidraw 提供了丰富的素材库，包含手绘风格的图标、图标等：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483021073-3aa0b5e7-f39a-45d6-8ede-5972ea1a188e.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">可以在模版市场来搜索需要的组件：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483021001-8fd59aa9-f22a-4f63-aa52-9dd081678fe4.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">除此之外，Excalidraw 还有很多特点：</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">界面简洁，模版众多，支持键盘快捷键，可以快速上手；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">免注册，支持切换为中文语言；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">内容安全，采用端到端加密，不会将内容上传到云端；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持导出图片、导出源文件、分享链接、实时协作、将内容保存到工作区。</span></p></li>
</ul>
<p style=""><strong><span fontsize="" color="black" style="color: black">在线地址：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/">https://excalidraw.com/</a></p>
<h2 style="" id="%E4%BA%8C%E3%80%81draw.io">二、<a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/">Draw.io</a></h2>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/"><span fontsize="" color="black" style="color: black">draw.io</span></a><span fontsize="" color="black" style="color: black"> 是一款免费的在线图表编辑工具，可以绘制流程图、UML、类图、组织结构图、泳道图、E-R图、思维导图等，堪称画图神器！</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483020964-62d99079-af0a-46a6-9de1-24b414045ef5.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/"><span fontsize="" color="black" style="color: black">draw.io</span></a><span fontsize="" color="black" style="color: black"> 提供了丰富的模版，支持商务、图表、cloud、工程、流程图、布局、地图、网络、软件、表格、UML、Venn等。还可以自定义模板。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483021086-1af0cf88-a9dc-4a52-ab16-cc7399d6c58b.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">除此之外，</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/"><span fontsize="" color="black" style="color: black">draw.io</span></a><span fontsize="" color="black" style="color: black"> 还有很多特点：</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持语言设置为中文；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持使用图层；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持设置手绘风格、自带多种图标，支持自定义图标；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持将图片导出为PNG、JEPG、SVG、PDF、HTML、XML等，支持导出高级设置，如调整DPI、宽高、缩放、背景等。</span></p></li>
</ul>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/"><span fontsize="" color="black" style="color: black">draw.io</span></a><span fontsize="" color="black" style="color: black"> 使用：</span></p>
<ul>
 <li>
  <p style=""><strong><span fontsize="" color="black" style="color: black">在线地址：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/">https://app.diagrams.net/</a></p></li>
 <li>
  <p style=""><strong><span fontsize="" color="black" style="color: black">下载地址：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/">https://github.com/jgraph/drawio/releases</a></p></li>
</ul>
<h2 style="" id="%E4%B8%89%E3%80%81%E8%AF%AD%E9%9B%80">三、语雀</h2>
<p style=""><span fontsize="" color="black" style="color: black">语雀是一款面向未来的文档和知识协同工具，可以用来记笔记，也可以用来画图。语雀画板支持基础图形、流程图、UML图、ER图、思维导图等。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483021900-0a25cdaf-5a80-46d0-b77c-f8e5da8bdba2.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">语雀画板还支持直接从 icofont 搜索、添加图标：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483021924-a788c943-2ae3-4b42-94c2-e603f189ff4a.png&amp;size=m" width="604" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">其还提供了一些常用图的模版：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483022579-b3141752-df4c-4bed-a7d4-196f42553c31.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">除此之外，语雀画板还有以下特点：</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持团队协作绘图；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">保存版本，恢复历史版本。</span></p></li>
</ul>
<p style=""><span fontsize="" color="black" style="color: black">语雀使用：</span></p>
<ul>
 <li>
  <p style=""><strong><span fontsize="" color="black" style="color: black">常用模板：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/">https://www.yuque.com/danchun-eekrs/pbxiht</a></p></li>
 <li>
  <p style=""><strong><span fontsize="" color="black" style="color: black">在线地址：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/">https://www.yuque.com/</a></p></li>
</ul>
<h2 style="" id="%E5%9B%9B%E3%80%81xmind">四、Xmind</h2>
<p style=""><span fontsize="" color="black" style="color: black">XMind 是一个跨平台的思维导图软件，具有多种结构样式，除了普通的思维导图，还包括树形图、逻辑图、鱼骨图、时间轴、树状表格等等，不同的结构样式可以自由组合混用，同时支持一键更换结构样式。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483022479-865264f8-a84a-4979-ac93-e87bf53e2b07.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">Xmind 提供了很多类型的思维导图，并支持设置多种主题风格：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483022483-2ae0a0b4-940d-4624-bbda-804acb8c52bb.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">除此之外，Xmind 还有很多特点：</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">可以灵活的定制节点外观、插入图标外，还有多种样式、主题色彩、贴图可以选择，主题颜色和样式同样支持自由组合；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">除了 XMind 自身的格式，还支持导入 FreeMind、MindManager 等软件的文件格式，支持导出PNG、SVG、PDF、Markdown、Word、Excel 等；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持进行动态演示。</span></p></li>
</ul>
<p style=""><strong><span fontsize="" color="black" style="color: black">下载地址：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/">https://xmind.app/download/</a></p>
<h2 style="" id="%E4%BA%94%E3%80%81carbon">五、Carbon</h2>
<p style=""><span fontsize="" color="black" style="color: black">Carbon是开源免费的代码图片生成器，可以快速生成漂亮的代码图片。操作简单，直接将代码粘贴在代码区，设置想要的格式即可。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483022727-5514eebc-cb78-43b2-b5d8-83f9c8b24966.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">生成的代码图片非常适合用来分享：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483022926-f3a1673e-6b54-4e8a-97d9-94e6b8eb37d8.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">除此之外，Carbbon 还有很多特点：</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持多种代码风格、支持自定义代码风格，支持主流的各种编程语言；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持设置代码的字体、行号、行数等文本格式；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">支持设置图片的边距、背景、颜色、阴影、大小等。</span></p></li>
</ul>
<p style=""><strong><span fontsize="" color="black" style="color: black">在线地址：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/">https://carbon.now.sh/</a></p>
<h2 style="" id="%E5%85%AD%E3%80%81processon">六、ProcessOn</h2>
<p style=""><span fontsize="" color="black" style="color: black">ProcessOn 是一个在线协作绘图平台，支持在线制作思维导图、流程图、组织结构图、网络拓扑图、鱼骨图、UML图等。不过其免费版只支持添加 9 张图。所以这里主要推荐其丰富的模版市场，可以通过分类、关键字搜索来查找合适的模版：：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483023750-c564dfe1-0f8c-4f2f-bd5f-13153ef41b79.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">这些模版可以为我们提供一些绘图思路：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670483023896-53756aa8-7102-442b-8a2a-c8048b6411b4.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><strong><span fontsize="" color="black" style="color: black">在线地址：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://excalidraw.com/">https://www.processon.com/diagrams/new#template</a></p>]]></description><guid isPermaLink="false">/archives/cheng-xu-yuan-bi-bei-de-hua-tu-gong-ju</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E7%2594%25BB%25E5%259B%25BE.png&amp;size=m" type="image/jpeg" length="0"/><category>开源工具</category><pubDate>Fri, 12 Jan 2024 13:14:00 GMT</pubDate></item><item><title><![CDATA[ElasticSearch可视化工具]]></title><link>https://xiaoming728.com/archives/elasticsearchke-shi-hua-gong-ju</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=ElasticSearch%E5%8F%AF%E8%A7%86%E5%8C%96%E5%B7%A5%E5%85%B7&amp;url=/archives/elasticsearchke-shi-hua-gong-ju" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E4%B8%80%E3%80%81%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E7%AE%80%E4%BB%8B">一、开源项目简介</h3>
<p style="text-align: center; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1695697112838-fc9c3b86-847e-4b8e-86c9-7ebb2c0f775d.png&amp;size=m" width="200" style="display: inline-block"></p>
<p style=""></p>
<p style=""><strong>es-client：elasticsearch查询客户端。</strong></p>
<p style="">elasticsearch的客户端比较出名的就是elasticsearch head 和Kibana了， 但是elasticsearch head已经停止更新，且样式老旧，功能不全；而Kibana虽功能全面，但是启动麻烦，大部分功能用不上，很不灵活，所以采用vite2+vue3+ts+arco-design进行开发了一个elasticsearch的客户端。</p>
<p style="">注意：2.8.3版本之后，开发重心将会以utools插件为主，不再以浏览器插件为主</p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E5%BC%80%E6%BA%90%E5%8D%8F%E8%AE%AE">二、开源协议</h3>
<p style="">使用Apache-2.0开源协议</p>
<h3 style="" id="%E4%B8%89%E3%80%81%E7%95%8C%E9%9D%A2%E5%B1%95%E7%A4%BA">三、界面展示</h3>
<h4 style="" id="%E9%A1%B9%E7%9B%AE%E9%A2%84%E8%A7%88">项目预览</h4>
<ul>
 <li>
  <p style="">首页</p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1695697113105-e82290a1-e7d1-4f1c-8938-8fb1a791d1ce.png&amp;size=m" width="1080" style="display: inline-block"></p>
<ul>
 <li>
  <p style="">数据浏览</p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1695697113036-9a1b6f21-b611-4f16-a415-4c946e3d86bd.png&amp;size=m" width="1080" style="display: inline-block"></p>
<ul>
 <li>
  <p style="">基础查询</p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1695697113161-e09b87b8-2298-4d41-8118-57d5ffa22732.png&amp;size=m" width="1080" style="display: inline-block"></p>
<ul>
 <li>
  <p style="">高级查询</p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1695697113192-8b31a1df-6087-49d9-b5aa-7a76d935eb91.png&amp;size=m" width="1080" style="display: inline-block"></p>
<ul>
 <li>
  <p style="">设置</p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1695697113200-cc159f8a-6b5a-4922-a37c-6d400a885be6.png&amp;size=m" width="1080" style="display: inline-block"></p>
<ul>
 <li>
  <p style="">关于</p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1695697113342-0fc33e86-3938-4254-b501-8f437bd629bb.png&amp;size=m" width="1080" style="display: inline-block"></p>
<h3 style="" id="%E5%9B%9B%E3%80%81%E5%8A%9F%E8%83%BD%E6%A6%82%E8%BF%B0">四、功能概述</h3>
<p style="">ES查询客户端，elasticsearch可视化工具。</p>
<h4 style="" id="%E5%8A%9F%E8%83%BD%E7%89%B9%E6%80%A7">功能特性</h4>
<ul>
 <li>
  <p style="">页面美观</p></li>
</ul>
<ul>
 <li>
  <p style="">使用arco-design组件库，布局更加美观合理</p></li>
</ul>
<ul>
 <li>
  <p style="">可视化查询</p></li>
</ul>
<ul>
 <li>
  <p style="">可以轻松构建查询条件，无需了解_search语法即可查询</p></li>
</ul>
<ul>
 <li>
  <p style="">问题反馈</p></li>
</ul>
<ul>
 <li>
  <p style="">使用兔小巢构建的反馈社区，倾听您的建议</p></li>
</ul>
<ul>
 <li>
  <p style="">索引管理</p></li>
</ul>
<ul>
 <li>
  <p style="">快速查看索引信息，进行简单的索引操作</p></li>
</ul>
<ul>
 <li>
  <p style="">数据导出</p></li>
</ul>
<ul>
 <li>
  <p style="">支持多种格式（JSON,CSV,TXT...）数据导出</p></li>
</ul>
<ul>
 <li>
  <p style="">深色模式</p></li>
</ul>
<ul>
 <li>
  <p style="">可以自由切换浅色模式与深色模式</p></li>
</ul>
<ul>
 <li>
  <p style="">JSON视图</p></li>
</ul>
<ul>
 <li>
  <p style="">通过Highlight.js实现JSON美化</p></li>
</ul>
<ul>
 <li>
  <p style="">表格视图</p></li>
</ul>
<ul>
 <li>
  <p style="">支持将查询的数据以表格形式展示</p></li>
</ul>
<ul>
 <li>
  <p style="">支持认证</p></li>
</ul>
<ul>
 <li>
  <p style="">可以通过Basic认证访问到elasticsearch</p></li>
</ul>
<ul>
 <li>
  <p style="">复制按钮</p></li>
</ul>
<ul>
 <li>
  <p style="">一键复制查询的结果</p></li>
</ul>
<ul>
 <li>
  <p style="">http客户端支持</p></li>
</ul>
<ul>
 <li>
  <p style="">拥有和kibana一样的查询方式</p></li>
</ul>
<ul>
 <li>
  <p style="">低分辨率适应</p></li>
</ul>
<ul>
 <li>
  <p style="">布局合理，在低分辨率情况下也能正常展示</p></li>
</ul>
<h4 style="" id="%E9%A1%B9%E7%9B%AE%E5%8A%9F%E8%83%BD">项目功能</h4>
<ul>
 <li>
  <p style="">链接管理功能</p></li>
 <li>
  <p style="">索引浏览功能</p></li>
 <li>
  <p style="">索引管理功能</p></li>
 <li>
  <p style="">语法提示与高亮</p></li>
 <li>
  <p style="">......</p></li>
</ul>
<h3 style="" id="%E4%BA%94%E3%80%81%E6%9E%84%E5%BB%BA%E5%AE%89%E8%A3%85">五、构建安装</h3>
<h4 style="" id="%E6%BA%90%E7%A0%81%E6%9E%84%E5%BB%BA">源码构建</h4>
<p style="">依赖安装</p>
<p style="">本项目使用yarn进行包管理</p>
<pre><code>yarn install</code></pre>
<p style="">构建浏览器</p>
<p style="">也是构建想天浏览器</p>
<pre><code>yarn build</code></pre>
<p style="">构建完成后，代码在【dist】目录下</p>
<h4 style="" id="%E6%9E%84%E5%BB%BAedge%E6%8F%92%E4%BB%B6">构建edge插件</h4>
<p style="">也是所有 Chromium 内核浏览器的插件</p>
<pre><code>yarn build:edge</code></pre>
<h4 style="" id="%E6%9E%84%E5%BB%BA%E6%A1%8C%E9%9D%A2%E5%AE%A2%E6%88%B7%E7%AB%AF">构建桌面客户端</h4>
<p style="">本项目客户端是基于Tauri，所以构建桌面客户端，首先你需要了解Tauri。</p>
<p style="">你需要参考这篇文章来了解如何安装环境，安装环境后</p>
<pre><code># 构建Windows客户端
yarn build:tauri:windows
yarn tauri build</code></pre>
<p style="">根据提示可以看到安装包未知</p>
<h3 style="" id="%E5%85%AD%E3%80%81%E5%8F%91%E8%A1%8C%E7%89%88%E6%9C%AC">六、发行版本</h3>
<ul>
 <li>
  <p style="">Edge插件</p></li>
 <li>
  <p style="">火狐插件</p></li>
 <li>
  <p style="">想天浏览器</p></li>
 <li>
  <p style="">windows安装包</p></li>
 <li>
  <p style="">utools</p></li>
 <li>
  <p style="">vscode</p></li>
 <li>
  <p style="">IDEA（第三方提供：es-client）</p></li>
</ul>
<h3 style="" id="%E4%B8%83%E3%80%81%E6%BA%90%E7%A0%81%E5%9C%B0%E5%9D%80">七、<strong><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">源码地址</span></strong></h3>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://es-client.esion.xyz/">https://es-client.esion.xyz/</a></p>]]></description><guid isPermaLink="false">/archives/elasticsearchke-shi-hua-gong-ju</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FElasticSearch.png&amp;size=m" type="image/jpeg" length="0"/><category>开源工具</category><pubDate>Fri, 12 Jan 2024 13:13:00 GMT</pubDate></item><item><title><![CDATA[IntellijIDEA常用插件]]></title><link>https://xiaoming728.com/archives/wei-ming-ming-wen-zhang</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=IntellijIDEA%E5%B8%B8%E7%94%A8%E6%8F%92%E4%BB%B6&amp;url=/archives/wei-ming-ming-wen-zhang" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="%E5%89%8D%E8%A8%80">前言</h2>
<p style="">基本上每个程序员都会写代码，但写代码的速度不尽相同。</p>
<p style="">为什么有些人，一天只能写几百行代码？</p>
<p style="">而有些人，一天可以写几千行代码？</p>
<p style="">有什么办法可以提升开发效率，在相同的时间内写出更多的代码呢？</p>
<p style="">今天我跟大家一起聊聊在idea中，能提升编码效率的12种插件，希望对大家有所帮助。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559718495-1475c656-16cb-4233-a5e8-3caa02523272.png&amp;size=m" width="1080" style="display: inline-block"></p>
<h2 style="" id="1.-lombok">1. lombok</h2>
<p style="">之前对 lombok 还有争议，到底该不该在项目中使用。</p>
<p style="">现在新版的 idea 已经内置了 lombok 插件，所以用它是一种趋势。</p>
<p style="">我之所以把 lombok 放在第一个介绍，是因为它真的可以帮我少写很多代码，特别是 entity、DTO、VO、BO 中的。</p>
<p style="">我们用 User 类举例，以前定义 javabean 需要写如下代码：</p>
<pre><code>public class User {

    private Long id;
    private String name;
    private Integer age;
    private String address;

    public User() {

    }

    public User(Long id, String name, Integer age, String address) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    public String getAddress() {
        return address;
    }


    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) returntrue;
        if (o == null || getClass() != o.getClass()) returnfalse;
        User user = (User) o;
        return Objects.equals(id, user.id) &amp;&amp;
            Objects.equals(name, user.name) &amp;&amp;
            Objects.equals(age, user.age) &amp;&amp;
            Objects.equals(address, user.address);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, age, address);
    }

    @Override
    public String toString() {
        return"User{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", age=" + age +
            ", address='" + address + '\'' +
            '}';
    }
}</code></pre>
<p style="">该 User 类中包含了：成员变量、getter/setter 方法、构造方法、equals、hashCode 方法。</p>
<p style="">乍一看，代码还是挺多的。而且还有个问题，如果 User 类中的代码修改了，比如：age 字段改成字符串类型，或者 name 字段名称修改了，是不是需要同步修改相关的成员变量、getter/setter 方法、构造方法、equals、hashCode 方法全都修改一遍？</p>
<p style="">好消息是用 lombok 可以解决这个问题。</p>
<p style="">如果是 idea2020.3 之前的版本，需要在 idea 中安装如下插件：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559718381-b818a861-1af2-47ba-a5e2-ae261ec7c51b.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">但 idea2020.3 之后，idea 已经内置了 lombok 的功能。</p>
<p style="">有了 lombok 插件，现在我们在 idea 只用这样写代码，就能实现上面的功能了：</p>
<pre><code>@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class User {

    private Long id;
    private String name;
    private Integer age;
    private String address;
}
</code></pre>
<p style="">简直太轻松了，真的可以少写很多代码。</p>
<p style="">此外，我们还需要在项目的 pom 文件中，引入 lombok 的依赖包，不然项目会跑不起来。</p>
<h2 style="" id="2.-free-mybatis-plugin">2. Free Mybatis plugin</h2>
<p style="">在国内mybatis已经成为了最主流的数据库框架了，该框架属于半自动化的 ORM 持久化框架，相对于 hibernate 这种全自动化的持久化框架更灵活，性能更高。</p>
<p style="">在mybatis中，我们需要自己定义 mapper 和对应的 xml 文件完成绑定。</p>
<p style="">在这里我们以用户表为例，首先需要定义 UserMapper 接口：</p>
<pre><code>public interface UserMapper {
  int insertUser(UserModel user);
}</code></pre>
<p style="">然后需要 UserMapper.xml 配置文件：</p>
<pre><code>&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"&gt;
&lt;mapper namespace="com.sue.jump.mappers.UserMapper"&gt;

  &lt;sql id="selectUserVo"&gt;
    id, name, age, sex
  &lt;/sql&gt;

  &lt;insert id="insertUser" parameterType="com.sue.jump.model.UserModel"&gt;
    INSERT INTO user
    &lt;trim prefix="(" suffix=")" suffixOverrides=","&gt;
      &lt;if test="id != null "&gt;
        id,
      &lt;/if&gt;
      &lt;if test="name != null  and name != ''"&gt;
        name,
      &lt;/if&gt;
      &lt;if test="age != null "&gt;
        age,
      &lt;/if&gt;
      &lt;if test="sex != null "&gt;
        sex,
      &lt;/if&gt;
    &lt;/trim&gt;
    &lt;trim prefix="values (" suffix=")" suffixOverrides=","&gt;
      &lt;if test="id != null "&gt;
        #{id},
      &lt;/if&gt;
      &lt;if test="name != null  and name != ''"&gt;
        #{name},
      &lt;/if&gt;
      &lt;if test="age != null "&gt;
        #{age},
      &lt;/if&gt;
      &lt;if test="sex != null "&gt;
        #{sex},
      &lt;/if&gt;
    &lt;/trim&gt;
  &lt;/insert&gt;
&lt;/mapper&gt;</code></pre>
<p style="">UserMapper.xml 文件中，mapper 标签的 namespace 对应 UserMapper 接口名，而 insert 标签的 id=insertUser，正好对应 UserMapper 接口中的 insertUser 方法。</p>
<p style="">那么，在项目中如何通过 UserMapper 类中的 getUser 方法，能够快速访问 UserMapper.xml 文件中的 getUser 方法？</p>
<p style="">答：这就需要使用Free Mybatis plugin插件了。</p>
<p style="">安装了该插件之后，在 UserMapper 接口的接口名和方法名的左边，多了两个绿色的箭头，我们点击该箭头，就能跳转到 UserMapper.xml 文件对应的 mapper 标签或者 insertUser 语句上。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559718825-f96509d2-012a-40b9-a4dc-d4b3633b846d.png&amp;size=m" width="638" style="display: inline-block"></p>
<p style="">此外，在 UserMapper.xml 文件的 insertUser 语句的左边，也会多出一个绿色的箭头，我们点击该箭头，也能跳转到 UserMapper 接口的 insertUser 方法上。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559718399-9d2f861e-16f8-4d69-8546-0bc4b8bb43a1.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">有了这个插件，我们就能在 mapper 和 xml 之间自由切换自由玩耍了，再也不用像以前那样搜索来搜索去。</p>
<h2 style="" id="3.translation">3.Translation</h2>
<p style="">有些小伙伴，包括我自己可能英语不太好。我们在给变量或者方法取名时，要想半天。特别是在阅读 JDK 英文文档时，遇到了一些生僻字，简直头大。</p>
<p style="">有个好消息是使用：Translation插件，能够让我们在文档中自由飞翔。</p>
<p style="">安装完Translation插件之后，在 other settings 中多了一个 Translation 菜单。</p>
<p style="">点击该菜单：</p>
<p style="">在右边的窗口中，可以选择翻译软件。</p>
<p style="">选中需要翻译的英文文档：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559718229-3acb176b-4f67-49af-8ab3-eb9fa8807a65.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">在右键弹窗的窗口中，选择 Translation 选项，会弹如下窗口：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559719056-b2efc6a8-30b6-4b52-93cc-f192df44f573.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">一段英文段落，一下子翻译成了中文，简直太爽了。</p>
<h2 style="" id="4.alibaba-java-coding-guidelines"><a target="_blank" rel="noopener noreferrer nofollow" href="http://4.Alibaba">4.Alibaba</a> Java Coding Guidelines</h2>
<p style="">如果你是从事 Java 开发工作的小伙伴，肯定看过阿里巴巴的《Java 开发手册》。</p>
<p style="">该手册总结了我们在日常开发过程中，可能会遇到的问题。从编程规约、异常日志、单位测试、安全规约、Mysql 数据库和工程结构这 6 大方面，规范了开发的流程，确保我们能写出高效、优雅的代码。</p>
<p style="">但这些规范性的东西，仅仅靠人的自觉性，很难达到预期的效果。</p>
<p style="">为了解决这个问题，阿里巴巴推出了Alibaba Java Coding Guidelines插件，能够通过该插件，直接查出不合规范的代码。</p>
<p style="">安装了该插件之后，按下快捷键：Ctrl+Alt+Shift+J，可以对整个项目或单个文件进行编码规约扫描。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559719111-a2538aa9-ed53-4854-bf8a-ab27303a96e6.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">扫描后会将不规范的代码按从高到低排列。</p>
<p style="">目前有三个等级显示在下方：</p>
<ul>
 <li>
  <p style="">Blocker 崩溃</p></li>
 <li>
  <p style="">Critical 严重</p></li>
 <li>
  <p style="">Major 重要</p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559719157-f8209cf6-4285-40f6-b015-b4822cdd286f.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">点击左边其中一个不规范的代码行，右边窗口会立刻显示不规范的详细代码，便于我们快速定位问题。nice!</p>
<h2 style="" id="5.-generateallsetter">5. GenerateAllSetter</h2>
<p style="">很多时候，我们需要给某个对象赋值，如果参数比较多的话，需要手写大量的setter或者getter代码。</p>
<p style="">有没有办法一键搞定呢？</p>
<p style="">答：有，使用GenerateAllSetter插件。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559719127-8036cc97-5571-42a6-a74a-9251b7ae6da4.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">安装完插件之后，在创建的对象上，按下快捷键：alt + enter。</p>
<p style="">在弹出的窗口中选择：Generate all setter with default value。</p>
<p style="">就会自动生成如下代码：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559719618-898fc179-eb6f-4394-ab61-99e5bea6fd3d.png&amp;size=m" width="762" style="display: inline-block"></p>
<p style="">简直太方便了。</p>
<h2 style="" id="6.-sequencediagram">6. SequenceDiagram</h2>
<p style="">我们平时在阅读源码时，为了梳理清楚内部逻辑，经常需要画一些时序图。</p>
<p style="">如果我们直接画，会浪费很多时间，而且画的图不一定正确。</p>
<p style="">这时可以使用：SequenceDiagram插件。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559719688-293d2b95-5dbb-43a2-8b05-4cc4983ca692.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">选择具体某个方法，右键选择：sequence diagram 选项：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559719858-b22cc6ec-7bf7-486a-a70b-7db5fbf08563.png&amp;size=m" width="952" style="display: inline-block"></p>
<p style="">之后，会出现时序图：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559719824-559e484e-1667-4ceb-ba06-ca2474d7a4fa.png&amp;size=m" width="476" style="display: inline-block"></p>
<p style="">从此以后，就成为画图高手了，完美。</p>
<h2 style="" id="7.-checkstyle-idea">7. CheckStyle-IDEA</h2>
<p style="">在代码格式方面，有许多地方需要我们注意，比如：无用导入、没写注释、语法错误、方法太长等等。</p>
<p style="">有没有办法，可以在 idea 中一次性检测出上面的这些问题呢？</p>
<p style="">答：使用CheckStyle-IDEA插件。</p>
<p style="">CheckStyle-IDEA是一个检测代码格式是否满足规范的工具，其中用得比较多的是Google规范和Sun规范。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559720207-3ffbe69e-5798-41b2-bb88-f5be9543f1c7.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">安装完插件后，在 idea 的下方会出现 CheckStyle 选项：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559720593-8e63c107-7905-4ed5-ba1f-da7d94ebd06c.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">点击左边的绿色按钮，可以扫描代码。在中间位置，会显示不符合代码规范的原因。</p>
<p style="">双击代码，即可直接跳转到具体代码：</p>
<h2 style="" id="8.jrebel-and-xrebel">8.JRebel and XRebel</h2>
<p style="">在 idea 中开发 Java 项目，有个很不爽的地方是：每次修改一个类或者接口，都需要重启服务，否则不会运行最新地方。而每次重启，都需要花大量的时间。</p>
<p style="">有没有办法，Java 代码修改后不用重启系统，立即生效呢？</p>
<p style="">答：使用JRebel and XRebel插件。</p>
<p style="">如图：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559720227-c0264186-00f0-406b-8835-2d9c39a57d4a.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">安装完成之后，这里会有两个绿色的按钮，并且在右边多了一个选项 Select Rebel Agents：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559720748-b80fca61-1d0c-4aae-9022-e50593e2a8cd.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">其中一个绿色的按钮，表示热部署启动项目，另外一个表示用 debug 默认热部署启动项目。</p>
<p style="">Select Rebel Agents 选项中包含三个值：</p>
<ul>
 <li>
  <p style="">JRebel：修改完代码，不重启服务，期望代码直接生效。</p></li>
 <li>
  <p style="">XRebel：请求过程中，各个部分代码性能监控。例如：方法执行时间、出现的异常、SQL 执行时间、输出的 Log、MQ 执行时间等。</p></li>
 <li>
  <p style="">JRebel+XRebel：修改完代码，不重启服务，并且监控代码。</p></li>
</ul>
<h2 style="" id="9.-codota">9. Codota</h2>
<p style="">说实话，idea 现有的代码提示功能，已经很强大了。</p>
<p style="">但如果你使用过Codota插件，它会让你写代码的速度更上一层楼。</p>
<p style="">安装完插件之后，我们在写代码时，它会给你一些提示：</p>
<p style="">这些提示是基于 ai 统计出来的，非常有参考价值。</p>
<h2 style="" id="10.-gsonformat">10. GsonFormat</h2>
<p style="">很多时候，我需要把json中的参数，转换成实体对象中的参数。或者把实体对象中的参数，转换成json中的参数。</p>
<p style="">以前我们都是手动一个变量一个变量拷贝。但现在有个好消息是，idea 的GsonFormat插件可以帮我们完成这件事。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559720738-e985e87a-db37-4386-8a6c-f56f171fb837.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">安装完插件之后，先创建一个空类：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559720692-aad2055e-1a71-45e5-9f88-2a58397eaa63.png&amp;size=m" width="444" style="display: inline-block"></p>
<p style="">按下快捷键：alt + s，会弹出下面这个窗口：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559720829-6feaf174-07b8-44f6-a129-3e78ace7255c.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">然后在该窗口中，录入 json 数据。</p>
<p style="">点击确定按钮，就会自动生成这些代码，简直帅呆了。</p>
<h2 style="" id="11.-rainbow-brackets">11. Rainbow Brackets</h2>
<p style="">我们平时写代码的时候，括号是让我们非常头疼的地方，特别是代码逻辑很多、层层嵌套的情况。</p>
<p style="">一眼很难看出，代码是从哪个括号开始，到哪个反括号结束的。</p>
<p style="">有没有办法解决这个问题呢？</p>
<p style="">答：使用Rainbow Brackets插件。</p>
<p style="">安装完插件之后，括号和反括号，在代码中会自动按照不同颜色做区分：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559721106-8c09f42b-e028-446d-8b46-511e65e3d5e0.png&amp;size=m" width="874" style="display: inline-block"></p>
<p style="">非常显目，非常直观。</p>
<h2 style="" id="12.-codeglance">12. CodeGlance</h2>
<p style="">有些时候，我们阅读的代码很多，比如某个类中包含的方法和成员变量很多。</p>
<p style="">从上往下，一点点往下翻，会浪费很多时间。那么有没有办法，能够快速翻到想看的代码呢？</p>
<p style="">答：有，可以使用CodeGlance插件。<img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559721356-c549f83d-df54-4206-ad80-68c931c8ca47.png&amp;size=m" width="1080" style="display: inline-block">安装完插件之后，在代码右侧，会出现下面这个窗口：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1665559721646-01bdba65-e092-4c88-ae2a-7a4259f111f2.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">它是代码的缩略图，通过它我们能够非常快速地切换代码块。</p>]]></description><guid isPermaLink="false">/archives/wei-ming-ming-wen-zhang</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fidea.png&amp;size=m" type="image/jpeg" length="0"/><category>开源工具</category><pubDate>Fri, 12 Jan 2024 13:13:00 GMT</pubDate></item><item><title><![CDATA[4个新的HTTP状态码：428、429、431、511]]></title><link>https://xiaoming728.com/archives/4ge-xin-de-httpzhuang-tai-ma-428-429-431-511</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=4%E4%B8%AA%E6%96%B0%E7%9A%84HTTP%E7%8A%B6%E6%80%81%E7%A0%81%EF%BC%9A428%E3%80%81429%E3%80%81431%E3%80%81511&amp;url=/archives/4ge-xin-de-httpzhuang-tai-ma-428-429-431-511" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1%E3%80%81428-precondition-required-(%E8%A6%81%E6%B1%82%E5%85%88%E5%86%B3%E6%9D%A1%E4%BB%B6)">1、428 Precondition Required (要求先决条件)</h3>
<p style=""><span style="font-size: 15px">先决条件是客户端发送 HTTP 请求时，必须要满足的一些预设条件。一个好的例子就是 If-None-Match 头，经常用在 GET 请求中。如果指定了 If-None-Match ，那么客户端只在响应中的 ETag 改变后才会重新接收回应。</span></p>
<p style=""><span style="font-size: 15px">先决条件的另外一个例子是 If-Match 头，一般用在 PUT 请求上，用于指示只更新但没有被改变的资源。这在多个客户端使用 HTTP 服务时用来防止彼此间覆盖相同内容的情况。</span></p>
<p style=""><span style="font-size: 15px">当服务器端使用 428 Precondition Required 状态码时，表示客户端必须发送上述的请求头才能执行该请求操作。这个方法为服务器提供一种有效的方法来阻止 “lost update”问题的出现。</span></p>
<h3 style="" id="2%E3%80%81429-too-many-requests-(%E5%A4%AA%E5%A4%9A%E8%AF%B7%E6%B1%82)">2、429 Too Many Requests (太多请求)</h3>
<p style=""><span style="font-size: 15px">当你需要限制客户端请求某个服务的数量，也就是限制请求速度时，该状态码就会非常有用。在此之前，有一些类似的状态码。例如“509 Bandwidth Limit Exceeded”。</span></p>
<p style=""><span style="font-size: 15px">如果你希望限制客户端对服务的请求数，可使用 429 状态码，同时包含一个 Retry-After 响应头用于告诉客户端多长时间后可以再次请求服务。</span></p>
<h3 style="" id="3%E3%80%81431-request-header-fields-too-large-(%E8%AF%B7%E6%B1%82%E5%A4%B4%E5%AD%97%E6%AE%B5%E5%A4%AA%E5%A4%A7)">3、431 Request Header Fields Too Large (请求头字段太大)</h3>
<p style=""><span style="font-size: 15px">某些情况下，客户端发送 HTTP 请求头会变得很大，那么服务器可发送 431 Request Header Fields Too Large 来指明该问题。</span></p>
<p style=""><span style="font-size: 15px">我不太清楚为什么没有 430 状态码，而是直接从 429 跳到 431，我尝试搜索但没有结果。唯一的猜测是 430 Forbidden 跟 403 Forbidden 太像了，为了避免混淆才这么做的，天知道！</span></p>
<h3 style="" id="4%E3%80%81511-network-authentication-required-(%E8%A6%81%E6%B1%82%E7%BD%91%E7%BB%9C%E8%AE%A4%E8%AF%81)">4、511 Network Authentication Required (要求网络认证)</h3>
<p style=""><span style="font-size: 15px">对我来说这个状态码很有趣，如果你在开发一个 HTTP 服务器，你不一定需要处理该状态码，但如果你在编写 HTTP 客户端，那这个状态码就非常重要。</span></p>
<p style=""><span style="font-size: 15px">如果你频繁使用笔记本和智能手机，你可能会注意到大量的公用 Wifi 服务要求你必须接受一些协议或者必须登录后才能使用，这是通过拦截HTTP流量实现的。当用户试图访问网络返回一个重定向和登录，这很讨厌，但是实际情况就是这样的。</span></p>
<p style=""><span style="font-size: 15px">使用这些“拦截”客户端，会有一些讨厌的副作用。在 RFC 中提到以下这两个的例子：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">如果你在登录Wifi前访问某个网站，网络设备将会拦截首个请求，这些设备往往也有自己的网站图标“favicon.ico”。登录后你会发现，有一段时间内你访问的网站图标一直是Wifi登录网站的图标。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">如果客户端使用HTTP请求来查找文档，网络将会响应一个登录页，这样你的客户端就会解析错误并导致客户端运行异常，在现实中这种问题非常常见。</span></p></li>
</ul>
<p style=""><span style="font-size: 15px">而 511 状态码的提出就是为了解决这个问题。因此，如果你正在编写 HTTP 的客户端，你最好还是检查 511 状态码以确认是否需要认证后才能访问。</span></p>]]></description><guid isPermaLink="false">/archives/4ge-xin-de-httpzhuang-tai-ma-428-429-431-511</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fhttp%2520codes.webp&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:12:06 GMT</pubDate></item><item><title><![CDATA[区块链技术]]></title><link>https://xiaoming728.com/archives/qu-kuai-lian-ji-shu</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF&amp;url=/archives/qu-kuai-lian-ji-shu" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="%E6%A6%82%E5%BF%B5%E5%AE%9A%E4%B9%89">概念定义</h2>
<p style=""><span style="font-size: 15px">什么是区块链？从科技层面来看，区块链涉及数学、密码学、互联网和计算机编程等很多科学技术问题。从应用视角来看，简单来说，区块链是一个分布式的共享账本和数据库，具有去中心化、不可篡改、全程留痕、可以追溯、集体维护、公开透明等特点。这些特点保证了区块链的“诚实”与“透明”，为区块链创造信任奠定基础。而区块链丰富的应用场景，基本上都基于区块链能够解决信息不对称问题，实现多个主体之间的协作信任与一致行动。</span></p>
<p style=""><span style="font-size: 15px">区块链是分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式。区块链（Blockchain），是比特币的一个重要概念，它本质上是一个去中心化的数据库，同时作为比特币的底层技术，是一串使用密码学方法相关联产生的数据块，每一个数据块中包含了一批次比特币网络交易的信息，用于验证其信息的有效性（防伪）和生成下一个区块。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">国家互联网信息办公室2019年1月10日发布《</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://baike.baidu.com/item/%E5%8C%BA%E5%9D%97%E9%93%BE%E4%BF%A1%E6%81%AF%E6%9C%8D%E5%8A%A1%E7%AE%A1%E7%90%86%E8%A7%84%E5%AE%9A/23245975"><span style="font-size: 15px">区块链信息服务管理规定</span></a><span style="font-size: 15px">》，自2019年2月15日起施行。</span></p>
<p style=""></p>
<h2 style="" id="%E7%B1%BB%E5%9E%8B">类型</h2>
<h3 style="" id="%E5%85%AC%E6%9C%89%E5%8C%BA%E5%9D%97%E9%93%BE">公有区块链</h3>
<p style=""><span style="font-size: 15px">公有区块链（Public Block Chains)是指：世界上任何个体或者团体都可以发送交易，且交易能够获得该区块链的有效确认，任何人都可以参与其共识过程。公有区块链是最早的区块链，也是应用最广泛的区块链，各大bitcoins系列的虚拟数字货币均基于公有区块链，世界上有且仅有一条该币种对应的区块链 。</span></p>
<h3 style="" id="%E8%81%94%E5%90%88%EF%BC%88%E8%A1%8C%E4%B8%9A%EF%BC%89%E5%8C%BA%E5%9D%97%E9%93%BE">联合（行业）区块链</h3>
<p style=""><span style="font-size: 15px">行业区块链（Consortium Block Chains)：由某个群体内部指定多个预选的节点为记账人，每个块的生成由所有的预选节点共同决定（预选节点参与共识过程），其他接入节点可以参与交易，但不过问记账过程(本质上还是托管记账，只是变成分布式记账，预选节点的多少，如何决定每个块的记账者成为该区块链的主要风险点），其他任何人可以通过该区块链开放的API进行限定查询。</span></p>
<h3 style="" id="%E7%A7%81%E6%9C%89%E5%8C%BA%E5%9D%97%E9%93%BE">私有区块链</h3>
<p style=""><span style="font-size: 15px">私有区块链（Private Block Chains)：仅仅使用区块链的总账技术进行记账，可以是一个公司，也可以是个人，独享该区块链的写入权限，本链与其他的分布式存储方案没有太大区别。传统金融都是想实验尝试私有区块链，而公链的应用例如bitcoin已经工业化，私链的应用产品还在摸索当中。</span></p>
<p style=""></p>
<h2 style="" id="%E7%89%B9%E5%BE%81">特征</h2>
<p style=""><span style="font-size: 15px">去中心化。区块链技术不依赖额外的第三方管理机构或硬件设施，没有中心管制，除了自成一体的区块链本身，通过分布式核算和存储，各个节点实现了信息自我验证、传递和管理。去中心化是区块链最突出最本质的特征&nbsp;。</span></p>
<p style=""><span style="font-size: 15px">开放性。区块链技术基础是开源的，除了交易各方的私有信息被加密外，区块链的数据对所有人开放，任何人都可以通过公开的接口查询区块链数据和开发相关应用，因此整个系统信息高度透明&nbsp;。</span></p>
<p style=""><span style="font-size: 15px">独立性。基于协商一致的规范和协议(类似比特币采用的哈希算法等各种数学算法)，整个区块链系统不依赖其他第三方，所有节点能够在系统内自动安全地验证、交换数据，不需要任何人为的干预。</span></p>
<p style=""><span style="font-size: 15px">安全性。只要不能掌控全部数据节点的51%，就无法肆意操控修改网络数据，这使区块链本身变得相对安全，避免了主观人为的数据变更。</span></p>
<p style=""><span style="font-size: 15px">匿名性。除非有法律规范要求，单从技术上来讲，各区块节点的身份信息不需要公开或验证，信息传递可以匿名进行。</span></p>
<p style=""></p>
<h2 style="" id="%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF">核心技术</h2>
<h3 style="" id="%E5%88%86%E5%B8%83%E5%BC%8F%E8%B4%A6%E6%9C%AC">分布式账本</h3>
<p style=""><span style="font-size: 15px">分布式账本指的是交易记账由分布在不同地方的多个节点共同完成，而且每一个节点记录的是完整的账目，因此它们都可以参与监督交易合法性，同时也可以共同为其作证。</span></p>
<p style=""><span style="font-size: 15px">跟传统的分布式存储有所不同，区块链的分布式存储的独特性主要体现在两个方面：一是区块链每个节点都按照块链式结构存储完整的数据，传统分布式存储一般是将数据按照一定的规则分成多份进行存储。二是区块链每个节点存储都是独立的、地位等同的，依靠共识机制保证存储的一致性，而传统分布式存储一般是通过中心节点往其他备份节点同步数据。没有任何一个节点可以单独记录账本数据，从而避免了单一记账人被控制或者被贿赂而记假账的可能性。也由记账节点足够多，理论上讲除非所有的节点被破坏，否则账目就不会丢失，从而保证了账目数据的安全性。</span></p>
<h3 style="" id="%E9%9D%9E%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86">非对称加密</h3>
<p style=""><span style="font-size: 15px">存储在区块链上的交易信息是公开的，但是账户身份信息是高度加密的，只有在数据拥有者授权的情况下才能访问到，从而保证了数据的安全和个人的隐私。</span></p>
<h3 style="" id="%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6">共识机制</h3>
<p style=""><span style="font-size: 15px">共识机制就是所有记账节点之间怎么达成共识，去认定一个记录的有效性，这既是认定的手段，也是防止篡改的手段。区块链提出了四种不同的共识机制，适用于不同的应用场景，在效率和安全性之间取得平衡。</span></p>
<p style=""><span style="font-size: 15px">区块链的共识机制具备“少数服从多数”以及“人人平等”的特点，其中“少数服从多数”并不完全指节点个数，也可以是计算能力、股权数或者其他的计算机可以比较的特征量。“人人平等”是当节点满足条件时，所有节点都有权优先提出共识结果、直接被其他节点认同后并最后有可能成为最终共识结果。以比特币为例，采用的是工作量证明，只有在控制了全网超过51%的记账节点的情况下，才有可能伪造出一条不存在的记录。当加入区块链的节点足够多的时候，这基本上不可能，从而杜绝了造假的可能。</span></p>
<h3 style="" id="%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6">智能合约</h3>
<p style=""><span style="font-size: 15px">智能合约是基于这些可信的不可篡改的数据，可以自动化的执行一些预先定义好的规则和条款。以保险为例，如果说每个人的信息（包括医疗信息和风险发生的信息）都是真实可信的，那就很容易的在一些标准化的保险产品中，去进行自动化的理赔。在保险公司的日常业务中，虽然交易不像银行和证券行业那样频繁，但是对可信数据的依赖是有增无减。因此，笔者认为利用区块链技术，从数据管理的角度切入，能够有效地帮助保险公司提高风险管理能力。具体来讲主要分投保人风险管理和保险公司的风险监督。</span></p>
<p style=""></p>
<h2 style="" id="%E6%95%B0%E5%AD%97%E7%89%88%E6%9D%83%E5%BA%94%E7%94%A8">数字版权应用</h2>
<p style=""><span style="font-size: 15px">通过区块链技术，可以对作品进行鉴权，证明文字、视频、音频等作品的存在，保证权属的真实、唯一性。作品在区块链上被确权后，后续交易都会进行实时记录，实现数字版权全生命周期管理，也可作为司法取证中的技术性保障。例如，美国纽约一家创业公司Mine Labs开发了一个基于区块链的元数据协议，这个名为Mediachain的系统利用IPFS文件系统，实现数字作品版权保护，主要是面向数字图片的版权保护应用。</span></p>
<p style=""></p>
<h2 style="" id="%E5%85%AC%E7%9B%8A%E5%BA%94%E7%94%A8">公益应用</h2>
<p style=""><span style="font-size: 15px">区块链上存储的数据，高可靠且不可篡改，天然适合用在社会公益场景。公益流程中的相关信息，如捐赠项目、募集明细、资金流向、受助人反馈等，均可以存放于区块链上，并且有条件地进行透明公开公示，方便社会监督。</span></p>
<p style=""></p>
<h2 style="" id="%E7%94%B5%E5%AD%90%E6%A1%A3%E6%A1%88%E5%BA%94%E7%94%A8">电子档案应用</h2>
<p style=""><span style="font-size: 15px">（待完善）</span></p>]]></description><guid isPermaLink="false">/archives/qu-kuai-lian-ji-shu</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E5%258C%25BA%25E5%259D%2597%25E9%2593%25BE.webp&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:12:00 GMT</pubDate></item><item><title><![CDATA[如何写出高质量的技术文章]]></title><link>https://xiaoming728.com/archives/ru-he-xie-chu-gao-zhi-liang-de-ji-shu-wen-zhang</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%A6%82%E4%BD%95%E5%86%99%E5%87%BA%E9%AB%98%E8%B4%A8%E9%87%8F%E7%9A%84%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0&amp;url=/archives/ru-he-xie-chu-gao-zhi-liang-de-ji-shu-wen-zhang" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px">对于一个从小不喜欢语文、不喜欢阅读、作文很少及格的理科生来说，做梦也没想到，有一天我会写一篇文章教人如何写文章&nbsp; :)</span></p>
<h1 style="" id="%E4%B8%80-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%86%99%E6%96%87%E7%AB%A0">一&nbsp; 为什么要写文章</h1>
<p style=""><span style="font-size: 15px">懂了，不一定能说出来，说得出来，不一定能写出来。这就是写文章最大的好处，官方术语叫“费曼教学法”。写文章是一个逼迫自己深入理解问题、把问题想清楚，整理好思路，并能清晰表达出来的过程。其本质是一种自我学习、自我提升、构建知识体系的最佳方法。<br>
  除此之外，写文章还有一个副产品——帮助我们扩大影响力。就拿我来说，我大概是在4年前，开始有规划地搭建自己的知识体系，包括阅读、记笔记、写文章、分享。<br>
  在这期间，我连续3年获得最佳年度作者，上头条的文章也不少。另外，阿里技术公众号，也发表了我10篇左右的文章，其中有2篇入选了创刊最佳文章，很多篇文章都有3万+阅读的不错表现。<br>
  基于这些总结沉淀，我在人民邮电出版社出版了一本书《代码精进之路：从码农到工匠》，我本人也凭借本书获得了人邮IT类年度最佳作者。</span></p>
<h1 style="" id="%E4%BA%8C-%E4%BB%80%E4%B9%88%E6%98%AF%E5%A5%BD%E7%9A%84%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0">二&nbsp; 什么是好的技术文章</h1>
<p style=""><span style="font-size: 15px">关于优质文章的标准，有一个对好文章的判断是：“文章框架完整、思考深入清晰、正文至少80%以上为原创技术干货。”这引起了不少的争议。<br>
  有争议很正常，没有争议才奇怪呢，好的技术文章，就和好的技术绩效一样，主观性太强......很难有一个客观标准。<br>
  如果硬要对文章质量进行量化的话，有些指标可能有帮助。&nbsp;比如文章的浏览量、点赞数、评论数、收藏数等指标。有用，但也只是参考作用，最后还是需要人的评判。<br>
  所以抛开这些因素不看，我认为好的技术文章至少应该满足两个条件：</span></p>
<ol>
 <li>
  <p style=""><span style="font-size: 15px">一个是要传达有价值的信息。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">另一个是要结构和逻辑清晰，具备一定可读性和可理解性。</span></p></li>
</ol>
<p style=""></p>
<p style=""><span style="font-size: 15px">在此基础之上，如果能做到有文采、够风趣那就更好了。</span></p>
<h1 style="" id="%E4%B8%89-%E5%A6%82%E4%BD%95%E5%86%99%E5%A5%BD%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0">三&nbsp; 如何写好技术文章</h1>
<h2 style="" id="%E5%86%85%E5%AE%B9%E6%9C%89%E7%94%A8">内容有用</h2>
<p style=""><span style="font-size: 15px">一篇好文章，一本好书，最重要的是要让读者有获得感，要对读者有用，要言之有物。<br>
  这里的“物”可大可小，不一定非要是一个很大的命题，恰恰相反，一篇文章的篇幅有限，能把一件“小事情”说清楚就已经很不错了。<br>
  我曾经发表过一篇文章——《阿里缩写和专业术语大全》，做的事情很简单，就是把我在阿里碰到的缩写全都整理成册。就这么一篇“小文章”，却成了我最火的一篇文章，有将近100K的阅读，3K的点赞。<br>
  为什么大家会如此关注这篇没有“技术含量”的文章？很简单，很多人都有好奇心，大家都希望了解缩写背后的全称和来历，这正是这篇文章的价值所在。<br>
  类似的，像云原生技术介绍、前端技术体系大全、新人入职手册、《马总演讲集》等都属于这种信息整合类的文章。<br>
  除此之外，那些有自己技术见解和思考，敢说真话的文章，也会受到大家的欢迎。<br>
  比如，我觉得很多技术团队不应该有架构师这个岗位，所以写了《人人都是架构师：架构是一种能力，不是title！》。我觉得很多的软件复杂度来自于工程师的乱作为，比如滥用流程引擎，治理复杂度的根本是抽象思维和结构化思维，于是写了《一文教会你如何写复杂业务代码》。<br>
  这些文章之所以受到欢迎，是因为引起了很多同学的共鸣，对他们有帮助。</span></p>
<h2 style="" id="%E7%BB%93%E6%9E%84%E6%B8%85%E6%99%B0">结构清晰</h2>
<p style=""><span style="font-size: 15px">有了好的内容，还要注意文章的结构。就像一道菜，要讲究色香味俱全，即使有了最好的食材，但是做出来的样子一团糟，也会影响食欲，称不上是一道好菜。<br>
  关于结构，我推荐你去看一本书——《金字塔原理》，我本人也写过不少关于结构化思维的文章。金字塔原理教导我们在写作、表达的时候，要构建清晰的结构。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1616029864131-b96ca81f-dd00-4c1a-9ec2-2e54a166864f.png&amp;size=m" width="720" style="display: inline-block"></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">对于一篇文章来说，金字塔的顶点是中心论点——通常就是文章的标题。围绕着这个中心论点，我们可以用多个观点去支撑中心论点，如果表达的内容很多，观点还可以进一步往下细分。形成一个“以上统下、逻辑递进”的金字塔结构。<br>
  通过这种形式写出的文章，就会显得逻辑清晰，结构紧凑。<br>
  对于技术文章来说，我们可以考虑使用3W2H模型来帮助我们构建结构。比如我要写一篇关于抽象能力的文章，就可以通过以下角度去说：</span></p>
<ol>
 <li>
  <p style=""><span style="font-size: 15px">What：什么是抽象；</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">Why：抽象为什么重要；</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">How：如何进行抽象；</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">Where：抽象可以用在什么地方；</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">How much：抽象到什么程度。</span></p></li>
</ol>
<p style=""><span style="font-size: 15px"><br>
  同样，我现在正在写的这篇文章，我也是通过这种方式来搭建结构的：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1616029882144-3ba71b76-b501-4188-9658-dbfe94ff7b78.png&amp;size=m" width="719" style="display: inline-block"></p>
<h2 style="" id="%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0">刻意练习</h2>
<p style=""><span style="font-size: 15px">开篇说过了，我以前没有写作的习惯，小时候语文作文也经常不及格。后面竟然出版了自己的书，说明写作作为一项技能，是可以习得的，是可以通过练习提高的。<br>
  因为写的多了，练习的多了，水平自然就会提高。然而，所谓的《刻意练习》（也是一本书），不是简单地重复，而是要给自己阶段性的设定更高的目标，这样才会持续地进步。<br>
  比如，我现在已经能比较流畅地写作，我就会去追求如何把文章写的更加引人入胜。在《风格感觉：21世纪写作指南》里面提到一篇文章，它的开头是这样写的：“我们都会死，我们是幸运的...”，像这种冲突感和悬念，就会很自然地吸引读者继续阅读下去。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">我这篇文章也借用了同样的手法&nbsp;&nbsp;:)</span></p>
<h2 style="" id="%E8%BF%AD%E4%BB%A3%E4%BC%98%E5%8C%96">迭代优化</h2>
<p style=""><span style="font-size: 15px">写文章和写代码有非常多相似的地方。我会经常拿写文章和写代码做类比。比如，文章和代码都需要结构清晰。又如，好的系统不是设计出来的，是迭代出来的。好的文章也是如此，需要不断的打磨、修改，我的很多文章都是经过多次修改，重新编排结构，补充删除信息，调整措词，直到我觉得满意为止。<br>
  就拿现在这篇文章来说，也是修改了好几版，第一遍的时候内容很散，结构也不清晰，有点纠结，不知道要如何写下去。后面想到一个“以身作则”的方法，即这篇文章本身应该就可以作为一个sample来介绍如何写文章。带着这个思路，迭代几次之后，就逐渐成了一篇像样的文章了。<br>
  所以，重要的是要敢于去“动笔”，不要担心一开始的粗枝大叶，万事开头难，写着...写着... 你就有感觉了。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">原创&nbsp;张建飞&nbsp;阿里技术&nbsp;2021-03-17</span></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247502777&amp;amp;idx=1&amp;amp;sn=6d65b015317725b92cb1cb25c97895e8&amp;amp;chksm=e92af6b6de5d7fa02cdad3c5387452269893daf9bb73e144159492b6f0003a0279bc1151f79e&amp;amp;scene=178&amp;amp;cur_album_id=1427971693057998849#rd"><span style="font-size: 15px">https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;mid=2247502777&amp;idx=1&amp;sn=6d65b015317725b92cb1cb25c97895e8&amp;chksm=e92af6b6de5d7fa02cdad3c5387452269893daf9bb73e144159492b6f0003a0279bc1151f79e&amp;scene=178&amp;cur_album_id=1427971693057998849#rd</span></a></p>]]></description><guid isPermaLink="false">/archives/ru-he-xie-chu-gao-zhi-liang-de-ji-shu-wen-zhang</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fwork.webp&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:11:09 GMT</pubDate></item><item><title><![CDATA[微服务架构鉴权的应用场景]]></title><link>https://xiaoming728.com/archives/wei-fu-wu-jia-gou-jian-quan-de-ying-yong-chang-jing</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E9%89%B4%E6%9D%83%E7%9A%84%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF&amp;url=/archives/wei-fu-wu-jia-gou-jian-quan-de-ying-yong-chang-jing" width="1" height="1" alt="" style="opacity:0;">
<p style="">传统的单体应用体系下，应用是一个整体，一般针对所有的请求都会进行权限校验。请求一般会通过一个权限的拦截器进行权限的校验，在登录时将用户信息缓存到 session 中，后续访问则从缓存中获取用户信息</p>
<p style=""></p>
<p style="">但在微服务架构下，一个应用会被拆分成若干个微应用，每个微应用都需要对访问进行鉴权，每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器，还包括其他服务的调用时，单体应用架构下的鉴权方式就不是特别合适了。因此在设计架构中，要考虑外部应用接入的场景、用户与服务的鉴权、服务与服务的鉴权等多种鉴权场景。</p>
<p style=""></p>
<p style="">目前主流的方案由四种：</p>
<p style=""></p>
<h4 style="" id="%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95%EF%BC%88sso%EF%BC%89">单点登录（SSO）</h4>
<p style="">一次登入，多地使用。这种方案意味着每个面向用户的服务都必须与认证服务交互，进而产生大量琐碎的网络流量和重复的工作，当动辄数十个微应用时，这种方案的弊端会更加明显。</p>
<p style=""></p>
<h4 style="" id="%E5%88%86%E5%B8%83%E5%BC%8F-session-%E6%96%B9%E6%A1%88">分布式 Session 方案</h4>
<p style="">借助reids或其他共享存储中，将用户认证的信息存储在其中，通常使用用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时，用户数据可以从共享存储中获取。在某些场景下，这种方案很不错，用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制，因此需要通过安全链接来访问，这时解决方案的实现就通常具有相当高的复杂性了。</p>
<p style=""></p>
<h4 style="" id="%E5%AE%A2%E6%88%B7%E7%AB%AF-token-%E6%96%B9%E6%A1%88">客户端 Token 方案</h4>
<p style="">令牌在客户端生成，由身份验证服务进行签名，并且必须包含足够的信息，以便可以在所有微服务中建立用户身份。令牌会附加到每个请求上，为微服务提供用户身份验证，这种解决方案的安全性相对较好，但身份验证注销是一个大问题，缓解这种情况的方法可以使用短期令牌和频繁检查认证服务等。对于客户端令牌的编码方案，Borsos 更喜欢使用 JSON Web Tokens（JWT），它足够简单且库支持程度也比较好。</p>
<p style=""></p>
<h4 style="" id="%E5%AE%A2%E6%88%B7%E7%AB%AF-token-%E4%B8%8E-api-%E7%BD%91%E5%85%B3%E7%BB%93%E5%90%88">客户端 Token 与 API 网关结合</h4>
<p style="">这个方案意味着所有请求都通过网关，从而有效地隐藏了微服务。 在请求时，网关将原始用户令牌转换为内部会话 ID 令牌。在这种情况下，注销就不是问题，因为网关可以在注销时撤销用户的令牌。</p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/wei-fu-wu-jia-gou-jian-quan-de-ying-yong-chang-jing</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fauth-cover.jpg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:11:00 GMT</pubDate></item><item><title><![CDATA[有选择性的使用领域驱动设计]]></title><link>https://xiaoming728.com/archives/you-xuan-ze-xing-de-shi-yong-ling-yu-qu-dong-she-ji</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E6%9C%89%E9%80%89%E6%8B%A9%E6%80%A7%E7%9A%84%E4%BD%BF%E7%94%A8%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1&amp;url=/archives/you-xuan-ze-xing-de-shi-yong-ling-yu-qu-dong-she-ji" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px">来源：</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://www.blogjava.net/johnnylzb/"><span style="font-size: 15px">Johnny.Liang</span></a></p>
<p style=""><span style="font-size: 15px">时间：2010-06-26 17:47</span></p>
<p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://www.blogjava.net/johnnylzb/"><span style="font-size: 15px">http://www.blogjava.net/johnnylzb/archive/2010/06/26/324563.html</span></a></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">本系列的第一篇博文抛砖引玉，大谈领域驱动设计的优势，这里笔者还是希望以客观的态度，谈谈领域驱动设计的缺点及其不适合使用的场景，以让读者可以有选择性的使用领域驱动设计。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">我们知道，没有最好，只有最合适，设计也是一样。因此，所谓设计，就是以你和你的团队的知识、经验和智慧，全面充分的考虑各种内外因素后，在你们的设计方案中作出合理的选择的过程。而这些影响你们选择的因素主要有：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">技术框架的特征和约束（如果你的项目决定使用C语言进行开发，那么首先在设计方法上，就需要使用面向过程而非面向对象的设计方法）。</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">时间的压力和约束（你永远不可能告诉你的老板，给我10年时间，我和我的团队将为你设计出世界上最优秀的软件）。</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">你的团队的能力、经验、性格、价值观等因素的约束（你不能期望一个长期从事遗留系统维护项目或大部分成员是缺乏经验的高校毕业生的团队能很好的按照你的设计意图去实现你的高度抽象的优秀设计，同时你也别指望一帮家里经济条件不错，本着过来熬时间的家伙会乐意与你一起刻苦钻研、精益求精）。</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">你的系统的特征（如果你想把一个足够简单而且在可以预计的将来都不存在很大规模的需求变更的系统设计得很复杂，很精妙，具有很好的扩展性，但要为此付出巨大的时间、人力成本，这显然是一种不理智的过度设计（Over design））。</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">其他外在因素的约束（你的项目需要参与投标，你必须压缩人力、时间等以让你的项目成本成为巨大的竞争资本）。</span></p></li>
</ul>
<p style=""></p>
<p style=""><span style="font-size: 15px">当然，上述的考虑因素站在比较高的角度，通常是项目经理、架构师需要考虑的问题，但这当中你应该会得到一些启发。回到我们的主题，我们首先看看，领域驱动设计相对于传统的面向过程式的设计，有什么缺点：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">复杂化：面向过程思维之所以一直很受欢迎，是因为它很直观，非常符合大部分人的思维习惯，大部分人拿到一个问题，通常都是会很直观的想第一步做什么、第二步做什么，如果怎样，应该怎样，否则怎样……，可以说，任何水平的程序员，都能很好的使用面向过程的方法进行设计和开发。同时，由于我们教育水平的落后和整体IT环境的制约，可以这样说，真正掌握面向对象思维和设计方法的程序员的比例非常低（虽然绝大部分都使用面向对象的语言和工具），而本身面向对象思维要求人有很好的抽象思维能力，因为你需要把一个复杂的系统一层层的抽象为简单的部分，需要把现实世界的事物（有些是可见的，但有些是不可见）合理的抽象为计算机世界的不同元素。这些都不是一些很容易做的事情，要做得好，就更难。</span></p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">团队的抗拒：如果你的团队（很大可能）大部分人都习惯于用很直观的面向过程的方式进行设计和开发，你需要推动你的团队转换思维来采用面向对象的方式进行领域驱动设计，通常会遭到多数人的抗拒。因为人都是有惰性的，他们习惯安于现状，而改变是痛苦的，他们要付出额外的努力，需要进行学习，但以笔者的经验，相当一部分程序员，特别是有一定工作年限的程序员，他们从事IT工作都只是为了获得一份不错的报酬，因此他们的学习动力非常有限，而且，他们都相当自负，被说服的难度比较大。</span></p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">管理、开发和维护的成本高：复杂度更高，意味着你需要花更多的时间进行设计，同时需要花出额外的时间进行必要的培训，而且需要有更完善的文档（设计文档，API文档，培训文档等）。领域驱动设计的抽象程度比较高，因此必需有良好的文档，否则，随着项目的不断迭代、升级和维护，它很容易因为后来者的误解而慢慢回归面向过程的设计，甚至会变得“四不象”，领域驱动设计的成本优势是随着时间的推移慢慢体现的（见下图），如果出现这种情况，所有前面付出的努力都会付诸东流。</span></p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1629341549295-3661526f-e032-4988-bda3-d61a770be0ec.png&amp;size=m" width="637" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">系统的初始阶段，领域驱动设计需要付出更大的成本，但随着时间的推移，领域驱动设计的成本效益优势会逐步显现</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">性能比较低：使用纯面向对象的方式进行领域驱动设计的程序，其系统开销通常要比面向过程设计的程序高，从而性能相对较低（关于具体的例子，后续的博文会提及）。</span></p></li>
</ul>
<p style=""></p>
<p style=""><span style="font-size: 15px">那么，假设我们在时间、团队能力及各种资源都允许的情况下，是否就可以麻木的全盘使用领域驱动设计呢？正如本博文的标题一样，答案是否定的，我们需要有选择性的使用。让我们来看看一些指导性原则：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">使用领域驱动设计，并不代表整个系统的方方面面都必须遵从领域驱动设计的原则，需要根据实际情况，让适合的部分使用领域驱动设计，让不适合的部分使用面向过程的设计。</span></p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">对于那些业务规则非常简单，通常只有增、删、改、查的简单操作，而且也不大可能发生大规模需求变更的模块，可以让业务实体成为一个“贫血模型”，例如一些基础数据：系统参数、商品类型、国家、地址信息（注：对于这点，本人持保留态度，因为这些业务虽然非常简单，但既然选择了领取驱动设计，即使把这些业务实体设计为“充血模型”，即把极其简单的业务逻辑也封装在业务实体中，也并不比使用“贫血模型”成本高，而它却带来了统一设计风格的好处）。</span></p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">对于查询操作，特别是复杂的查询操作，出于性能的考虑，可以用结构化查询逻辑一次性完成，并把这些逻辑封装在Repository（即技术上的DAO）中（这方面的具体例子，后面关于“查询通道”和“领域对象仓库”的博文会更具体的阐述）。我们可以看到，对于一些业务视图，以及报表模块，很明显不适合使用面向对象的方式设计，因为这些“视图”和“报表”，本质上就不是业务实体。<br></span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">同样出于性能的考虑，在业务实体的实现逻辑中，某些操作不适合过度偏执的使用面向对象方式。例如，在“订单”的“新增订单明细”（order.addOrderItem(orderItem)）中，如果业务逻辑规定一张订单中包含优惠商品的明细数目不能超过20条，使用面向对象的方式，就需要把订单中的所有订单明细全部加载，然后逐个明细判断其对应的商品是否优惠商品，再统计出优惠商品的数目，这样很明显是低效率和高开销的，这里只需要使用Repository提供的一个统计方法，用一个结构化查询逻辑返回统计结果即可，而这就是非面向对象的方式。</span></p></li>
</ul>
<p style=""></p>
<p style=""><span style="font-size: 15px">本博文给有志于领域驱动设计的读者泼了一下冷水，提出一些“反模式”（Bitter），是为了让读者冷静一下，在领域驱动设计过程中作出更灵活和更合理的选择。关于这方面的论述，笔者在这里浅尝则止，限于水平、经验和表达能力，不敢胡乱卖弄，建议读者可以参考阅读Martin Fowler的《Patterns of Enterprise Application Architecture》一书的相关观点。</span></p>]]></description><guid isPermaLink="false">/archives/you-xuan-ze-xing-de-shi-yong-ling-yu-qu-dong-she-ji</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdomain-driven-design.png&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:10:00 GMT</pubDate></item><item><title><![CDATA[浅析VO、DTO、DO、PO 的概念、区别和用处]]></title><link>https://xiaoming728.com/archives/qian-xi-vo-dto-do-po-de-gai-nian-qu-bie-he-yong-chu</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E6%B5%85%E6%9E%90VO%E3%80%81DTO%E3%80%81DO%E3%80%81PO%20%E7%9A%84%E6%A6%82%E5%BF%B5%E3%80%81%E5%8C%BA%E5%88%AB%E5%92%8C%E7%94%A8%E5%A4%84&amp;url=/archives/qian-xi-vo-dto-do-po-de-gai-nian-qu-bie-he-yong-chu" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px">来源：微信公众号-石杉的架构笔记</span></p>
<p style=""><span style="font-size: 15px">时间： 2021-08-17</span></p>
<p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&amp;amp;mid=2247506981&amp;amp;idx=1&amp;amp;sn=ee95ace609e6575fc953693941875e81&amp;amp;chksm=fba53426ccd2bd3006417783360277fe155cd406b0141c6638a387b1874889bd488a1e70be22&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0818fvVs6I5NVTla5RXDsyOp&amp;amp;sharer_sharetime=1629239489496&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px">https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&amp;mid=2247506981&amp;idx=1&amp;sn=ee95ace609e6575fc953693941875e81&amp;chksm=fba53426ccd2bd3006417783360277fe155cd406b0141c6638a387b1874889bd488a1e70be22&amp;mpshare=1&amp;scene=23&amp;srcid=0818fvVs6I5NVTla5RXDsyOp&amp;sharer_sharetime=1629239489496&amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd</span></a></p>
<p style=""><span style="font-size: 15px">该文章作者引用《</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://www.blogjava.net/johnnylzb/archive/2010/05/27/321968.html"><span style="font-size: 15px">领域驱动设计系列文章（2）— 浅析VO、DTO、DO、PO的概念、区别和用处</span></a><span style="font-size: 15px">》对文章内容进行了完善和优化，原文章共三篇，主要说明领域驱动说明。</span></p>
<p style=""><span style="font-size: 15px">原文章链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&amp;amp;mid=2247506981&amp;amp;idx=1&amp;amp;sn=ee95ace609e6575fc953693941875e81&amp;amp;chksm=fba53426ccd2bd3006417783360277fe155cd406b0141c6638a387b1874889bd488a1e70be22&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0818fvVs6I5NVTla5RXDsyOp&amp;amp;sharer_sharetime=1629239489496&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px">http://www.blogjava.net/johnnylzb/archive/2010/05/27/321968.html</span></a></p>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">本篇文章主要讨论一下我们经常会用到的一些对象：VO、DTO、DO和PO。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">由于不同的项目和开发人员有不同的命名习惯，这里我首先对上述的概念进行一个简单描述，名字只是个标识，我们重点关注其概念：</span></p>
<p style=""></p>
<h3 style="" id="%E6%A6%82%E5%BF%B5">概念</h3>
<ul>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: inherit">VO（View Object）：</span></strong><span style="font-size: 15px; color: inherit">视图对象，用于展示层，它的作用是把某个指定页面（或组件）的所有数据封装起来。</span></p></li>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: inherit">DTO（Data Transfer Object）：</span></strong><span style="font-size: 15px; color: inherit">数据传输对象，这个概念来源于J2EE的设计模式，原来的目的是为了EJB的分布式应用提供粗粒度的数据实体，以减少分布式调用的次数，从而提高分布式调用的性能和降低网络负载，但在这里，我泛指用于展示层与服务层之间的数据传输对象。</span></p></li>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: inherit">DO（Domain Object）：</span></strong><span style="font-size: 15px; color: inherit">领域对象，就是从现实世界中抽象出来的有形或无形的业务实体。</span></p></li>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: inherit">PO（Persistent Object）：</span></strong><span style="font-size: 15px; color: inherit">持久化对象，它跟持久层（通常是关系型数据库）的数据结构形成一一对应的映射关系，如果持久层是关系型数据库，那么，数据表中的每个字段（或若干个）就对应PO的一个（或若干个）属性。</span></p></li>
</ul>
<h3 style="" id=""></h3>
<h3 style="" id="%E6%A8%A1%E5%9E%8B">模型</h3>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">下面以一个时序图建立简单模型来描述上述对象在三层架构应用中的位置</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fjpeg%2F550960%2F1629338797216-9071d112-26d4-49c3-8f8d-5b5cbeb2c0e5.jpeg&amp;size=m" width="701" style="display: inline-block"></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">用户发出请求（可能是填写表单），表单的数据在展示层被匹配为VO。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">展示层把VO转换为服务层对应方法所要求的DTO，传送给服务层。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">服务层首先根据DTO的数据构造（或重建）一个DO，调用DO的业务方法完成具体业务。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">服务层把DO转换为持久层对应的PO（可以使用ORM工具，也可以不用），调用持久层的持久化方法，把PO传递给它，完成持久化操作。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">对于一个逆向操作，如读取数据，也是用类似的方式转换和传递，略。</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<h3 style="" id="vo%E4%B8%8Edto%E7%9A%84%E5%8C%BA%E5%88%AB">VO与DTO的区别</h3>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">大家可能会有个疑问（在笔者参与的项目中，很多程序员也有相同的疑惑）：既然DTO是展示层与服务层之间传递数据的对象，为什么还需要一个VO呢？<br></span></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">对！对于绝大部分的应用场景来说，DTO和VO的属性值基本是一致的，而且他们通常都是POJO，因此没必要多此一举，但不要忘记这是实现层面的思维，对于设计层面来说，概念上还是应该存在VO和DTO，因为两者有着本质的区别，DTO代表服务层需要接收的数据和返回的数据，而VO代表展示层需要显示的数据。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">用一个例子来说明可能会比较容易理解：例如服务层有一个getUser的方法返回一个系统用户，其中有一个属性是gender(性别)，对于服务层来说，它只从语义上定义：1-男性，2-女性，0-未指定，而对于展示层来说，它可能需要用“帅哥”代表男性，用“美女”代表女性，用“秘密”代表未指定。说到这里，可能你还会反驳，在服务层直接就返回“帅哥美女”不就行了吗？</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">对于大部分应用来说，这不是问题，但设想一下，如果需求允许客户可以定制风格，而不同风格对于“性别”的表现方式不一样，又或者这个服务同时供多个客户端使用（不同门户），而不同的客户端对于表现层的要求有所不同，那么，问题就来了。再者，回到设计层面上分析，从职责单一原则来看，服务层只负责业务，与具体的表现形式无关，因此，它返回的DTO，不应该出现与表现形式的耦合。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">理论归理论，这到底还是分析设计层面的思维，是否在实现层面必须这样做呢？一刀切的做法往往会得不偿失，下面我马上会分析应用中如何做出正确的选择。</span></p>
<p style=""></p>
<h3 style="" id="vo%E4%B8%8Edto%E7%9A%84%E5%BA%94%E7%94%A8">VO与DTO的应用</h3>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">上面只是用了一个简单的例子来说明VO与DTO在概念上的区别，本节将会告诉你如何在应用中做出正确的选择。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">在以下才场景中，我们可以考虑把VO与DTO二合为一（注意：是实现层面）：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">需求非常清晰稳定，而且客户端很明确只有一个的时候，没有必要把VO和DTO区分开来，这时候VO可以退隐，用一个DTO即可，为什么是VO退隐而不是DTO？回到设计层面，服务层的职责依然不应该与展示层耦合，所以，对于前面的例子，你很容易理解，DTO对于“性别”来说，依然不能用“帅哥美女”，这个转换应该依赖于页面的脚本（如JavaScript）或其他机制（JSTL、EL、CSS）</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">即使客户端可以进行定制，或者存在多个不同的客户端，如果客户端能够用某种技术（脚本或其他机制）实现转换，同样可以让VO退隐</span></p></li>
</ul>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">以下场景需要优先考虑VO、DTO并存：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">上述场景的反面场景</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">因为某种技术原因，比如某个框架（如Flex）提供自动把POJO转换为UI中某些Field时，可以考虑在实现层面定义出VO，这个权衡完全取决于使用框架的自动转换能力带来的开发和维护效率提升与设计多一个VO所多做的事情带来的开发和维护效率的下降之间的比对。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">如果页面出现一个“大视图”，而组成这个大视图的所有数据需要调用多个服务，返回多个DTO来组装（当然，这同样可以通过服务层提供一次性返回一个大视图的DTO来取代，但在服务层提供一个这样的方法是否合适，需要在设计层面进行权衡）。</span></p></li>
</ul>
<p style=""></p>
<h3 style="" id="dto%E4%B8%8Edo%E7%9A%84%E5%8C%BA%E5%88%AB">DTO与DO的区别</h3>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">首先是概念上的区别，DTO是展示层和服务层之间的数据传输对象（可以认为是两者之间的协议），而DO是对现实世界各种业务角色的抽象，这就引出了两者在数据上的区别，例如UserInfo和User，对于一个getUser方法来说，本质上它永远不应该返回用户的密码，因此UserInfo至少比User少一个password的数据。而在领域驱动设计中，正如第一篇系列文章所说，DO不是简单的POJO，它具有领域业务逻辑。</span></p>
<p style=""></p>
<h3 style="" id="dto%E4%B8%8Edo%E7%9A%84%E5%BA%94%E7%94%A8">DTO与DO的应用</h3>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">从上一节的例子中，细心的读者可能会发现问题：既然getUser方法返回的UserInfo不应该包含password，那么就不应该存在password这个属性定义，但如果同时有一个createUser的方法，传入的UserInfo需要包含用户的password，怎么办？</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">在设计层面，展示层向服务层传递的DTO与服务层返回给展示层的DTO在概念上是不同的，但在实现层面，我们通常很少会这样做（定义两个UserInfo，甚至更多），因为这样做并不见得很明智，我们完全可以设计一个完全兼容的DTO，在服务层接收数据的时候，不该由展示层设置的属性（如订单的总价应该由其单价、数量、折扣等决定），无论展示层是否设置，服务层都一概忽略，而在服务层返回数据时，不该返回的数据（如用户密码），就不设置对应的属性。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">对于DO来说，还有一点需要说明：为什么不在服务层中直接返回DO呢？这样可以省去DTO的编码和转换工作，原因如下：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">两者在本质上的区别可能导致彼此并不一一对应，一个DTO可能对应多个DO，反之亦然，甚至两者存在多对多的关系。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">DO具有一些不应该让展示层知道的数据</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">DO具有业务方法，如果直接把DO传递给展示层，展示层的代码就可以绕过服务层直接调用它不应该访问的操作，对于基于AOP拦截服务层来进行访问控制的机制来说，这问题尤为突出，而在展示层调用DO的业务方法也会因为事务的问题，让事务难以控制。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">对于某些ORM框架（如Hibernate）来说，通常会使用“延迟加载”技术，如果直接把DO暴露给展示层，对于大部分情况，展示层不在事务范围之内（Open session in view在大部分情况下不是一种值得推崇的设计），如果其尝试在Session关闭的情况下获取一个未加载的关联对象，会出现运行时异常（对于Hibernate来说，就是LazyInitiliaztionException）。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">从设计层面来说，展示层依赖于服务层，服务层依赖于领域层，如果把DO暴露出去，就会导致展示层直接依赖于领域层，这虽然依然是单向依赖，但这种跨层依赖会导致不必要的耦合。</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">对于DTO来说，也有一点必须进行说明，就是DTO应该是一个“扁平的二维对象”，举个例子来说明：如果User会关联若干个其他实体（例如Address、Account、Region等），那么getUser()返回的UserInfo，是否就需要把其关联的对象的DTO都一并返回呢？</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">如果这样的话，必然导致数据传输量的大增，对于分布式应用来说，由于涉及数据在网络上的传输、序列化和反序列化，这种设计更不可接受。如果getUser除了要返回User的基本信息外，还需要返回一个AccountId、AccountName、RegionId、RegionName，那么，请把这些属性定义到UserInfo中，把一个“立体”的对象树“压扁”成一个“扁平的二维对象”，笔者目前参与的项目是一个分布式系统，该系统不管三七二十一，把一个对象的所有关联对象都转换为相同结构的DTO对象树并返回，导致性能非常的慢。</span></p>
<p style="text-align: justify; "></p>
<h3 style="" id="do%E4%B8%8Epo%E7%9A%84%E5%8C%BA%E5%88%AB">DO与PO的区别</h3>
<p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">DO和PO在绝大部分情况下是一一对应的，PO是只含有get/set方法的POJO，但某些场景还是能反映出两者在概念上存在本质的区别：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">DO在某些场景下不需要进行显式的持久化，例如利用策略模式设计的商品折扣策略，会衍生出折扣策略的接口和不同折扣策略实现类，这些折扣策略实现类可以算是DO，但它们只驻留在静态内存，不需要持久化到持久层，因此，这类DO是不存在对应的PO的。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">同样的道理，某些场景下，PO也没有对应的DO，例如老师Teacher和学生Student存在多对多的关系，在关系数据库中，这种关系需要表现为一个中间表，也就对应有一个TeacherAndStudentPO的PO，但这个PO在业务领域没有任何现实的意义，它完全不能与任何DO对应上。这里要特别声明，并不是所有多对多关系都没有业务含义，这跟具体业务场景有关，例如：两个PO之间的关系会影响具体业务，并且这种关系存在多种类型，那么这种多对多关系也应该表现为一个DO，又如：“角色”与“资源”之间存在多对多关系，而这种关系很明显会表现为一个DO——“权限”。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">某些情况下，为了某种持久化策略或者性能的考虑，一个PO可能对应多个DO，反之亦然。例如客户Customer有其联系信息Contacts，这里是两个一对一关系的DO，但可能出于性能的考虑（极端情况，权作举例），为了减少数据库的连接查询操作，把Customer和Contacts两个DO数据合并到一张数据表中。反过来，如果一本图书Book，有一个属性是封面cover，但该属性是一副图片的二进制数据，而某些查询操作不希望把cover一并加载，从而减轻磁盘IO开销，同时假设ORM框架不支持属性级别的延迟加载，那么就需要考虑把cover独立到一张数据表中去，这样就形成一个DO对应对个PO的情况。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">PO的某些属性值对于DO没有任何意义，这些属性值可能是为了解决某些持久化策略而存在的数据，例如为了实现“乐观锁”，PO存在一个version的属性，这个version对于DO来说是没有任何业务意义的，它不应该在DO中存在。同理，DO中也可能存在不需要持久化的属性。</span></p></li>
</ul>
<p style=""></p>
<h3 style="" id="do%E4%B8%8Epo%E7%9A%84%E5%BA%94%E7%94%A8">DO与PO的应用</h3>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">由于ORM框架的功能非常强大而大行其道，而且JavaEE也推出了JPA规范，现在的业务应用开发，基本上不需要区分DO与PO，PO完全可以通过JPA，Hibernate Annotations/hbm隐藏在DO之中。虽然如此，但有些问题我们还必须注意：</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">对于DO中不需要持久化的属性，需要通过ORM显式的声明，如：在JPA中，可以利用@Transient声明。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">对于PO中为了某种持久化策略而存在的属性，例如version，由于DO、PO合并了，必须在DO中声明，但由于这个属性对DO是没有任何业务意义的，需要让该属性对外隐藏起来，最常见的做法是把该属性的get/set方法私有化，甚至不提供get/set方法，但对于Hibernate来说，这需要特别注意，由于Hibernate从数据库读取数据转换为DO时，是利用反射机制先调用DO的空参数构造函数构造DO实例，然后再利用JavaBean的规范反射出set方法来为每个属性设值，如果不显式声明set方法，或把set方法设置为private，都会导致Hibernate无法初始化DO，从而出现运行时异常，可行的做法是把属性的set方法设置为protected。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: inherit">对于一个DO对应多个PO，或者一个PO对应多个DO的场景，以及属性级别的延迟加载，Hibernate都提供了很好的支持，请参考Hibnate的相关资料。</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">到目前为止，相信大家都已经比较清晰的了解VO、DTO、DO、PO的概念、区别和实际应用了。通过上面的详细分析，我们还可以总结出一个原则：</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><strong><span style="font-size: 15px; color: rgb(62, 62, 62)">分析设计层面和实现层面完全是两个独立的层面，即使实现层面通过某种技术手段可以把两个完全独立的概念合二为一，在分析设计层面，我们仍然（至少在头脑中）需要把概念上独立的东西清晰的区分开来，这个原则对于做好分析设计非常重要（工具越先进，往往会让我们越麻木）。</span></strong></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">第一篇系列博文抛砖引玉，大唱领域驱动设计的优势，但其实领域驱动设计在现实环境中还是有种种的限制，需要选择性的使用，正如我在《田七的智慧》博文中提到，我们不能永远的理想化的去选择所谓“最好的设计”，在必要的情况下，我们还是要敢于放弃，因为最合适的设计才是最好的设计。</span></p>
<p style="text-align: justify; "></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://www.blogjava.net/johnnylzb/archive/2010/05/26/321873.html">田七的智慧</a></p>
<p style="">我一直很喜欢攀藤的植物，之前在家里的阳台种了不少田七，偶然发现一个奇怪的问题，一些新长出来的嫩枝，过了几天突然凋谢了，一直不知道为什么，直到最近我找到了答案。<br>
  来到**城后，新“家”有个很大的窗，外面是防盗网，于是从家里带来了两颗田七苗，由于春天和向南的缘故，两颗小苗长得很快，每天都见它们向上攀爬了一大 截。两天前，发现它们爬到防盗网顶了，防盗网顶部是封闭的，它们没有了向上发展的空间，长出一截吊在半空。今晚浇水的时候，我突然发现吊在半空的嫩枝都凋 谢了，我终于明白了原因。田七非常聪明，当发现“前面是绝路”的时候，它会很明智的选择放弃。种过田七的人会知道，田七虽然是攀藤植物，但它是可以长出分 支的，而有些种植常识的人会知道，把生长过长的枝茎进行修剪，会让养分更充足，从而促进主茎中长出分支，形成更好的植株形状。我相信田七“放弃”的原因也 正在于此，相信很快它们就会长出分支来。</p>
<p style="">这个小小的发现其实蕴藏着一种显浅的生活智慧，这种智慧对于很多生物都是与生俱来的，但对于人来说，反而很多人都不懂，又或者做不到。</p>
<p style="">借用我的前总监经常告诫我的一句话告诫大家：“设计其实是一种选择，没有最好的设计，只有最合适的设计。”</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&amp;amp;mid=2247506981&amp;amp;idx=1&amp;amp;sn=ee95ace609e6575fc953693941875e81&amp;amp;chksm=fba53426ccd2bd3006417783360277fe155cd406b0141c6638a387b1874889bd488a1e70be22&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0818fvVs6I5NVTla5RXDsyOp&amp;amp;sharer_sharetime=1629239489496&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd">http://www.blogjava.net/johnnylzb/archive/2010/05/26/321873.html</a></p>]]></description><guid isPermaLink="false">/archives/qian-xi-vo-dto-do-po-de-gai-nian-qu-bie-he-yong-chu</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fpodtovo.jpeg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:10:00 GMT</pubDate></item><item><title><![CDATA[记一次性能优化，单台 4 核 8G 机器支撑 5 万 QPS]]></title><link>https://xiaoming728.com/archives/ji-yi-ci-xing-neng-you-hua-dan-tai-4-he-8g-ji-qi-zhi-cheng-5-wan-qps</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E8%AE%B0%E4%B8%80%E6%AC%A1%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%EF%BC%8C%E5%8D%95%E5%8F%B0%204%20%E6%A0%B8%208G%20%E6%9C%BA%E5%99%A8%E6%94%AF%E6%92%91%205%20%E4%B8%87%20QPS&amp;url=/archives/ji-yi-ci-xing-neng-you-hua-dan-tai-4-he-8g-ji-qi-zhi-cheng-5-wan-qps" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：segmentfault-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://segmentfault.com/u/leoython_5c50fb3dea65d">Leoython</a></p>
<p style="">发布：2019-01-31</p>
<p style="">连接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://segmentfault.com/u/leoython_5c50fb3dea65d">https://segmentfault.com/a/1190000018075241</a></p>
<h3 style="" id="%E5%89%8D%E8%A8%80"><span fontsize="" color="rgb(33, 37, 41)" style="color: rgb(33, 37, 41)">前言</span></h3>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">这篇文章的主题是记录一次Python程序的性能优化，在优化的过程中遇到的问题，以及如何去解决的。为大家提供一个优化的思路，首先要声明的一点是，我的方式不是唯一的，大家在性能优化之路上遇到的问题都绝对不止一个解决方案。</span></p>
<h3 style="" id="%E5%A6%82%E4%BD%95%E4%BC%98%E5%8C%96"><span fontsize="" color="rgb(33, 37, 41)" style="color: rgb(33, 37, 41)">如何优化</span></h3>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">首先大家要明确的一点是，脱离需求谈优化都是耍流氓，所以有谁跟你说在xx机器上实现了百万并发，基本上可以认为是不懂装懂了，单纯的并发数完全是无意义的。其次，我们优化之前必须要有一个目标，需要优化到什么程度，没有明确目标的优化是不可控的。再然后，我们必须明确的找出性能瓶颈在哪里，而不能漫无目的的一通乱搞。</span></p>
<h3 style="" id="%E9%9C%80%E6%B1%82%E6%8F%8F%E8%BF%B0"><span fontsize="" color="rgb(33, 37, 41)" style="color: rgb(33, 37, 41)">需求描述</span></h3>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">这个项目是我在上家公司负责一个单独的模块，本来是集成在主站代码中的，后来因为并发太大，为了防止出现问题后拖累主站服务，所有由我一个人负责拆分出来。对这个模块的拆分要求是，压力测试QPS不能低于3万，数据库负责不能超过50%，服务器负载不能超过70%, 单次请求时长不能超过70ms，错误率不能超过5%。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">环境的配置如下:</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">服务器：4核8G内存，centos7系统，ssd硬盘</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">数据库：Mysql5.7，最大连接数800</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">缓存: redis, 1G容量。</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">以上环境都是购买自腾讯云的服务。</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">压测工具：locust，使用腾讯的弹性伸缩实现分布式的压测。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">需求描述如下：</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">用户进入首页，从数据库中查询是否有合适的弹窗配置，如果没有，则继续等待下一次请求、如果有合适的配置，则返回给前端。这里开始则有多个条件分支，如果用户点击了弹窗，则记录用户点击，并且在配置的时间内不再返回配置，如果用户未点击，则24小时后继续返回本次配置，如果用户点击了，但是后续没有配置了，则接着等待下一次。</span></p>
<h3 style="" id="%E9%87%8D%E7%82%B9%E5%88%86%E6%9E%90"><span fontsize="" color="rgb(33, 37, 41)" style="color: rgb(33, 37, 41)">重点分析</span></h3>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">根据需求，我们知道了有几个重要的点：</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">1、需要找出合适用户的弹窗配置，</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">2、需要记录用户下一次返回配置的时间并记录到数据库中，</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">3、需要记录用户对返回的配置执行了什么操作并记录到数据库中。</span></p>
<h3 style="" id="%E8%B0%83%E4%BC%98"><span fontsize="" color="rgb(33, 37, 41)" style="color: rgb(33, 37, 41)">调优</span></h3>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">我们可以看到，上述三个重点都存在数据库的操作，不只有读库，还有写库操作。从这里我们可以看到如果不加缓存的话，所有的请求都压到数据库，势必会占满全部连接数，出现拒绝访问的错误，同时因为sql执行过慢，导致请求无法及时返回。所以，我们首先要做的就是讲写库操作剥离开来，提升每一次请求响应速度，优化数据库连接。整个系统的架构图如下：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1621415657337-2a678270-13e0-4df4-9035-3801cbbdf7c8.png&amp;size=m" width="717" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">将写库操作放到一个先进先出的消息队列中来做，为了减少复杂度，使用了redis的list来做这个消息队列。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">然后进行压测，结果如下：</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">QPS在6000左右502错误大幅上升至30%，服务器cpu在60%-70%之间来回跳动，数据库连接数被占满tcp连接数为6000左右，很明显，问题还是出在数据库，经过排查sql语句，查询到原因就是找出合适用户的配置操作时每次请求都要读取数据库所导致的连接数被用完。因为我们的连接数只有800，一旦请求过多，势必会导致数据库瓶颈。好了，问题找到了，我们继续优化，更新的架构如下</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1621415675905-d68ed200-4d3b-4c56-ba13-8d29f08adfaf.png&amp;size=m" width="701" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">我们将全部的配置都加载到缓存中，只有在缓存中没有配置的时候才会去读取数据库。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">接下来我们再次压测，结果如下：QPS压到2万左右的时候就上不去了，服务器cpu在60%-80%之间跳动，数据库连接数为300个左右，每秒tpc连接数为1.5万左右。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">这个问题是困扰我比较久的一个问题，因为我们可以看到，我们2万的QPS，但是tcp连接数却并没有达到2万，我猜测，tcp连接数就是引发瓶颈的问题，但是因为什么原因所引发的暂时无法找出来。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">这个时候猜测，既然是无法建立tcp连接，是否有可能是服务器限制了socket连接数，验证猜测，我们看一下，在终端输入ulimit -n命令，显示的结果为65535，看到这里，觉得socket连接数并不是限制我们的原因，为了验证猜测，将socket连接数调大为100001.</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">再次进行压测，结果如下：</span></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">QPS压到2.2万左右的时候就上不去了，服务器cpu在60%-80%之间跳动，数据库连接数为300个左右，每秒tpc连接数为1.7万左右。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">虽然有一点提升，但是并没有实质性的变化，接下来的几天时间，我发现都无法找到优化的方案，那几天确实很难受，找不出来优化的方案，过了几天，再次将问题梳理了一遍，发现，虽然socket连接数足够，但是并没有全部被用上，猜测，每次请求过后，tcp连接并没有立即被释放，导致socket无法重用。经过查找资料，找到了问题所在：</span></p>
<pre><code>tcp链接在经过四次握手结束连接后并不会立即释放，而是处于timewait状态，会等待一段时间，以防止客户端后续的数据未被接收。</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">好了，问题找到了，我们要接着优化，首先想到的就是调整tcp链接结束后等待时间，但是linux并没有提供这一内核参数的调整，如果要改，必须要自己重新编译内核，幸好还有另一个参数net.ipv4.tcp_max_tw_buckets， timewait 的数量，默认是 180000。我们调整为6000，然后打开timewait快速回收，和开启重用，完整的参数优化如下</span></p>
<pre><code>#timewait 的数量，默认是 180000。
net.ipv4.tcp_max_tw_buckets = 6000

net.ipv4.ip_local_port_range = 1024 65000

#启用 timewait 快速回收。
net.ipv4.tcp_tw_recycle = 1

#开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接。
net.ipv4.tcp_tw_reuse = 1</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">我们再次压测，结果显示：QPS5万，服务器cpu70%,数据库连接正常，tcp连接正常，响应时间平均为60ms，错误率为0%。</span></p>
<h3 style="" id="%E7%BB%93%E8%AF%AD"><span fontsize="" color="rgb(33, 37, 41)" style="color: rgb(33, 37, 41)">结语</span></h3>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">到此为止，整个服务的开发、调优、和压测就结束了。回顾这一次调优，得到了很多经验，最重要的是，深刻理解了web开发不是一个独立的个体，而是网络、数据库、编程语言、操作系统等多门学科结合的工程实践，这就要求web开发人员有牢固的基础知识，否则出现了问题还不知道怎么分析查找。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(33, 37, 41)">ps：服务端开启了 tcp_tw_recycle 和 tcp_tw_reuse是会导致一些问题的，我们为了优化选择牺牲了一部分，获得另一部分，这也是我们要明确的，具体的问题可以查看耗子叔的文章</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://coolshell.cn/articles/11564.html">TCP 的那些事儿（上）</a></p>]]></description><guid isPermaLink="false">/archives/ji-yi-ci-xing-neng-you-hua-dan-tai-4-he-8g-ji-qi-zhi-cheng-5-wan-qps</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E4%25BC%2598%25E5%258C%2596.jpg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:10:00 GMT</pubDate></item><item><title><![CDATA[优秀的代码都是如何分层的]]></title><link>https://xiaoming728.com/archives/you-xiu-de-dai-ma-du-shi-ru-he-fen-ceng-de</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E4%BC%98%E7%A7%80%E7%9A%84%E4%BB%A3%E7%A0%81%E9%83%BD%E6%98%AF%E5%A6%82%E4%BD%95%E5%88%86%E5%B1%82%E7%9A%84&amp;url=/archives/you-xiu-de-dai-ma-du-shi-ru-he-fen-ceng-de" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1%E3%80%81%E8%83%8C%E6%99%AF">1、背景</h3>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">说起应用分层，大部分人都会认为这个不是很简单嘛 就controller，service, mapper三层。看起来简单，很多人其实并没有把他们职责划分开，在很多代码中,controller做的逻辑比service还多,service往往当成透传了，这其实是很多人开发代码都没有注意到的地方，反正功能也能用，至于放哪无所谓呗。这样往往造成后面代码无法复用，层级关系混乱，对后续代码的维护非常麻烦。</span></p>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">的确在这些人眼中分层只是一个形式，前辈们的代码这么写的，其他项目代码这么写的，那么我也这么跟着写。但是在真正的团队开发中每个人的习惯都不同，写出来的代码必然带着自己的标签，有的人习惯controller写大量的业务逻辑，有的人习惯在service中之间调用远程服务，这样就导致了每个人的开发代码风格完全不同，后续其他人修改的时候，一看，我靠这个人写的代码和我平常的习惯完全不同，修改的时候到底是按着自己以前的习惯改，还是跟着前辈们走，这又是个艰难的选择，选择一旦有偏差，你的后辈又维护你的代码的时候，恐怕就要骂人了。</span></p>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">所以一个好的应用分层需要具备以下几点:&nbsp;</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">方便后续代码进行维护扩展；</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">分层的效果需要让整个团队都接受；</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">各个层职责边界清晰。</span></p></li>
</ul>
<p style=""></p>
<h3 style="" id="2%E3%80%81%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E5%88%86%E5%B1%82">2、如何进行分层</h3>
<h4 style="" id="2.1%E3%80%81%E9%98%BF%E9%87%8C%E8%A7%84%E8%8C%83">2.1、阿里规范</h4>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">在阿里的编码规范中约束的分层如下:</span></p>
<p style="text-align: justify; "></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2020%2Fpng%2F550960%2F1579240119219-b14f8940-1047-4e95-85e0-61607dbd4209.png&amp;size=m" width="642" style="display: inline-block"></p>
<p style=""></p>
<p style="text-align: justify; "><strong>开放接口层</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">：可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行 网关安全控制、流量控制等。</span></p>
<p style="text-align: justify; "><strong>终端显示层</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">：各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染，JS 渲染， JSP 渲染，移动端展示等。</span></p>
<p style="text-align: justify; "><strong>Web 层</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">：主要是对访问控制进行转发，各类基本参数校验，或者不复用的业务简单处理等。</span></p>
<p style="text-align: justify; "><strong>Service 层</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">：相对具体的业务逻辑服务层。</span></p>
<p style="text-align: justify; "><strong>Manager 层</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">：通用业务处理层，它有如下特征:1. 对第三方平台封装的层，预处理返回结果及转化异常信息;2. 对Service层通用能力的下沉，如缓存方案、中间件通用处理;3. 与DAO层交互，对多个DAO的组合复用。</span></p>
<p style="text-align: justify; "><strong>DAO 层</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">：数据访问层，与底层 MySQL、Oracle、Hbase 进行数据交互。</span></p>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">阿里巴巴规约中的分层比较清晰简单明了，但是描述得还是过于简单了，以及service层和manager层有很多同学还是有点分不清楚之间的关系，就导致了很多项目中根本没有Manager层的存在。下面介绍一下具体业务中应该如何实现分层。</span></p>
<p style="text-align: justify; "></p>
<h4 style="" id="2.2%E3%80%81%E4%BC%98%E5%8C%96%E5%88%86%E5%B1%82">2.2、优化分层</h4>
<p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">从我们的业务开发中总结了一个较为的理想模型,这里要先说明一下由于我们的rpc框架选用的是thrift可能会比其他的一些rpc框架例如dubbo会多出一层,作用和controller层类似</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2020%2Fpng%2F550960%2F1579240188652-df2efeb7-6207-4eaa-bbfb-2fc717b13a29.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)"><br>
  最上层controller和TService是我们阿里分层规范里面的第一层：</span><strong>轻业务逻辑，参数校验，异常兜底</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">。通常这种接口可以轻易更换接口类型，所以业务逻辑必须要轻，甚至不做具体逻辑。</span></p>
<p style=""></p>
<p style=""><strong>Service</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">：业务层，复用性较低，这里推荐每一个controller方法都得对应一个service,不要把业务编排放在controller中去做，为什么呢？如果我们把业务编排放在controller层去做的话，如果以后我们要接入thrift,我们这里又需要把业务编排在做一次，这样会导致我们每接入一个入口层这个代码都得重新复制一份如下图所示:&nbsp;</span></p>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2020%2Fpng%2F550960%2F1579240241788-175d4522-4214-4226-ab92-8935a9cc3898.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""></p>
<p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">这样大量的重复工作必定会导致我们开发效率下降，所以我们需要把业务编排逻辑都得放进service中去做:&nbsp;</span></p>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2020%2Fpng%2F550960%2F1579240255746-25f0ece4-7f7e-44d7-817e-4746b1099a60.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="text-align: justify; "><strong>Mannager</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">：可复用逻辑层。这里的Mannager可以是单个服务的，比如我们的cache,mq等等，当然也可以是复合的，当你需要调用多个Mannager的时候，这个可以合为一个Mannager，比如逻辑上的连表查询等。如果是httpMannager或rpcMannager需要在这一层做一些数据转换</span></p>
<p style="text-align: justify; "><strong>DAO</strong><span style="font-size: 14px; color: rgb(79, 79, 79)">：数据库访问层。主要负责“操作数据库的某张表，映射到某个java对象”，dao应该只允许自己的Service访问，其他Service要访问我的数据必须通过对应的Service。</span></p>
<p style="text-align: justify; "></p>
<h3 style="" id="3%E3%80%81%E5%88%86%E5%B1%82%E9%A2%86%E5%9F%9F%E6%A8%A1%E5%9E%8B%E7%9A%84%E8%BD%AC%E6%8D%A2">3、分层领域模型的转换</h3>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">在阿里巴巴编码规约中列举了下面几个领域模型规约:&nbsp;</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">DO（Data Object）：与数据库表结构一一对应，通过DAO层向上传输数据源对象。&nbsp;</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">DTO（Data Transfer Object）：数据传输对象，Service或Manager向外传输的对象。&nbsp;</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">BO（Business Object）：业务对象。由Service层输出的封装业务逻辑的对象。&nbsp;</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">AO（Application Object）：应用对象。在Web层与Service层之间抽象的复用对象模型，极为贴近展示层，复用度不高。&nbsp;</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">VO（View Object）：显示层对象，通常是Web向模板渲染引擎层传输的对象。&nbsp;</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">Query：数据查询对象，各层接收上层的查询请求。注意超过2个参数的查询封装，禁止使用Map类来传输。</span></p></li>
</ul>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2020%2Fpng%2F550960%2F1579240347588-7a3b66e8-a285-46ed-90f4-003cf8eb3d0f.png&amp;size=m" width="876" style="display: inline-block"></p>
<p style=""></p>
<p style=""><span style="font-size: 14px; color: rgb(79, 79, 79)">每一个层基本都自己对应的领域模型，这样就导致了有些人过于追求每一层都是用自己的领域模型，这样就导致了一个对象可能会出现3次甚至4次转换在一次请求中，当返回的时候同样也会出现3-4次转换，这样有可能一次完整的请求-返回会出现很多次对象转换。如果在开发中真的按照这么来，恐怕就别写其他的了，一天就光写这个重复无用的逻辑算了吧。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><strong>所以我们得采取一个折中的方案:&nbsp;</strong></p>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">1、允许Service/Manager可以操作数据领域模型，对于这个层级来说，本来自己做的工作也是做的是业务逻辑处理和数据组装。&nbsp;</span></p>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">2、Controller/TService层的领域模型不允许传入DAO层，这样就不符合职责划分了。&nbsp;</span></p>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">3、同理，不允许DAO层的数据传入到Controller/TService。</span></p>
<p style="text-align: justify; "></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2020%2Fpng%2F550960%2F1579240376164-16cb6386-a4cc-4287-ace0-e209f6f435ca.png&amp;size=m" width="919" style="display: inline-block"></p>
<h3 style="" id="4%E3%80%81%E6%80%BB%E7%BB%93">4、总结</h3>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">总的来说业务分层对于代码规范是比较重要，决定着以后的代码是否可复用，是否职责清晰，边界清晰。</span></p>
<p style="text-align: justify; "><span style="font-size: 14px; color: rgb(79, 79, 79)">当然这种分层其实见仁见智, 团队中的所有人的分层习惯也不同，所以很难权衡出一个标准的准则，总的来说只要满足职责逻辑清晰，后续维护容易，就是好的分层。</span></p>]]></description><guid isPermaLink="false">/archives/you-xiu-de-dai-ma-du-shi-ru-he-fen-ceng-de</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E4%25BB%25A3%25E7%25A0%2581%25E5%2588%2586%25E5%25B1%2582.jpg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:09:00 GMT</pubDate></item><item><title><![CDATA[通过现实例子显示领域驱动设计的威力]]></title><link>https://xiaoming728.com/archives/tong-guo-xian-shi-li-zi-xian-shi-ling-yu-qu-dong-she-ji-de-wei-li</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E9%80%9A%E8%BF%87%E7%8E%B0%E5%AE%9E%E4%BE%8B%E5%AD%90%E6%98%BE%E7%A4%BA%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E7%9A%84%E5%A8%81%E5%8A%9B&amp;url=/archives/tong-guo-xian-shi-li-zi-xian-shi-ling-yu-qu-dong-she-ji-de-wei-li" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://www.blogjava.net/johnnylzb/">Johnny.Liang</a></p>
<p style="">时间：2010-05-15 21:58</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="http://www.blogjava.net/johnnylzb/">http://www.blogjava.net/johnnylzb/archive/2010/05/15/321057.html</a></p>
<p style=""></p>
<p style="">曾经参与过系统维护或是在现有系统中进行迭代开发的软件工程师们，你们是否有过这样的痛苦经历：当需要修改一个Bug的时候，面对一个类中成百上千行的代码，没有注释，千奇百怪的方法和变量名字，层层嵌套的方法调用，混乱不堪的结构，不要说准确找到Bug所在的位置，就是要清晰知道一段代码究竟是做了什么也非常困难，最终，改对了一个Bug，却多冒出N个新Bug；同样的情况，当你拿到一份新的需求，需要在现有系统中添加功能的时候，面对一行行完全过程式的代码，需要使用一个功能时，不知道是应该自己编写，还是应该寻找是否已经存在的方法，编写一个非常简单的新、删、改功能，却要费尽九牛二虎之力，最终发现，系统存在着太多的重复逻辑，阅读、测试、修改非常困难。在经历了这些痛苦之后，你们是否会不约而同的发出一个感慨：与其进行系统维护和迭代开发，还不如重新设计开发一个新的系统来得痛快？</p>
<p style=""></p>
<p style="">面对这一系列让软件嵌入无底泥潭的问题，基于面向对象思想的领域驱动设计方法是一个很好的解决方法。从事过系统设计的富有经验的设计师们，对职责单一原则、信息专家、充血/贫血模型、模型驱动设计这些名词或概念应该不会感到陌生。面向对象的设计大师Martin Fowler不止一次的在他的Blog和著作《企业应用架构模式》中倡导过上述概论在设计中的巨大威力，而另外一位领域模型的出色专家Eric Evans的著作《领域驱动设计》也为我们提供了不少宝贵的经验和方法。</p>
<p style=""></p>
<p style="">笔者从事系统设计多年，将会在本系列文章中把本人对领域驱动设计的理解，结合工作过程中积累的实际项目经验进行浅析，希望与大家交流学习。</p>
<p style=""></p>
<p style="">在本系列博文的开篇中，我将会拿出一个显示的例子，先用传统的面向过程方式，使用贫血模型进行设计，然后再逐步加入需求变更，让读者发现，随着系统的不断变更，基于贫血模型的设计将会让系统慢慢陷入泥潭，越来越难于维护，然后再用基于面向对象的领域驱动设计重新上述过程，通过对比展示领域驱动设计对于复杂的业务系统的威力。<br></p>
<p style="">假设现在有一个银行支付系统项目，其中的一个重要的业务用例是账户转账业务。系统使用迭代的方式进行开发，在1.0版本中，该用例的功能需求非常简单，事件流描述如下：</p>
<p style="">主事件流：</p>
<p style="">1） 用户登录银行的在线支付系统</p>
<p style="">2） 选择用户在该银行注册的网上银行账户</p>
<p style="">3） 选择需要转账的目标账户，输入转账金额，申请转账</p>
<p style="">4） 银行系统检查转出账户的金额是否足够</p>
<p style="">5） 从转出账户中扣除转出金额（debit），更新转出账户的余额</p>
<p style="">6） 把转出金额加入到转入账户中（credit），更新转入账户的余额</p>
<p style="">备选事件流：</p>
<p style="">4a）如果转出账户中的余额不足，转账失败，返回错误信息</p>
<p style=""></p>
<h3 style="" id="%E9%9D%A2%E5%90%91%E8%BF%87%E7%A8%8B%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E5%BC%8F%EF%BC%88%E8%B4%AB%E8%A1%80%E6%A8%A1%E5%9E%8B%EF%BC%89">面向过程的设计方式（贫血模型）</h3>
<p style="">设计方案如下（忽略展示层部分）：</p>
<p style="">1） 设计一个账户交易服务接口AccountingService，设计一个服务方法transfer()，并提供一个具体实现类AccountingServiceImpl，所有账户交易业务的业务逻辑都置于该服务类中。</p>
<p style="">2） 提供一个AccountInfo和一个Account，前者是一个用于与展示层交换账户数据的账户数据传输对象，后者是一个账户实体（相当于一个EntityBean），这两个对象都是普通的JavaBean，具有相关属性和简单的get/set方法。<br>
 下面是AccountingServiceImpl.transfer()方法的实现逻辑（伪代码）：</p>
<pre><code>public class AccountingServiceImpl implements AccountingService {

       public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException {

              Account srcAccount = accountRepository.getAccount(srcAccountId);

              Account destAccount = accountRepository.getAccount(destAccountId);

              if(srcAccount.getBalance().compareTo(amount)&lt;0)

                     throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH);

              srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));

              destAccount.setBalance(destAccount.getBalance().add(amount));

       }
}

 
public class Account implements DomainObject {

       private Long id;

       private Bigdecimal balance;  

/**
 * getter/setter
 */

}</code></pre>
<p style="">可以看到，由于1.0版本的功能需求非常简单，按面向过程的设计方式，把所有业务代码置于AccountingServiceImpl中完全没有问题。</p>
<p style=""></p>
<p style="">这时候，新需求来了，在1.0.1版本中，需要为账户转账业务增加如下功能，在转账时，首先需要判断账户是否可用，然后，账户的余额还要分成两部分：冻结部分和活跃部分，处于冻结部分的金额不能用于任何交易业务，我们来看看变更后的代码：</p>
<pre><code>public class AccountingServiceImpl implements AccountingService {

       public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException {

              Account srcAccount = accountRepository.getAccount(srcAccountId);

              Account destAccount = accountRepository.getAccount(destAccountId);

              if(!srcAccount.isActive() || !destAccount.isActive())

                     throw new AccountingServiceException(AccountingService.ACCOUNT_IS_NOT_AVAILABLE);

              BigDecimal availableAmount = srcAccount.getBalance().substract(srcAccount.getFrozenAmount());

              if(availableAmount.compareTo(amount)&lt;0)

                     throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH);

              srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));

              destAccount.setBalance(destAccount.getBalance().add(amount));
       }
}

public class Account implements DomainObject {

       private Long id;

       private BigDecimal balance;

       private BigDecimal frozenAmount;

        /**
         * getter/setter
         */
}</code></pre>
<p style="">可以看到，情况变得稍微复杂了，这时候，1.0.2的需求又来了，需要在每次交易成功后，创建一个交易明细账，于是，我们又必须在transfer()方面里面增加创建并持久化交易明细账的业务逻辑：</p>
<pre><code>AccountTransactionDetails details= new AccountTransactionDetails(…);
accountRepository.save(details);</code></pre>
<p style="">业务需求不断复杂化：账户每笔转账的最大额度需要由其信用指数确定、需要根据银行的手续费策略计算并扣除一定的手续费用……，随着业务的复杂化，transfer()方法的逻辑变得越来越复杂，逐渐形成了上文所述的成百上千行代码。</p>
<p style="">有经验的程序员可能会做出类此“方法抽取”的重构，把转账业务按逻辑划分成若干块：判断余额是否足够、判断账户的信用指数以确定每笔最大转账金额、根据银行的手续费策略计算手续费、记录交易明细账……，从而使代码更加结构化。这是一个好的开始，但还是显然不足。</p>
<p style=""></p>
<p style="">假设某一天，系统需求增加一个新的模块，为系统增加一个网上商城，让银行用户可以进行在线购物，而在线购物也存在着很多与账户贷记借记业务相同或相似的业务逻辑：判断余额是否足够、对账户进行借贷操作（credit/debit）以改变余额、收取手续费用、产生交易明细账……</p>
<p style=""></p>
<p style="">面对这种情况，有两种解决办法：</p>
<p style="">1） 把AccountingServiceImpl中的相同逻辑拷贝到OnlineShoppingServiceImplementation中</p>
<p style="">2） 让OnlineShoppingServiceImpl调用AccountingServiceImpl的相同服务</p>
<p style="">显然，第二种方法比第一种方法更好，结构更清晰，维护更容易。但问题在于，这样就会形成网上商城服务模块与账户收支服务模块的不必要的依赖关系，系统的耦合度高了，如果系统为了更灵活的伸缩性，让每个大业务模块独立进行部署，还需要因为两者的依赖关系建立分布式调用，这无疑增加了设计、开发和运维的成本。</p>
<p style=""></p>
<p style="">有经验的设计人员可能会发现第三种解决办法：把相同的业务逻辑抽取成一个新的服务，作为公共服务同时供上述两个业务模块使用。这只是笔者将会马上讨论的方案——使用领域驱动设计。</p>
<p style=""></p>
<h3 style="" id="%E9%9D%A2%E5%90%91%E8%BF%87%E7%A8%8B%E7%9A%84%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E6%96%B9%E5%BC%8F%EF%BC%88%E5%85%85%E8%A1%80%E6%A8%A1%E5%9E%8B%EF%BC%89">面向过程的领域驱动设计方式（充血模型）</h3>
<p style="">为了节省篇幅，这里就直接以最复杂的业务需求来进行设计。</p>
<p style="">领域驱动设计的一个重要的概念是领域模型，首先，我们根据业务领域抽象出以下核心业务对象模型：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1629340562730-0493b045-9c40-4dd8-939e-854b7e5fe510.png&amp;size=m" width="979" style="display: inline-block"></p>
<p style="">Account：账户，是整个系统的最核心的业务对象，它包括以下属性：对象标识、账户号、是否有效标识、余额、冻结金额、账户交易明细集合、账户信用等级。</p>
<p style="">AccountTransactionDetails：账户交易明细，它从属于账户，每个账户有多个交易明细，它包括以下属性：对象标识、所属账户、交易类型、交易发生金额、交易发生时间。</p>
<p style="">AccountCreditDegree：账户信用等级，它用于限制账户的每笔交易发生金额，包含以下属性：对象标识、对应账户、信用指数。</p>
<p style="">BankTransactionFeeCalculator：银行交易手续费用计算器，它包含一个常量：每笔交易的手续费上限。</p>
<p style=""></p>
<p style="">我们知道，领域对象除了具有自身的属性和状态之外，它的一个很重要的标志是，它具有属于自己职责范围之内的行为，这些行为封装了其领域内的领域业务逻辑。于是，我们进行进一步的建模，根据业务需求为领域对象设计业务方法：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1629340946125-0fd2fc3f-2471-43f0-a02b-25512a71ecf4.png&amp;size=m" width="1064" style="display: inline-block"></p>
<p style="">根据职责单一的原则，我们把功能需求中描述的功能合理的分配到不同的领域对象中：</p>
<p style="">Account：</p>
<ul>
 <li>
  <p style="">credit：向银行账户存入金额，贷记</p></li>
 <li>
  <p style="">debit：从银行账户划出金额，借记</p></li>
 <li>
  <p style="">transferTo：把固定金额转入指定账户</p></li>
 <li>
  <p style="">createTransactionDetails：创建交易明细账</p></li>
 <li>
  <p style="">updateCreditIndex：更新账户的信用指数</p></li>
</ul>
<p style="">（我们可以看到，后两个业务方法被声明为protected，具体原因见后述）</p>
<p style="">AccountCreditDegree：</p>
<ul>
 <li>
  <p style="">getMaxTransactionAmount：获取所属账户的每笔交易最大金额</p></li>
</ul>
<p style="">BankTransactionFeeCalculator：</p>
<ul>
 <li>
  <p style="">calculateTransactionFee：根据交易信息计算该笔交易的手续费</p></li>
</ul>
<p style=""></p>
<p style="">经过这样的设计，前例中所有放置在服务对象的业务逻辑被分别划入不同的负责相关职责的领域对象当中，下面的时序图描述了AccountingServiceImpl的转账业务的实现逻辑（为了简化逻辑，我们忽略掉事物、持久化等逻辑）：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1629340896723-9935ad71-3031-4d85-8bda-b3b971a42e09.png&amp;size=m" width="681" style="display: inline-block"></p>
<p style="">再看看AccountingServiceImpl.transfer()的实现逻辑：</p>
<pre><code>public class AccountingServiceImpl implements AccountingService {

       public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountDomainException {

              Account srcAccount = accountRepository.getAccount(srcAccountId);

              Account destAccount = accountRepository.getAccount(destAccountId);

              srcAccount.transferTo(destAccount,amount);
       }
}</code></pre>
<p style="">我们可以看到，上例那些复杂的业务逻辑：判断余额是否足够、判断账户是否可用、改变账户余额、计算手续费、判断交易额度、产生交易明细账……，都不再存在于AccountingServiceImplementation的transfer方法中，它们被委派给负责这些业务的领域对象的业务方法中去，现在应该猜到为什么Account中有两个方法被声明为protected了吧，因为他们是在debit和credit方法被调用时，由这两个方法调用的，对于AccountingServiceImpl来说，由于产生交易明细（createTransactionDetails）和更新账户信用指数（updateCreditIndex）都不属于其职责范围，它不需要也无权使用这些逻辑。</p>
<p style=""></p>
<p style="">我们可以看到，使用领域驱动设计至少会带来下述优点：</p>
<ul>
 <li>
  <p style="">业务逻辑被合理的分散到不同的领域对象中，代码结构更加清晰，可读性，可维护性更高。</p></li>
 <li>
  <p style="">对象职责更加单一，内聚度更高。</p></li>
 <li>
  <p style="">复杂的业务模型可以通过领域建模（UML是一种主要方式）清晰的表达，开发人员甚至可以在不读源码的情况下就能了解业务和系统结构，这有利于对现存的系统进行维护和迭代开发。</p></li>
</ul>
<p style=""></p>
<p style="">再看看如果这时需要加入网上商城的一个新的模块，开发人员需要怎么去做，还记得上面提过的第三种方案吗？就是把账户贷记和借记的相关业务抽取到成一个公共服务，同时供银行在线支付系统和网上商城系统服务，其实这个公共的服务，本质上就是这些具有领域逻辑的领域对象：Account、AccountCreditDegree……，由此我们又可以发现领域驱动设计的一大优点：</p>
<ul>
 <li>
  <p style="">系统高度模块化，代码重用度高，不会出现太多的重复逻辑。</p></li>
</ul>
<p style=""></p>
<p style="">笔者经验尚浅，而且文笔拙劣，希望通过这样的一个场景的分析比较，能让读者初步认识到基于面向对象的领域驱动设计的威力，并在实际项目中尝试应用。本篇是领取驱动设计系列博文的第一篇，在系列文章的第二篇博文中，笔者将会浅析VO、DTO、DO、PO的概念、用处和区别，敬请各位对本系列博文感兴趣的读者关注并给予指导修正。</p>]]></description><guid isPermaLink="false">/archives/tong-guo-xian-shi-li-zi-xian-shi-ling-yu-qu-dong-she-ji-de-wei-li</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdomain-driven-design.png&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:09:00 GMT</pubDate></item><item><title><![CDATA[ULID - 一种比UUID更好的方案]]></title><link>https://xiaoming728.com/archives/ulid---yi-zhong-bi-uuidgeng-hao-de-fang-an</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=ULID%20-%20%E4%B8%80%E7%A7%8D%E6%AF%94UUID%E6%9B%B4%E5%A5%BD%E7%9A%84%E6%96%B9%E6%A1%88&amp;url=/archives/ulid---yi-zhong-bi-uuidgeng-hao-de-fang-an" width="1" height="1" alt="" style="opacity:0;">
<p style=""><strong>ULID</strong>：Universally Unique Lexicographically Sortable Identifier（通用唯一词典分类标识符）</p>
<p style=""><strong>UUID</strong>：Universally Unique Identifier（通用唯一标识符）</p>
<h2 style="" id="%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E9%80%89%E6%8B%A9uuid">为什么不选择UUID</h2>
<p style="">UUID 目前有 5 个版本：</p>
<p style="">版本1：在许多环境中是不切实际的，因为它需要访问唯一的，稳定的MAC地址，容易被攻击；</p>
<p style="">版本2：将版本 1 的时间戳前四位换为 POSIX 的 UID 或 GID，问题同上；</p>
<p style="">版本3：基于 MD5 哈希算法生成，生成随机分布的ID需要唯一的种子，这可能导致许多数据结构碎片化；</p>
<p style="">版本4：基于随机数或伪随机数生成，除了随机性外没有提供其他信息；</p>
<p style="">版本5：通过 SHA-1 哈希算法生成，生成随机分布的ID需要唯一的种子，这可能导致许多数据结构碎片化；</p>
<p style=""></p>
<p style="">这里面常用的就是 UUID4 了，但是，即使是随机的，但是也是存在冲突的风险。</p>
<p style="">和 UUID 要么基于随机数，要么基于时间戳不同，ULID 是既基于时间戳又基于随机数，时间戳精确到毫秒，毫秒内有1.21e + 24个随机数，不存在冲突的风险，而且转换成字符串比 UUID 更加友好。</p>
<h2 style="" id="ulid%E7%89%B9%E6%80%A7">ULID特性</h2>
<pre><code>ulid() # 01ARZ3NDEKTSV4RRFFQ69G5FAV</code></pre>
<ul>
 <li>
  <p style="">与UUID的128位兼容性</p></li>
 <li>
  <p style="">每毫秒1.21e + 24个唯一ULID</p></li>
 <li>
  <p style="">按字典顺序(也就是字母顺序)排序！</p></li>
 <li>
  <p style="">规范地编码为26个字符串，而不是UUID的36个字符</p></li>
 <li>
  <p style="">使用Crockford的base32获得更好的效率和可读性（每个字符5位）</p></li>
 <li>
  <p style="">不区分大小写</p></li>
 <li>
  <p style="">没有特殊字符（URL安全）</p></li>
 <li>
  <p style="">单调排序顺序（正确检测并处理相同的毫秒）</p></li>
</ul>
<h2 style="" id="ulid%E8%A7%84%E8%8C%83">ULID规范</h2>
<p style="">以下是在python(ulid-py)中实现的ULID的当前规范。二进制格式已实现</p>
<pre><code>01AN4Z07BY      79KA1307SR9X4MV3
|----------|    |----------------|
Timestamp          Randomness
10chars            16chars
48bits             80bits</code></pre>
<h3 style="" id="%E7%BB%84%E6%88%90">组成</h3>
<h4 style="" id="%E6%97%B6%E9%97%B4%E6%88%B3">时间戳</h4>
<ul>
 <li>
  <p style="">48位整数</p></li>
 <li>
  <p style="">UNIX时间（以毫秒为单位）</p></li>
 <li>
  <p style="">直到公元10889年，空间都不会耗尽。</p></li>
</ul>
<h4 style="" id="%E9%9A%8F%E6%9C%BA%E6%80%A7">随机性</h4>
<ul>
 <li>
  <p style="">80位随机数</p></li>
 <li>
  <p style="">如果可能的话，采用加密技术保证随机性</p></li>
</ul>
<h4 style="" id="%E6%8E%92%E5%BA%8F">排序</h4>
<p style="">最左边的字符必须排在最前面，最右边的字符必须排在最后（词汇顺序）。必须使用默认的ASCII字符集。在同一毫秒内，不能保证排序顺序</p>
<h4 style="" id="%E7%BC%96%E7%A0%81%E6%96%B9%E5%BC%8F">编码方式</h4>
<p style="">如图所示，使用了Crockford的Base32。该字母表不包括字母I，L，O和U，以避免混淆和滥用。</p>
<pre><code>0123456789ABCDEFGHJKMNPQRSTVWXYZ</code></pre>
<h4 style="" id="%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%B8%83%E5%B1%80%E5%92%8C%E5%AD%97%E8%8A%82%E9%A1%BA%E5%BA%8F">二进制布局和字节顺序</h4>
<p style="">组件被编码为16个八位位组。每个组件都以最高有效字节在前（网络字节顺序）进行编码。</p>
<pre><code>0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      32_bit_uint_time_high                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     16_bit_uint_time_low      |       16_bit_uint_random      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</code></pre>
<h3 style="" id="%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF">应用场景</h3>
<ul>
 <li>
  <p style="">替换数据库自增id，无需DB参与主键生成</p></li>
 <li>
  <p style="">分布式环境下，替换UUID，全局唯一且毫秒精度有序</p></li>
 <li>
  <p style="">比如要按日期对数据库进行分区分表，可以使用ULID中嵌入的时间戳来选择正确的分区分表</p></li>
 <li>
  <p style="">如果毫秒精度是可以接受的（毫秒内无序），可以按照ULID排序，而不是单独的created_at字段</p></li>
</ul>
<h2 style="" id="%E7%94%A8%E6%B3%95%EF%BC%88python%EF%BC%89">用法（python）</h2>
<p style="">安装</p>
<pre><code>pip install ulid-py</code></pre>
<p style="">创建一个全新的ULID。</p>
<p style="">时间戳记值（48位）来自time.time()，精度为毫秒。</p>
<p style="">随机值（80位）来自os.urandom()。</p>
<pre><code>&gt;&gt;&gt; import ulid
&gt;&gt;&gt; ulid.new()
&lt;ULID('01BJQE4QTHMFP0S5J153XCFSP9')&gt;</code></pre>
<p style="">根据现有的128位值（例如UUID）创建新的ULID 。</p>
<p style="">支持ULID值类型有 int，bytes，str，和UUID。</p>
<pre><code>&gt;&gt;&gt; import ulid, uuid
&gt;&gt;&gt; value = uuid.uuid4()
&gt;&gt;&gt; value
UUID('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
&gt;&gt;&gt; ulid.from_uuid(value)
&lt;ULID('09GF8A5ZRN9P1RYDVXV52VBAHS')&gt;</code></pre>
<p style="">从现有时间戳值（例如datetime对象）创建新的ULID 。</p>
<p style="">支持时间戳值类型有int，float，str，bytes，bytearray，memoryview，datetime，Timestamp，和ULID</p>
<pre><code>&gt;&gt;&gt; import datetime, ulid
&gt;&gt;&gt; ulid.from_timestamp(datetime.datetime(1999, 1, 1))
&lt;ULID('00TM9HX0008S220A3PWSFVNFEH')&gt;</code></pre>
<p style="">根据现有的随机数创建一个新的ULID。</p>
<p style="">支持随机值类型有int，float，str，bytes，bytearray，memoryview，Randomness，和ULID。</p>
<pre><code>&gt;&gt;&gt; import os, ulid
&gt;&gt;&gt; randomness = os.urandom(10)
&gt;&gt;&gt; ulid.from_randomness(randomness)
&gt;&gt;&gt; &lt;ULID('01BJQHX2XEDK0VN0GMYWT9JN8S')&gt;</code></pre>
<p style="">一旦有了ULID对象，就有多种与之交互的方法。</p>
<p style="">timestamp()方法将为您提供ULID的前48位的时间戳快照，而randomness()方法将为您提供后80位的随机数快照。</p>
<pre><code>&gt;&gt;&gt; import ulid
&gt;&gt;&gt; u = ulid.new()
&gt;&gt;&gt; u
&lt;ULID('01BJQM7SC7D5VVTG3J68ABFQ3N')&gt;
&gt;&gt;&gt; u.timestamp()
&lt;Timestamp('01BJQM7SC7')&gt;
&gt;&gt;&gt; u.randomness()
&lt;Randomness('D5VVTG3J68ABFQ3N')&gt;</code></pre>
<p style=""></p>
<p style="">github：<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/ahawker/ulid">https://github.com/ahawker/ulid</a></p>]]></description><guid isPermaLink="false">/archives/ulid---yi-zhong-bi-uuidgeng-hao-de-fang-an</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FULID.png&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:08:00 GMT</pubDate></item><item><title><![CDATA[开发的网页不应该大于14KB]]></title><link>https://xiaoming728.com/archives/kai-fa-de-wang-ye-bu-ying-gai-da-yu-14kb</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%BC%80%E5%8F%91%E7%9A%84%E7%BD%91%E9%A1%B5%E4%B8%8D%E5%BA%94%E8%AF%A5%E5%A4%A7%E4%BA%8E14KB&amp;url=/archives/kai-fa-de-wang-ye-bu-ying-gai-da-yu-14kb" width="1" height="1" alt="" style="opacity:0;">
<p style="">资深Web开发的经验之谈：为什么你开发的网页不应该大于14KB？</p>
<p style="">虽然我们生活在一个宽带无处不在、4/5G 几乎全覆盖的时代，但网站加载缓慢还是常态，就算我们打开一个以文本为中心的新闻网站，都可能需要至少 30 秒才能开始阅读。毕竟在内容膨胀时代，一张照片就能轻易超过 1MB 大小，许多网站为了显示几段文本，还会单独加载至少 10MB 的 JS 和自定义字体。</p>
<p style="">对此，对优化和极简主义充满热情的资深 Web 开发 Nathaniel 告诉我们，你应该让你的网页尽力控制在 14KB 以内，而且即使对于以富媒体为中心的网站，这条 14KB 的规则可能仍然值得遵循。如果 14KB 不足以用于最终布局，则需要优先考虑“首屏”字节，可以用发送给访问者的前 14KB 数据来渲染一些有用的东西，减少用户还没有开始阅读就流失掉的机会。</p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">网页越小，加载速度就越快——这一点都不奇怪。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">但令人感到惊讶的是，14KB 网页的加载速度比 15KB 要快得多——可能快 612 毫秒——而 15KB 和 16KB 网页之间的加载速度差异微乎其微。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">这是 TCP 慢启动算法导致的。本文将介绍这个算法、它的原理以及为什么你应该关注它。但首先我们需要快速过一遍一些基础知识。</span></p>
<h3 style="" id="1%E3%80%81tcp-%E6%98%AF%E4%BB%80%E4%B9%88">1、TCP 是什么</h3>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">传输控制协议（Transmission Control Protocol，TCP）是一种使用 IP 协议可靠地发送数据包的方法——有时被称为 TCP/IP。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">当浏览器向你的网站（或图像或样式表）发出请求时，它会使用 HTTP 请求。HTTP 建立在 TCP 之上，一个 HTTP 请求通常由许多 TCP 数据包组成。IP 只是一个将数据包从互联网上的一个位置发送到另一个位置的系统。IP 没有检查数据包是否成功到达目的地的方法。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">对于网站来说，确保所有的数据到达请求端是非常关键的，否则我们可能会因为丢失数据包无法获得完整的网页。但在网络的其他应用场景中，这一点并不那么重要——比如流媒体直播视频。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">TCP 是 IP 的扩展，浏览器和网站服务器通过它告诉对方哪些数据包已经成功到达。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">服务器发送一些数据包，然后等待浏览器已经收到数据包的响应（这叫确认或 ACK），然后它继续发送更多的数据包——或者如果它没有收到 ACK，将再次发送相同的数据包。</span></p>
<h3 style="" id="2%E3%80%81%E4%BB%80%E4%B9%88%E6%98%AF-tcp-%E6%85%A2%E5%90%AF%E5%8A%A8">2、什么是 TCP 慢启动</h3>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">TCP 慢启动是一种算法，服务器用它来确定一次可以发送多少数据包。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">当浏览器第一次连接到服务器时，服务器无法知道它们之间的带宽是多少。带宽是指在单位时间内网络可以传输的数据量。通常以比特 / 秒（b/s）为单位。我们可以用管道来作类比——把带宽想象成每秒从管道流出多少水。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">服务器不知道网络连接可以处理多少数据——所以它先发送少量且安全的数据——通常是 10 个 TCP 数据包。如果这些数据包成功地到达网站访问者，他们的计算机返回确认（ACK），表示数据包已经被收到了。然后，服务器发送更多的数据包，但这一次它将数据包的数量增加了一倍。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">这个过程会不断重复，直到数据包丢失，服务器没有收到 ACK。（此时，服务器会继续发送数据包，但速度较慢）。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">这就是 TCP 慢启动的要点——在现实当中，虽然算法各不相同，但这是它的基本原理。</span></p>
<h3 style="" id="3%E3%80%81%E9%82%A3%E4%B9%88-14kb-%E8%BF%99%E4%B8%AA%E6%95%B0%E5%AD%97%E6%98%AF%E6%80%8E%E4%B9%88%E6%9D%A5%E7%9A%84">3、那么 14KB 这个数字是怎么来的</h3>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">大多数 Web 服务器的 TCP 慢启动算法都是从发送 10 个 TCP 数据包开始的。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">TCP 数据包最大长度为 1500 字节。这个最大值不是由 TCP 规范设置的，它来自于以太网标准。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">每个 TCP 数据包的标头占了 40 个字节，其中 16 个字节用于 IP，另外 24 个字节用于 TCP。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">这样每个 TCP 数据包还剩下 1460 个字节。10 x 1460 = 14600 字节，或大约 14KB！</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">因此，如果你能把网站的网页——或网页的关键部分——压缩到 14KB，就可以为访问者节省大量的时间——他们和网站服务器之间的往返时间。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">一个数据往返能有多糟糕？但人们非常没有耐心——一个数据往返可能会出奇地长，具体多长取决于延迟……延迟是指数据包从源传输到目的地所花费的时间。如果带宽是每秒钟可以通过管道的水的数量，那么延迟就是一滴水进入管道后从另一端流出所花费的时间。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">下面是一个关于延迟有多糟糕的例子。</span></p>
<h4 style="" id="%E5%8D%AB%E6%98%9F%E7%BD%91%E7%BB%9C">卫星网络</h4>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">卫星网络是由环绕地球轨道的卫星提供的，在人烟稀少的地区、石油钻井平台、游轮以及飞机上，人们可以使用这种网络。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">为了说明这种糟糕的延迟，我们想象一群在石油钻井平台工作的兄弟把骰子忘在了家里，他们需要通过 </span><a target="_blank" rel="noopener noreferrer nofollow" href="http://missingdice.com"><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">missingdice.com</span></a><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">（少于 14KB）来玩《龙与地下城》游戏。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">首先，他们中的一个用手机发出一个网页请求……</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">手机将请求发送到钻井平台的 WiFi 路由器，路由器将数据发送给平台上的卫星天线，我们假设这可能需要 1 毫秒时间。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">然后，卫星天线将数据发送到地球轨道上方的卫星。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">通常，这是通过在地球表面上方 35786 公里处运行的轨道卫星实现的。光速为 299792458 米 / 秒，所以信息从地球发送到卫星需要 120 毫秒。然后，卫星将信息传回地面接收站，这又需要 120 毫秒。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">然后，地面站必须将请求发送到位于地球任意位置的服务器（当光通过光纤电缆传输时，速度会降至每秒 200000000 米）。如果地面站和服务器之间的距离等于纽约到伦敦之间的距离，那么大约需要 28 毫秒，如果地面站和服务器之间的距离等于纽约到悉尼之间的距离，则需要 80 毫秒——所以我们姑且定一个 60 毫秒的数字（这个数字便于计算）。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">然后，服务器需要处理请求，这可能需要 10 毫秒，然后服务器再次将它发送出去。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">回到地面站，进入太空，回到卫星天线，然后回到无线路由器，再到手机上。</span></p>
<p style=""><span fontsize="" color="rgb(63, 63, 63)" style="color: rgb(63, 63, 63)">手机 -&gt; WiFi 路由器 -&gt;卫星天线 -&gt;卫星 -&gt; 地面站 -&gt; 服务器 -&gt; 地面站 -&gt; 卫星 -&gt; 卫星天线 -&gt; WiFi 路由器 -&gt; 手机</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">如果我们算一下，就是 10 + ( 1 + 120 + 120 + 60 ) x 2 = 612 毫秒。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">这是每次往返额外的 612 毫秒——也许这看起来不是很长时间，但你的网站可能只是为了获取第一个资源就需要许多个往返。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">另外，HTTPS 在完成第一个往返之前需要额外的两次往返——这使延迟达到了 1836 毫秒！</span></p>
<h4 style="" id="%E5%AF%B9%E4%BA%8E%E7%94%9F%E6%B4%BB%E5%9C%A8%E9%99%86%E5%9C%B0%E4%B8%8A%E7%9A%84%E4%BA%BA%EF%BC%8C%E5%BB%B6%E8%BF%9F%E5%8F%88%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84">对于生活在陆地上的人，延迟又是怎样的</h4>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">卫星网络似乎是一个极端的例子——我选择它作为例子是因为它能够充分说明了网络延迟这个问题——但对于生活在陆地上的人来说，延迟可能比这更糟糕，原因有很多。</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">2G 网络的延迟通常在 300 毫秒到 1000 毫秒之间；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">3G 网络的延迟可以在 100 毫秒到 500 毫秒之间；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">嘈杂的移动网络——比如在一个异常拥挤的地方，比如音乐节；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">处理大流量的服务器；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">其他一些不好的东西。</span></p></li>
</ul>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">不稳定的网络连接也会导致数据包丢失——导致需要另一个往返来获取丢失的数据包。</span></p>
<h3 style="" id="4%E3%80%81%E4%BA%86%E8%A7%A3%E4%BA%86-14kb-%E6%B3%95%E5%88%99%EF%BC%8C%E6%8E%A5%E4%B8%8B%E6%9D%A5%E5%8F%AF%E4%BB%A5%E5%81%9A%E4%BA%9B%E4%BB%80%E4%B9%88">4、了解了 14KB 法则，接下来可以做些什么</h3>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">当然，你应该让你的网页尽可能的小——你爱你的访客，你希望他们开心。将每个页面的大小控制在 14KB 以内是一个不错的主意。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">这 14KB 可以是压缩数据——所以实际上可以对应大约 50KB 的未压缩数据——这已经非常慷慨了。要知道，阿波罗 11 的制导计算机只有 72KB 内存。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">去掉自动播放的视频、弹出窗口、Cookie、Cookie 横幅、社交网络按钮、跟踪脚本、JavaScript 和 CSS 框架，以及所有其他人们不喜欢的垃圾——你可能就能实现 14KB 法则。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">假设你已经尽力将所有内容控制在 14KB 以内，但仍然做不到——但 14KB 法则仍然很有用。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">你可以用发送给访问者的前 14KB 数据来渲染一些有用的东西——例如一些关键的 CSS、JS 和解释如何使用你的应用程序的前几段文本。</span></p>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">需要注意的是，14KB 法则包含了 HTTP 标头——这些是未压缩的（即使是 HTTP/2 的第一个响应），也包含图片，所以你应该只加载在页面上方的内容，并保持它们最小，或者使用占位符，让访问者知道他们在等待一些更好的内容。</span></p>
<h4 style="" id="%E5%85%B3%E4%BA%8E%E8%BF%99%E4%B8%AA%E6%B3%95%E5%88%99%E7%9A%84%E4%B8%80%E4%BA%9B%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9">关于这个法则的一些注意事项</h4>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">14KB 法则更像是一种经验之谈，而不是计算的基本法则。</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">一些服务器已经将 TCP 慢启动初始窗口从 10 个数据包增加到 30 个；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">有时服务器知道它可以从更大数量的数据包开始传输，因为它使用 TLS 握手来建立一个更大的窗口；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">服务器可以缓存路由可管理的数据包数量，并在下一次连接时发送更多的数据包；</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">还有其他需要注意的地方——这里有一篇文章更深入地探讨关于为什么 14KB 法则并不总是这么回事。</span><span fontsize="" color="rgb(136, 136, 136)" style="color: rgb(136, 136, 136)">（</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.tunetheweb.com/blog/critical-resources-and-the-first-14kb/）。"><span fontsize="" color="rgb(136, 136, 136)" style="color: rgb(136, 136, 136)">https://www.tunetheweb.com/blog/critical-resources-and-the-first-14kb/）。</span></a></p></li>
</ul>
<h4 style="" id="http%2F2-%E5%92%8C-14kb-%E6%B3%95%E5%88%99">HTTP/2 和 14KB 法则</h4>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">有一种观点认为，在使用 HTTP/2 时，14KB 法则不再适用。我已经读了所有我能读到的关于这个问题的东西，但我还没有看到任何证据表明使用 HTTP/2 的服务器已经停止使用 TCP 慢启动（从 10 个数据包开始）。</span></p>
<h4 style="" id="http%2F3-%E5%92%8C-quic">HTTP/3 和 QUIC</h4>
<p style=""><span fontsize="" color="rgb(74, 74, 74)" style="color: rgb(74, 74, 74)">与 HTTP/2 类似，有一种观点认为 HTTP/3 和 QUIC 将废除 14KB 法则——事实并非如此。实际上，QUIC 仍然建议使用 14KB 法则。</span></p>
<p style=""><strong><span fontsize="" color="rgb(136, 136, 136)" style="color: rgb(136, 136, 136)">原文链接：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://endtimes.dev/why-your-website-should-be-under-14kb-in-size/￼"><span fontsize="" color="rgb(136, 136, 136)" style="color: rgb(136, 136, 136)">https://endtimes.dev/why-your-website-should-be-under-14kb-in-size/</span><br></a></p>]]></description><guid isPermaLink="false">/archives/kai-fa-de-wang-ye-bu-ying-gai-da-yu-14kb</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F14kb.png&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:07:27 GMT</pubDate></item><item><title><![CDATA[6个设计原则-优雅代码的秘密]]></title><link>https://xiaoming728.com/archives/6ge-she-ji-yuan-ze-you-ya-dai-ma-de-mi-mi</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=6%E4%B8%AA%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99-%E4%BC%98%E9%9B%85%E4%BB%A3%E7%A0%81%E7%9A%84%E7%A7%98%E5%AF%86&amp;url=/archives/6ge-she-ji-yuan-ze-you-ya-dai-ma-de-mi-mi" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：微信公众号- 捡田螺的小男孩</p>
<p style="">发布： 2022-10-20 07:52</p>
<p style="">连接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/qy3jLa1JUdwJ9XNBsw44uw">https://mp.weixin.qq.com/s/qy3jLa1JUdwJ9XNBsw44uw</a></p>
<h2 style="" id="%E5%89%8D%E8%A8%80">前言</h2>
<p style=""><span fontsize="" color="black" style="color: black">优雅的代码，犹如亭亭玉立的美女，让人赏心悦目。而糟糕的代码，却犹如屎山，让人避而远之。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">如何写出优雅的代码呢？那就要理解并熟悉应用这</span><strong><span fontsize="" color="black" style="color: black">6个设计原则啦：开闭原则、单一职责原则、接口隔离原则 、迪米特法则、里氏替换原则、依赖倒置原则</span></strong><span fontsize="" color="black" style="color: black">。本文呢，将通过代码demo，让大家轻松理解这6个代码设计原则，加油~</span></p>
<h2 style="" id="1.-%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99">1. 开闭原则</h2>
<p style=""><span fontsize="" color="black" style="color: black">开闭原则，即</span><strong><span fontsize="" color="black" style="color: black">对扩展开放，对修改关闭</span></strong><span fontsize="" color="black" style="color: black">。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">对于扩展和修改，我们怎么去理解它呢？</span><strong><span fontsize="" color="black" style="color: black">扩展开放</span></strong><span fontsize="" color="black" style="color: black">表示，未来业务需求是变化万千，</span><strong><span fontsize="" color="black" style="color: black">代码应该保持灵活的应变能力</span></strong><span fontsize="" color="black" style="color: black">。</span><strong><span fontsize="" color="black" style="color: black">修改关闭</span></strong><span fontsize="" color="black" style="color: black">表示不允许在原来类修改，</span><strong><span fontsize="" color="black" style="color: black">保持稳定性</span></strong><span fontsize="" color="black" style="color: black">。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670484043474-1d4fa208-a975-448a-8c28-1505de8e9bf9.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">因为日常需求是不断迭代更新的，所以我们经常需要在原来的代码中修改。如果代码设计得不好，扩展性不强，每次需求迭代，都要在原来代码中修改，很可能会引入</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">bug</span><span fontsize="" color="black" style="color: black">。因此，我们的代码应该遵循开闭原则，也就是</span><strong><span fontsize="" color="black" style="color: black">对扩展开放，对修改关闭</span></strong><span fontsize="" color="black" style="color: black">。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">为了方便大家理解开闭原则，</span><strong><span fontsize="" color="black" style="color: black">我们来看个例子</span></strong><span fontsize="" color="black" style="color: black">：假设有这样的业务场景，大数据系统把文件推送过来，根据不同类型采取不同的解析方式。多数的小伙伴就会写出以下的代码：</span></p>
<pre><code>if(type=="A"){
   //按照A格式解析
}else if(type=="B"){
    //按B格式解析
}else{
    //按照默认格式解析
}</code></pre>
<p style=""><span fontsize="" color="black" style="color: black">这段代码有什么问题呢？</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">如果分支变多，这里的代码就会变得臃肿，难以维护，可读性低。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">如果你需要接入一种新的解析类型，那只能在原有代码上修改。</span></p></li>
</ul>
<p style=""><span fontsize="" color="black" style="color: black">显然，</span><strong><span fontsize="" color="black" style="color: black">增加、删除某个逻辑</span></strong><span fontsize="" color="black" style="color: black">，都需要修改到原来类的代码，这就违反了</span><strong><span fontsize="" color="black" style="color: black">开闭原则</span></strong><span fontsize="" color="black" style="color: black">了。为了解决这个问题，我们可以使用</span><strong><span fontsize="" color="black" style="color: black">策略模式</span></strong><span fontsize="" color="black" style="color: black">去优化它。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">你可以先声明一个文件解析的接口，如下：</span></p>
<pre><code>public interface IFileStrategy {
    
    //属于哪种文件解析类型，A或者B
    FileTypeResolveEnum gainFileType();
    
    //封装的公用算法（具体的解析方法）
    void resolve(Object param);
}</code></pre>
<p style=""><span fontsize="" color="black" style="color: black">然后实现不同策略的解析文件，如类型A解析：</span></p>
<pre><code>@Component
public class AFileResolve implements IFileStrategy {
    
    @Override
    public FileTypeResolveEnum gainFileType() {
        return FileTypeResolveEnum.File_A_RESOLVE;
    }

    @Override
    public void resolve(Object objectparam) {
      logger.info("A 类型解析文件，参数：{}",objectparam);
      //A类型解析具体逻辑
    }
}</code></pre>
<p style=""><span fontsize="" color="black" style="color: black">如果未来需求变更的话，比如增加、删除某个逻辑，不会再修改到原来的类啦，只需要修改对应的文件解析类型的类即可。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">对于如何使用设计模式，大家有兴趣的话，可以看我以前的这篇文章哈：</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247495616&amp;idx=1&amp;sn=e74c733d26351eab22646e44ea74d233&amp;chksm=cf2230e9f855b9ffe1ddb9fe15f72a273d5de02ed91cc97f3066d4162af027299718e2bf748e&amp;token=1183092541&amp;lang=zh_CN&amp;scene=21#wechat_redirect">实战！工作中常用到哪些设计模式</a></p>
<h2 style="" id="2.-%E5%8D%95%E4%B8%80%E8%81%8C%E8%B4%A3%E5%8E%9F%E5%88%99">2. 单一职责原则</h2>
<p style=""><span fontsize="" color="black" style="color: black">单一职责原则：一个类或者一个接口，最好只负责一项职责。比如一个类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">C</span><span fontsize="" color="black" style="color: black">违反了单一原则，它负责两个职责</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">P1</span><span fontsize="" color="black" style="color: black">和</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">P2</span><span fontsize="" color="black" style="color: black">。当职责</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">P1</span><span fontsize="" color="black" style="color: black">需要修改时，就会改动到类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">C</span><span fontsize="" color="black" style="color: black">，这就可能导致原本正常的</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">P2</span><span fontsize="" color="black" style="color: black">也受影响。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">如何更好理解呢？比如你实现一个图书管理系统，一个类既有图书的增删改查，又有读者的增删改查，你就可以认为这个类违反了</span><strong><span fontsize="" color="black" style="color: black">单一原则</span></strong><span fontsize="" color="black" style="color: black">。因为这个类涉及了不同的功能职责点，你可以把这个拆分。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670484043414-f98da676-841c-4372-99a2-90841d00d320.png&amp;size=m" width="622" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">以上图书管理系统这个例子，违反单一原则，按业务拆分。这比较好理解，但是有时候，一个类并不是那么好区分。这时候大家可以看这个标准，</span><strong><span fontsize="" color="black" style="color: black">来判断功能职责是否单一</span></strong><span fontsize="" color="black" style="color: black">:</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">类中的私有方法过多</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">你很难给类起一个合适的名字</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">类中的代码行数、函数或者属性过多</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">类中大量的方法都是集中操作类中的某几个属性</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">类依赖的其他类过多，或者依赖类的其他类过多</span></p></li>
</ul>
<p style=""><span fontsize="" color="black" style="color: black">比如，你写了一个方法，这个方法包括了</span><strong><span fontsize="" color="black" style="color: black">日期处理</span></strong><span fontsize="" color="black" style="color: black">和</span><strong><span fontsize="" color="black" style="color: black">借还书</span></strong><span fontsize="" color="black" style="color: black">的业务操作，你就可以把日期处理抽到私有方法。再然后，如果你发现，很多私有方法，都是类似的日期处理，你就可以把这个日期处理方法抽成一个工具类。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">日常开发中，单一原则的思想都有体现的。比如微服务拆分。</span></p>
<h2 style="" id="3.-%E6%8E%A5%E5%8F%A3%E9%9A%94%E7%A6%BB%E5%8E%9F%E5%88%99">3. 接口隔离原则</h2>
<p style=""><span fontsize="" color="black" style="color: black">接口隔离原则：</span><strong><span fontsize="" color="black" style="color: black">接口的调用者或者使用者，不应该强迫依赖它不需要的接口</span></strong><span fontsize="" color="black" style="color: black">。它要求建立单一的接口，不要建立庞大臃肿的接口，尽量细化接口，接口中的方法尽量少，让接口中只包含客户（调用者）感兴趣的方法。即一个类对另一个类的依赖应该建立在最小的接口上。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">比如类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">A</span><span fontsize="" color="black" style="color: black">通过接口</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">I</span><span fontsize="" color="black" style="color: black">依赖类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">B</span><span fontsize="" color="black" style="color: black">，类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">C</span><span fontsize="" color="black" style="color: black">通过接口</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">I</span><span fontsize="" color="black" style="color: black">依赖类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">D</span><span fontsize="" color="black" style="color: black">，如果接口</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">I</span><span fontsize="" color="black" style="color: black">对于类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">A</span><span fontsize="" color="black" style="color: black">和类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">B</span><span fontsize="" color="black" style="color: black">来说，都不是最小接口，则类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">B</span><span fontsize="" color="black" style="color: black">和类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">D</span><span fontsize="" color="black" style="color: black">必须去实现他们不需要的方法。如下图：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670484043460-21dc4b36-dcac-4a0a-a9a9-daedc754c9a5.png&amp;size=m" width="1062" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">这个图表达的意思是：类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">A</span><span fontsize="" color="black" style="color: black">依赖接口</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">I</span><span fontsize="" color="black" style="color: black">中的</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">method1</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">method2</span><span fontsize="" color="black" style="color: black">，类B是对类A依赖的实现。类C依赖接口</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">I</span><span fontsize="" color="black" style="color: black">中的</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">method1</span><span fontsize="" color="black" style="color: black">、</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">method3</span><span fontsize="" color="black" style="color: black">，类D是对类C依赖的实现。对于实现类B和D，它们都存在用不到的方法，但是因为实现了接口I，所以必须要实现这些用不到的方法。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">可以看下以下代码：</span></p>
<pre><code>public interface I {

    void method1();

    void method2();

    void method3();
}

@Service
public class A {

    @Resource(name="B")
    private I i;

    public void depend1() {
        i.method1();
    }

    public void depend2(){
        i.method2();
    }

}

@Service("B")
public class B implements I {

    @Override
    public void method1() {
        System.out.println("类B实现接口I的方法1");
    }

    @Override
    public void method2() {
        System.out.println("类B实现接口I的方法2");
    }

    //没用到这个方法，但是也要默认实现，因为I有这个接口方法
    @Override
    public void method3() {

    }
}

@Service
public class C {

    @Resource(name="D")
    private I i;

    public void depend1(I i){
        i.method1();
    }

    public void depend3(I i){
        i.method3();
    }

}

@Service("D")
public class D implements I {

    @Override
    public void method1() {
        System.out.println("类D实现接口I的方法1");
    }

    //没用到这个方法，但是也要默认实现，因为I有这个接口方法
    @Override
    public void method2() {

    }

    @Override
    public void method3() {
        System.out.println("类D实现接口I的方法3");
    }
}</code></pre>
<p style=""><span fontsize="" color="black" style="color: black">大家可以发现，如果接口过于臃肿，只要接口中出现的方法，不管对依赖于它的类有没有用到，实现类都必须去实现这些方法。实现类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">B</span><span fontsize="" color="black" style="color: black">没用到</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">method3</span><span fontsize="" color="black" style="color: black">，它也要有个默认实现。实现类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">D</span><span fontsize="" color="black" style="color: black">没用到</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">method2</span><span fontsize="" color="black" style="color: black">，它也要有个默认实现。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">显然，这不是一个好的设计，违反了接口隔离原则。我们可以对接口</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">I</span><span fontsize="" color="black" style="color: black">进行拆分。拆分后的设计如图2所示：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2022%2Fpng%2F550960%2F1670484043512-4ec0446e-1b2b-4247-821a-04822294b264.png&amp;size=m" width="1080" style="display: inline-block"></p>
<p style=""><span fontsize="" color="black" style="color: black">接口是不是分得越细越好呢？并不是。日常开发中，采用接口隔离原则对接口进行约束时，要注意以下几点：</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">接口尽量小，但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实，但是如果过小，则会造成接口数量过多，使设计复杂化。所以一定要适度。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">为依赖接口的类定制服务，只暴露给调用的类它需要的方法，它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务，才能建立最小的依赖关系。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">提高内聚，减少对外交互。使接口用最少的方法去完成最多的事情。运用接口隔离原则，一定要适度，接口设计的过大或过小都不好。设计接口的时候，只有多花些时间去思考和筹划，才能准确地实践这一原则。</span></p></li>
</ul>
<h2 style="" id="4.-%E8%BF%AA%E7%B1%B3%E7%89%B9%E6%B3%95%E5%88%99">4. 迪米特法则</h2>
<p style=""><span fontsize="" color="black" style="color: black">定义：又叫最少知道原则。一个类对于其他类知道的越少越好，就是说一个对象应当对其他对象有尽可能少的了解,只和朋友谈心，不和陌生人说话。它的核心思想就是，</span><strong><span fontsize="" color="black" style="color: black">尽量降低类与类之间的耦合，尽最大能力减小代码修改带来的对原有的系统的影响</span></strong><span fontsize="" color="black" style="color: black">。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">比如一个生活例子：你对你的对象肯定了解的很多，但是如果你对别人的对象也了解很多，你的对象要是知道，那就要出大事了。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">我们来看下一个违反迪米特法则的例子，业务场景是这样的：一个学校，要求打印出所有师生的ID。</span></p>
<pre><code>//学生  
class Student{  
    private String id;  
    public void setId(String id){  
        this.id = id;  
    }  
    public String getId(){  
        return id;  
    }  
}  

//老师  
class Teacher{  
    private String id;  
    public void setId(String id){  
        this.id = id;  
    }  
    public String getId(){  
        return id;  
    }  
}  

//管理者（班长）
public class Monitor {
 
    //所有学生
    public List&lt;Student&gt; getAllStudent(){
        List&lt;Student&gt; list = new ArrayList&lt;Student&gt;();
        for(int i=0; i&lt;100; i++){
            Student student = new Student();
            //为每个学生分配个ID
            student.setId("学生Id："+i);
            list.add(student);
        }
        return list;
    }

}

//校长
public class Principal {

    //所有教师
    public List&lt;Teacher&gt; getAllTeacher(){
        List&lt;Teacher&gt; list = new ArrayList&lt;Teacher&gt;();
        for(int i=0; i&lt;20; i++){
            Teacher emp = new Teacher();
            //为全校老师按顺序分配一个ID
            emp.setId("老师编号"+i);
            list.add(emp);
        }
        return list;
    }

    //所有师生
    public void printAllTeacherAndStudent(ClassMonitor classMonitor) {

        List&lt;Student&gt; list1 = classMonitor.getAllStudent();
        for (Student s : list1) {
            System.out.println(s.getId());
        }

        List&lt;Teacher&gt; list2 = this.getAllTeacher();
        for (Teacher teacher : list2) {
            System.out.println(teacher.getId());
        }
    }
}</code></pre>
<p style=""><span fontsize="" color="black" style="color: black">这块代码。问题出在类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">Principal</span><span fontsize="" color="black" style="color: black">中，根据迪米特法则，只能与直接的朋友发生通信，而</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">Student</span><span fontsize="" color="black" style="color: black">类并不是</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">Principal</span><span fontsize="" color="black" style="color: black">类的直接朋友（</span><strong><span fontsize="" color="black" style="color: black">以局部变量出现的耦合不属于直接朋友</span></strong><span fontsize="" color="black" style="color: black">），从逻辑上讲校长</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">Principal</span><span fontsize="" color="black" style="color: black">只与管理者</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">Monitor</span><span fontsize="" color="black" style="color: black">耦合就行了，可以让</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">Principal</span><span fontsize="" color="black" style="color: black">继承类</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">Monitor</span><span fontsize="" color="black" style="color: black">，重写一个</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">printMember</span><span fontsize="" color="black" style="color: black">的方法。优化后的代码如下:</span></p>
<pre><code>public class Monitor {

    public List&lt;Student&gt; getAllStudent(){
        List&lt;Student&gt; list = new ArrayList&lt;Student&gt;();
        for(int i=0; i&lt;100; i++){
            Student student = new Student();
            //为每个学生分配个ID
            student.setId("学生Id："+i);
            list.add(student);
        }
        return list;
    }

    public void printMember() {
        List&lt;Student&gt; list = this.getAllStudent();
        for (Student student : list) {
            System.out.println(student.getId());
        }
    }
}

public class Principal extends Monitor {

    public List&lt;Teacher&gt; getAllTeacher(){
        List&lt;Teacher&gt; list = new ArrayList&lt;Teacher&gt;();
        for(int i=0; i&lt;30; i++){
            Teacher emp = new Teacher();
            //为全校老师按顺序分配一个ID
            emp.setId("老师编号"+i);
            list.add(emp);
        }
        return list;
    }

    public void printMember() {
        super.printMember();

        for (Teacher teacher : this.getAllTeacher()) {
            System.out.println(teacher.getId());
        }
    }
}
</code></pre>
<h2 style="" id="5.-%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8D%A2%E5%8E%9F%E5%88%99">5. 里氏替换原则</h2>
<p style=""><span fontsize="" color="black" style="color: black">里氏替换原则：</span></p>
<p style=""><span fontsize="" color="black" style="color: black">如果对每一个类型为</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">S</span><span fontsize="" color="black" style="color: black">的对象</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">o1</span><span fontsize="" color="black" style="color: black">，都有类型为</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">T</span><span fontsize="" color="black" style="color: black">的对象</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">o2</span><span fontsize="" color="black" style="color: black">，使得以</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">T</span><span fontsize="" color="black" style="color: black">定义的所有程序</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">P</span><span fontsize="" color="black" style="color: black">在所有的对象</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">o1</span><span fontsize="" color="black" style="color: black">都代换成</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">o2</span><span fontsize="" color="black" style="color: black">时，程序</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">P</span><span fontsize="" color="black" style="color: black">的行为没有发生变化，那么类型</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">S</span><span fontsize="" color="black" style="color: black">是类型</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">T</span><span fontsize="" color="black" style="color: black">的子类型。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">一句话来描述就是：</span><strong><span fontsize="" color="black" style="color: black">只要有父类出现的地方，都可以用子类来替代，而且不会出现任何错误和异常。</span></strong><span fontsize="" color="black" style="color: black">更通俗点讲，就是子类可以扩展父类的功能，但是不能改变父类原有的功能。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">其实，对里氏替换原则的定义可以总结如下：</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">子类可以实现父类的抽象方法，但不能覆盖父类的非抽象方法</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">子类中可以增加自己特有的方法</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">当子类的方法重载父类的方法时，方法的前置条件（即方法的输入参数）要比父类的方法更宽松</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">当子类的方法实现父类的方法时（重写/重载或实现抽象方法），方法的后置条件（即方法的的输出/返回值）要比父类的方法更严格或相等</span></p></li>
</ul>
<p style=""><span fontsize="" color="black" style="color: black">我们来看个例子：</span></p>
<pre><code>public class Cache {

    public void set(String key, String value) {

    }
}

public class RedisCache extends Cache {

    public void set(String key, String value) {

    }

}</code></pre>
<p style=""><span fontsize="" color="black" style="color: black">这里例子是没有违反里氏替换原则的，任何父类、父接口出现的地方子类都可以出现。如果给</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">RedisCache</span><span fontsize="" color="black" style="color: black">加上参数校验，如下：</span></p>
<pre><code>public class Cache {

    public void set(String key, String value) {

    }
}

public class RedisCache extends Cache {

    public void set(String key, String value) {
        if (key == null || key.length() &lt; 10 || key.length() &gt; 100) {
            System.out.println("key的长度不符合要求");
            throw new IllegalArgumentException();
        }
    }
}</code></pre>
<p style=""><span fontsize="" color="black" style="color: black">这就违反了里氏替换原则了，因为子类方法增加了加了参数校验，抛出了异常，虽然子类仍然可以来替换父类。</span></p>
<h2 style="" id="6.%E4%BE%9D%E8%B5%96%E5%80%92%E7%BD%AE%E5%8E%9F%E5%88%99">6.依赖倒置原则</h2>
<p style=""><span fontsize="" color="black" style="color: black">依赖倒置原则定义：</span></p>
<p style=""><span fontsize="" color="black" style="color: black">高层模块不应该依赖低层模块，两者都应该依赖其抽象；抽象不应该依赖细节，细节应该依赖抽象。它的核心思想是：</span><strong><span fontsize="" color="black" style="color: black">要面向接口编程，而不要面向实现编程</span></strong><span fontsize="" color="black" style="color: black">。</span></p>
<p style=""><span fontsize="" color="black" style="color: black">依赖倒置原则可以降低类间的耦合性、提高系统的稳定性、减少并行开发引起的风险、提高代码的可读性和可维护性。要满足依赖倒置原则，我们需要在项目中满足这个规则：</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">每个类尽量提供接口或抽象类，或者两者都具备。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">变量的声明类型尽量是接口或者是抽象类。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">任何类都不应该从具体类派生。</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(1, 1, 1)" style="color: rgb(1, 1, 1)">使用继承时尽量遵循里氏替换原则。</span></p></li>
</ul>
<p style=""><span fontsize="" color="black" style="color: black">我们来看一段</span><strong><span fontsize="" color="black" style="color: black">违反依赖倒置原则</span></strong><span fontsize="" color="black" style="color: black">的代码，业务需求是：顾客从淘宝购物。代码如下：</span></p>
<pre><code>class Customer{
    public void shopping(TaoBaoShop shop){
        //购物
        System.out.println（shop.buy());
    }
}</code></pre>
<p style=""><span fontsize="" color="black" style="color: black">以上代码是存在问题的，如果未来产品变更需求，改为顾客从京东上购物，就需要把代码修改为：</span></p>
<pre><code>class Customer{
    public void shopping(JingDongShop shop){
        //购物
        System.out.println（shop.buy());
    }
}</code></pre>
<p style=""><span fontsize="" color="black" style="color: black">如果产品又变更为从天猫购物呢？那有得修改代码了，显然这违反了</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">开闭原则</span><span fontsize="" color="black" style="color: black">。顾客类设计时，同具体的购物平台类绑定了，这违背了</span><strong><span fontsize="" color="black" style="color: black">依赖倒置</span></strong><span fontsize="" color="black" style="color: black">原则。可以设计一个</span><span fontsize="" color="rgb(239, 112, 96)" style="color: rgb(239, 112, 96)">shop</span><span fontsize="" color="black" style="color: black">接口，不同购物平台（如淘宝、京东）实现于这个接口，即修改顾客类面向</span><strong><span fontsize="" color="black" style="color: black">该接口编程</span></strong><span fontsize="" color="black" style="color: black">，就可以解决这个问题了。代码如下：</span></p>
<pre><code>class Customer{
    public void shopping(Shop shop){
        //购物
        System.out.println（shop.buy());
    }
}

interface Shop{
   String buy();
}

Class TaoBaoShop implements Shop{
     public String buy(){
       return "从淘宝购物";
     }
}

Class JingDongShop implements Shop{
      public String buy(){
       return "从京东购物";
     }
}</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/6ge-she-ji-yuan-ze-you-ya-dai-ma-de-mi-mi</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdomain-driven-design.png&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:07:00 GMT</pubDate></item><item><title><![CDATA[线上服务故障应急机制讨论]]></title><link>https://xiaoming728.com/archives/xian-shang-fu-wu-gu-zhang-ying-ji-ji-zhi-tao-lun</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E7%BA%BF%E4%B8%8A%E6%9C%8D%E5%8A%A1%E6%95%85%E9%9A%9C%E5%BA%94%E6%80%A5%E6%9C%BA%E5%88%B6%E8%AE%A8%E8%AE%BA&amp;url=/archives/xian-shang-fu-wu-gu-zhang-ying-ji-ji-zhi-tao-lun" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：CSDN-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://guisu.blog.csdn.net/">hguisu</a></p>
<p style="">发布： 2022-08-30 10:58:57</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://guisu.blog.csdn.net/">https://blog.csdn.net/hguisu/article/details/126599183</a></p>
<p style=""></p>
<p style="">最近由于疏忽误操作导致一次大故障，在此结合网上和实践经验，总结一下线上服务故障应急机制，警惕自己时刻注意服务稳定性问题。</p>
<h3 style="" id="%E5%89%8D%E8%A8%80">前言</h3>
<p style="">海恩法则</p>
<p style="">· 事故的发生是量的积累的结果。</p>
<p style="">· 再好的技术、再完美的规章 ， 在实际操作层面也无法取代人自身的素质和责任心 。</p>
<p style="">墨菲定律</p>
<p style="">· 任何事情都没有表面看起来那么简单 。</p>
<p style="">· 所有事情的发展都会比你预计的时间长 。</p>
<p style="">· 会出错的事总会出错。</p>
<p style="">· 如果你担心某种情况发生，那么它更有可能发生 。</p>
<p style=""></p>
<p style="">警示我们，在互联网公司里，对生产环境发生的任何怪异现象和问题 都不要轻易忽视，对于其背后的原因一定要彻查。同样，海恩法则也强调任何严重事故的背后 都是多次小问题的积累，积累到一定的量级后会导致质变，严重的问题就会浮出水面 。 那么，我们需要对线上服务产生的任何征兆，哪怕是一个小问题，也要刨根问底: 这就需要我们有技术攻关的能力，对任何现象都要秉着以下原则： 为什么发生？ 发生了怎么应对？ 怎么恢复？ 怎么避免？ 对问题要彻查，不能因为问题的现象不明显而忽略 。</p>
<h3 style="" id="%E4%B8%80%E3%80%81%E7%BA%BF%E4%B8%8A%E5%BA%94%E6%80%A5%E7%9A%84%E7%9B%AE%E6%A0%87%E3%80%81%E5%8E%9F%E5%88%99%E3%80%81%E6%96%B9%E6%B3%95">一、线上应急的目标、原则、方法</h3>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1674963142294-31145e05-1738-4bcd-a97c-5aeed23fca21.png&amp;size=m" width="439" style="display: inline-block"></p>
<h4 style="" id="1%E3%80%81%E5%BA%94%E6%80%A5%E7%9B%AE%E6%A0%87">1、应急目标</h4>
<p style="">行动的方向在关键时间正确把握，在应急过程中不能偏离目标。</p>
<p style=""><span fontsize="" color="rgb(223, 42, 63)" style="color: rgb(223, 42, 63)">生产环境发生故障，要快速优先想办法恢复服务，避免或减少因故障造成的损失，降低对用户的影响。</span></p>
<h4 style="" id="2%E3%80%81%E5%BA%94%E6%80%A5%E5%8E%9F%E5%88%99">2、应急原则</h4>
<p style="">对应应急原则总结如下：</p>
<p style="">（1）第一时间恢复系统而不是彻底查找原因解决问题，快速止损。</p>
<p style="">（2）有明显的资金损失时，要在第一时间升级，快速止损。该条用在金融领域尤为关键。</p>
<p style="">（3）当前应急负责人若短时间内无法解决问题，必须升级处理。</p>
<p style="">（4）应急过程中在不影响用户体验前提下，要保留部分现场和数据。便于恢复后定位分析问题原因。</p>
<h4 style="" id="3%E3%80%81%E5%BA%94%E6%80%A5%E6%96%B9%E6%B3%95%E5%92%8C%E6%B5%81%E7%A8%8B">3、应急方法和流程</h4>
<p style="">线上应急必须有组织、有计划的进行。</p>
<h4 style="" id="4%E3%80%81%E7%BA%BF%E4%B8%8A%E5%BA%94%E6%80%A5%E4%B8%BB%E8%A6%81%E5%88%86%E4%B8%BA%E5%85%AD%E4%B8%AA%E9%98%B6%E6%AE%B5%EF%BC%9A">4、线上应急主要分为六个阶段：</h4>
<p style=""><span fontsize="" color="rgb(223, 42, 63)" style="color: rgb(223, 42, 63)">应急要有总体目标：尽快恢复问题，消除影响。不管应急的那个阶段，首要问题都是优先恢复系统问题，恢复问题不要求立马定位问题，也不一定有完美的解决方案。</span></p>
<p style="">一般通过经验判断，启动线上的问题预处理方案等，达到快读恢复问题的目标。同时，要注意保留部分现场，便于事后定位解决并复盘问题。</p>
<h5 style="" id="%EF%BC%881)%E3%80%81%E5%8F%91%E7%8E%B0%E9%97%AE%E9%A2%98">（1)、发现问题</h5>
<p style="">通常针对系统层面来说的，问题的发现一定是借助于系统的告警、自动化监控等机制来实现的，不能由用户、业务方来告诉通知你的系统出现问题了，如果这样，你的系统出现问题已经持续了一段时间了。</p>
<p style="">监控层面，通常都是对系统层面、应用层面、资源层面进行监控。</p>
<p style="">对系统层面的监控包括：系统的CPU利用率、系统负载、内存是会用情况、网络IO负载、磁盘负载、I/O等待、交换区的使用、线程数及打开的文件句柄数等进行监控，一旦超出阈值，就及时告警。</p>
<p style="">对应用层面的监控包括对服务接口的响应时间、吞吐量、调用频次、接口成功率即接口的波动率进行监控。</p>
<p style="">对资源层面的监控包括对数据库、缓存和消息队列的监控。我们通常会对数据库负载、慢SQL、连接数等进行监控；对缓存的连接数、占用内存、吞吐量、响应时间等进行监控；对消息队列的响应时间、吞吐量、负载、积压情况进行监控。</p>
<h5 style="" id="%EF%BC%882)%E3%80%81%E5%AE%9A%E4%BD%8D%E9%97%AE%E9%A2%98">（2)、定位问题</h5>
<p style="">首先要根据经验来分析 ，应急团队中有人对相应问题有经验，并确定能够通过某种手段来进行恢复，则应第一时间快速恢复，同时保留现场，然后定位问题。</p>
<p style="">应急人员定位过程中可能需要与业务负责人、技术负责人、技术人员、运营和运维一起，对产生问题的原因进行快速分析。</p>
<p style="">需要考虑如下问题：</p>
<p style="">（1）问题系统最近是否进行了上线操作？</p>
<p style="">（2）依赖的基础平台和资源是否进行了上线或者升级？</p>
<p style="">（3）依赖的系统最近是否进行了上线？</p>
<p style="">（4）运营是否在系统里面做过运营变更？</p>
<p style="">（5）网络是否有波动，联系运维人员协助排查？</p>
<p style="">（6）最近的业务访问量是否正常，是否有异常流量？</p>
<p style="">（7）服务的适用房是否有促销活动？</p>
<h5 style="" id="%EF%BC%883)%E3%80%81%E8%A7%A3%E5%86%B3%E9%97%AE%E9%A2%98">（3)、解决问题</h5>
<p style="">解决问题的阶段有时在应急处理中，有时在应急处理后。理想情况下，出现问题系统启动应急预案，每个系统会对各种问题设计止损、兜底、降级开关等策略。因此，发生严重问题先使用启用这些预案来恢复问题，之后再定位和解决问题。</p>
<p style="">解决问题当然要以定位问题为基础，必须清楚的明确分析出问题的根本原因，再提出解决问题的有效方案，切记没有明确原因之前，不要使用各种可能方法来尝试修复问题，这样可能还没有解决当前问题，可能会引出了另外一个问题。</p>
<h5 style="" id="%EF%BC%884)%E3%80%81%E6%B6%88%E9%99%A4%E5%BD%B1%E5%93%8D">（4)、消除影响</h5>
<p style="">解决问题时，某个问题可能还没有被解决就已恢复，无论在那种情况下都需要消除问题产生的影响。</p>
<h5 style="" id="%EF%BC%885)%E3%80%81%E5%A4%8D%E7%9B%98%E9%97%AE%E9%A2%98">（5)、复盘问题</h5>
<p style="">消除问题后，需要应急团队与相关方回顾事故产生的原因、应急过程的合理性，对树立处理啊的问题提出整改措施，主要聚焦一下几个问题：</p>
<p style="">（1）类似的问题还有哪些没有想到？</p>
<p style="">（2）做了哪些事情，这个事故就不会发生了？</p>
<p style="">（3）做了哪些事情，这个事故即使发生了也不会产生损失？</p>
<p style="">（4）做了哪些事情，这个事故即使法神过来，也不会产生这么大的损失？</p>
<p style="">当然，回顾事故目的不再犯类似的错误，而不是惩罚当事人。</p>
<h5 style="" id="%EF%BC%886)%E3%80%81%E9%81%BF%E5%85%8D%E6%8E%AA%E6%96%BD">（6)、避免措施</h5>
<p style="">根据回顾问题提出的改进方案和避免措施，我们必须以正式的项目管理方式进行统一管理，如果有项目经理的角色，则将避免措施和改进措施一并交给项目经理去跟进；如果没有，则请建立一个改进措施和避免措施的跟进方案和机制，否则，久而久之，问题就被忽略了。</p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E6%8A%80%E6%9C%AF%E6%94%BB%E5%85%B3%E6%96%B9%E6%B3%95%E8%AE%BA">二、技术攻关方法论</h3>
<p style="">技术攻关流程图：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1674963377207-fcefdb77-c418-4056-a878-38ee850a070b.png&amp;size=m" width="464" style="display: inline-block"></p>
<p style="">技术攻关的目标是解决问题。</p>
<p style="">从问题发生的环境和背景入手，优先考虑上述图中的提到的几个问题：</p>
<h4 style="" id="1%E3%80%81%E6%9C%80%E8%BF%91%E6%98%AF%E5%90%A6%E6%9C%89%E5%8F%98%E6%9B%B4%E3%80%81%E5%8D%87%E7%BA%A7%E6%88%96%E4%B8%8A%E7%BA%BF%E6%93%8D%E4%BD%9C%EF%BC%9F">1、最近是否有变更、升级或上线操作？</h4>
<p style="">优先考虑这一条，特别是上线完成后收到系统告警，用户反馈的相关问题及时关注，如果因上线导致出现的问题，要第一时间回滚处理，避免扩大影响。</p>
<p style="">同时，建立健全上线流程和上线评审机制，每次上线都需要有快速回滚方案。</p>
<h4 style="" id="2%E3%80%81%E4%B9%8B%E5%89%8D%E6%98%AF%E5%90%A6%E6%9C%89%E9%81%87%E5%88%B0%E8%BF%87%E7%B1%BB%E4%BC%BC%E7%9A%84%E9%97%AE%E9%A2%98%EF%BC%9F">2、之前是否有遇到过类似的问题？</h4>
<p style="">根据历史经验判断系统是否曾出现过相同或类似的问题，如果有解决类似的问题经验，可以参考快速的应用历史经验解决问题。</p>
<p style="">要求每次故障后复盘并总结故障原因，并给出问题解决方案，积累到经验库。</p>
<h4 style="" id="3%E3%80%81%E6%98%AF%E5%90%A6%E6%9C%89%E7%9B%B8%E5%85%B3%E9%A2%86%E5%9F%9F%E7%9A%84%E4%B8%93%E5%AE%B6%EF%BC%9F">3、是否有相关领域的专家？</h4>
<p style="">遇到了更深层次的问题，比如遭遇DDOS攻击、性能扛不住、网络故障、使用的中间件频繁告警等。类似问题先求助相关领域专家，他们积累了更加丰富的经验，或能更深入了解原因并快速解决问题。</p>
<p style="">以上流程仍然无法解决问题，就需要自己想办法做技术攻关了。</p>
<h3 style="" id="%E4%B8%89%E3%80%81%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90%E6%96%B9%E6%B3%95%E8%AE%BA">三、问题分析方法论</h3>
<p style="">对于任何问题的分析，需要从以下几个方面入手来分析：</p>
<p style=""><strong>简称：5W</strong></p>
<p style="">When：什么时候出现的问题？</p>
<p style="">What：什么出现了问题？</p>
<p style="">Who：谁在什么时间里发现了问题？问题影响了谁？</p>
<p style="">Where：哪里出现了问题？</p>
<p style="">Why：为什么出现了问题？</p>
<p style="">根据以上的分析，帮助你理清思路，初步对系统做判断，然后从这个系统的日志、数据、工具，并结合代码定位分析问题原因。</p>
<p style="">这里也就体现了系统中日志的重要性，好的日志能协助快速而准确的定位问题。</p>
<p style="">可以想办法「最小化复现」线上问题，最小化复现是问题产生时所依赖的组件最小化集合，容易搭建，减少了使用组件的范围，有助于迅速定位问题原因。</p>
<p style="">如果能在一个可控的环境或者仿真环境上重现问题，或者通过远程调试的手段也能协助定位问题。</p>
<p style="">定位到问题原因后，要给出解决方案。</p>
<p style="">评估解决方案对线上的影响，权衡利弊，选择最佳方案，并给出选择的原因。</p>
<p style="">将问题解决方案报备给上级进行评审，评审通过后再实施。方案需要在开发环境和QA环境进行验证，不仅仅要验证方案所解决的问题，同时，还要避免对现有功能有所影响，因此可能还需要进一步回归验证。</p>
<p style="">通过这样一系列技术攻关流程，可以保障技术攻关过程中得到完整、正确且高效的问题解决之道。</p>]]></description><guid isPermaLink="false">/archives/xian-shang-fu-wu-gu-zhang-ying-ji-ji-zhi-tao-lun</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fbug.jpg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:06:00 GMT</pubDate></item><item><title><![CDATA[从前端智能化看“低代码/无代码”]]></title><link>https://xiaoming728.com/archives/cong-qian-duan-zhi-neng-hua-kan-di-dai-ma-wu-dai-ma</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E4%BB%8E%E5%89%8D%E7%AB%AF%E6%99%BA%E8%83%BD%E5%8C%96%E7%9C%8B%E2%80%9C%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E2%80%9D&amp;url=/archives/cong-qian-duan-zhi-neng-hua-kan-di-dai-ma-wu-dai-ma" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：阿里技术</p>
<p style="">日期：2021-04-16</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd">https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;mid=2247503126&amp;idx=1&amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;mpshare=1&amp;scene=23&amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;sharer_sharetime=1618548954166&amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd</a></p>
<h2 style="" id="%E4%B8%80-%E6%A6%82%E5%BF%B5">一&nbsp; 概念</h2>
<h4 style="" id="1-%E4%BB%80%E4%B9%88%E6%98%AF%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%EF%BC%9F%E4%B8%9A%E7%95%8C%E5%AF%B9%E4%BA%8E%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E6%98%AF%E5%90%A6%E5%AD%98%E5%9C%A8%E5%85%B6%E4%BB%96%E4%B8%8D%E5%90%8C%E7%9A%84%E7%90%86%E8%A7%A3%EF%BC%9F">1 &nbsp;什么是低代码/无代码开发？业界对于低代码/无代码开发是否存在其他不同的理解？</h4>
<p style="">行业里流行观点，低代码是更加易用的搭建系统，无代码是图形化和可视化编程。这种观点把低代码和无代码开发分别置于 UI 和逻辑两个环节，以工具属性定义搭建和可视化编程要解决的问题。另一种观点则是把低代码/无代码看作一个方法的两个阶段，就像对自动驾驶的 L0 ~ L5 共 6 个不同阶段一样，把我之前在《人机协同的编程方式》[1] 一文提出的人机协同编程的概念，划分为低代码/无代码两个阶段。较之第一种我更加认同第二种观点，不仅因为是我提出的，更因为第二种观点是以软件工程的统一视角定义、分析和解决问题，而第一种观点只是局部和过程的优化而非颠覆性创新。</p>
<p style="">今天“人机协同的编程方式”把软件工程从拼装 UI 和编写业务逻辑里解放出来，逐步向业务能力、基础能力、底层能力等高技术含量工作过渡。更多内容参考《前端智能化：思维转变之路》[2]。</p>
<h4 style="" id="2-%E4%BD%8E%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E5%92%8C%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E4%B9%8B%E9%97%B4%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">2 &nbsp;低代码开发和无代码开发之间的区别是什么？</h4>
<p style="">接着上述所答，既然低代码和无代码属于“人机协同编程”的两个阶段，低代码就是阶段一、无代码则是阶段二，分别对应“人机协作”和“人机协同”。协作和协同最大的区别就是：心有灵犀。不论低代码还是无代码，均有服务的对象：用户。不论用户是程序员还是非编程人员，均有统一目标：生成代码。不论源码开发、低代码还是无代码，都是在用不同的方式描述程序，有代码、图形、DSL……等。“人机协作”的阶段，这些描述有各种限制、约束，应用的业务场景亦狭窄。“人机协同”的阶段，则限制、约束减少，应用的业务场景亦宽广。“心有灵犀”就是指：通过 AI 对描述进行学习和理解，从而减少限制和约束，适应更多业务场景。因此，传统低代码/无代码和“人机协同编程”生成代码相比，最大的不同就是有心和无心，机器有心而平台无心。</p>
<h2 style="" id="%E4%BA%8C-%E8%83%8C%E6%99%AF">二&nbsp; 背景</h2>
<h4 style="" id="1-%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E4%B8%8E%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E9%A2%86%E5%9F%9F%E7%9A%84%E4%B8%80%E4%BA%9B%E7%BB%8F%E5%85%B8%E6%80%9D%E6%83%B3%E3%80%81%E6%96%B9%E6%B3%95%E5%92%8C%E6%8A%80%E6%9C%AF%EF%BC%8C%E4%BE%8B%E5%A6%82%E8%BD%AF%E4%BB%B6%E5%A4%8D%E7%94%A8%E4%B8%8E%E6%9E%84%E4%BB%B6%E7%BB%84%E8%A3%85%E3%80%81%E8%BD%AF%E4%BB%B6%E4%BA%A7%E5%93%81%E7%BA%BF%E3%80%81dsl%EF%BC%88%E9%A2%86%E5%9F%9F%E7%89%B9%E5%AE%9A%E8%AF%AD%E8%A8%80%EF%BC%89%E3%80%81%E5%8F%AF%E8%A7%86%E5%8C%96%E5%BF%AB%E9%80%9F%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E3%80%81%E5%8F%AF%E5%AE%9A%E5%88%B6%E5%B7%A5%E4%BD%9C%E6%B5%81%EF%BC%8C%E4%BB%A5%E5%8F%8A%E6%AD%A4%E5%89%8D%E4%B8%9A%E7%95%8C%E6%B5%81%E8%A1%8C%E7%9A%84%E4%B8%AD%E5%8F%B0%E7%AD%89%E6%A6%82%E5%BF%B5%EF%BC%8C%E4%B9%8B%E9%97%B4%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB%EF%BC%9F">1 &nbsp;低代码/无代码开发与软件工程领域的一些经典思想、方法和技术，例如软件复用与构件组装、软件产品线、DSL（领域特定语言）、可视化快速开发工具、可定制工作流，以及此前业界流行的中台等概念，之间是什么关系？</h4>
<p style="">从库、框架、脚手架开始，软件工程就踏上了追求效率的道路。在这个道路之上，低代码、无代码的开发方式算是宏愿。复用、组件化和模块化、DSL、可视化、流程编排……都是在达成宏愿过程中的尝试，要么在不同环节、要么以不同方式，但都还在软件工程领域内思考。中台概念更多是在业务视角下提出的，软件工程和技术领域内类似的概念更多是叫：平台。不论中台还是平台，就不仅是在过程中的尝试，而是整体和系统的创新尝试。我提出前端智能化的“人机协同编程”应该同属于软件工程和技术领域，在类似中台的业务领域我提出“需求暨生产”的全新业务研发模式，则属于业务领域。这些概念之间无非：左右、上下、新旧关系而已。</p>
<h4 style="" id="2-%E6%AD%A4%E5%A4%96%EF%BC%8C%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E4%B8%8Edevops%E3%80%81%E4%BA%91%E8%AE%A1%E7%AE%97%E4%B8%8E%E4%BA%91%E5%8E%9F%E7%94%9F%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%97%B4%E5%8F%88%E6%98%AF%E4%BB%80%E4%B9%88%E6%A0%B7%E7%9A%84%E5%85%B3%E7%B3%BB%EF%BC%9F">2 &nbsp;此外，低代码/无代码开发与DevOps、云计算与云原生架构之间又是什么样的关系？</h4>
<p style="">DevOps、云计算……都属于基础技术，基础技术的变化势必带来上层应用层技术变化。没有云计算的容器化、弹性缩扩容，做分布式系统是很困难的，尤其在 CI/CD、部署、运维、监控、调优……等环节更甚，什么南北分布、异地多活、平行扩展、高可用……都需要去关注。但是，云计算和DevOps等基础技术的发展，内化并自动化解决了上述问题，大大降低了关注和使用成本，这就是心有灵犀，在这样的基础技术之上构建应用层技术，限制少、约束小还能适应各种复杂场景。</p>
<h2 style="" id="%E4%B8%89-%E6%80%9D%E6%83%B3%E6%96%B9%E6%B3%95">三&nbsp; 思想方法</h2>
<h4 style="" id="1-%E6%94%AF%E6%92%91%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E7%9A%84%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">1 &nbsp;支撑低代码/无代码开发的核心技术是什么？</h4>
<p style="">我认为低代码/无代码开发的核心技术，过去是“复用”，今天是 AI 驱动的“人机协同编程”。过去的低代码/无代码开发多围绕着提升研发效能入手，今天 AI 驱动的“人机协同编程”则是围绕着提升交付效率入手。因此，低代码/无代码开发以“人机协同编程”为主要实现手段的话，AI 是其核心技术。</p>
<h4 style="" id="2-%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E7%9A%84%E7%81%AB%E7%83%AD%E6%98%AF%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF%E4%B8%8A%E7%9A%84%E9%87%8D%E8%A6%81%E5%8F%98%E9%9D%A9%E5%92%8C%E7%AA%81%E7%A0%B4%EF%BC%8C%E8%BF%98%E6%98%AF%E7%BB%8F%E5%85%B8%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E6%80%9D%E6%83%B3%E3%80%81%E6%96%B9%E6%B3%95%E5%92%8C%E6%8A%80%E6%9C%AF%E9%9A%8F%E7%9D%80%E6%8A%80%E6%9C%AF%E5%92%8C%E4%B8%9A%E5%8A%A1%E7%A7%AF%E7%B4%AF%E7%9A%84%E4%B8%8D%E6%96%AD%E5%8F%91%E5%B1%95%E8%80%8C%E7%84%95%E5%8F%91%E5%87%BA%E7%9A%84%E6%96%B0%E7%94%9F%E6%9C%BA%EF%BC%9F">2 &nbsp;低代码/无代码开发的火热是软件开发技术上的重要变革和突破，还是经典软件工程思想、方法和技术随着技术和业务积累的不断发展而焕发出的新生机？</h4>
<p style="">计算机最初只在少数人掌握，如今，几乎人人手持一台微型计算机：智慧手机。当初为程序员和所谓“技术人员”的专利，而今，几乎人人都会操作和使用计算机。然而，人们对计算机的操作是间接的，需要有专业的人士和企业提前编写软件，人们通过软件使用计算机的各种功能。随着计算机算力和功能的不断发展，随着社会的数字化和信息化，今天的人们越来越难以被提前定制好的软件所满足。低代码/无代码开发则赋予人们创造软件的能力，进而帮助人们低成本、即时、高效的直接生产符合自己需求的软件，进而操作众多复杂的电子设备和数字世界建立联结。我认为，这是不可逆的趋势，也是低代码/无代码开发的大方向。</p>
<h2 style="" id="%E5%9B%9B-%E7%8E%B0%E7%8A%B6%E8%BF%9B%E5%B1%95">四&nbsp; 现状进展</h2>
<h4 style="" id="1-%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E5%B7%B2%E7%BB%8F%E5%8F%91%E5%B1%95%E5%88%B0%E4%BB%80%E4%B9%88%E7%A8%8B%E5%BA%A6%EF%BC%9F">1 &nbsp;低代码/无代码开发已经发展到什么程度？</h4>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fwebp%2F550960%2F1618555286690-cd738d8a-2ca5-4a2f-a988-cebf44ce0805.webp&amp;size=m" width="1080" style="display: inline-block"></p>
<p style="">imgcook</p>
<ul>
 <li>
  <p style="">2w 多用户、6w 多模块、 0 前端参与研发的双十一等大促营销活动、70% 阿里前端在使用</p></li>
 <li>
  <p style="">79.26% 无人工参与的线上代码可用率、90.9% 的还原度、Icon 识别准确率 83%、组件识别 85%、布局还原度 92.1%、布局人工修改概率 75%</p></li>
 <li>
  <p style="">研发效率提升 68%</p></li>
</ul>
<p style=""></p>
<p style="">uicook</p>
<ul>
 <li>
  <p style="">营销活动和大促场景 ui 智能生成比例超过 90%</p></li>
 <li>
  <p style="">日常频道导购业务 ui 智能生成覆盖核心业务</p></li>
 <li>
  <p style="">纯 ui 智能化和个性化带来的业务价值提升超过 8%</p></li>
</ul>
<p style=""></p>
<p style="">bizcook</p>
<ul>
 <li>
  <p style="">初步完成基于 NLP 的需求标注和理解系统</p></li>
 <li>
  <p style="">初步完成基于 NLP 的服务注册和理解系统</p></li>
 <li>
  <p style="">初步完成基于 NLP 的胶水层业务逻辑代码生成能力</p></li>
</ul>
<p style=""></p>
<p style="">reviewcook</p>
<ul>
 <li>
  <p style="">针对资损防控自动化扫描、CV 和 AI 自动化识别资损风险和舆情问题</p></li>
 <li>
  <p style="">和测试同学共建的 UI 自动化测试、数据渲染和 Mock 驱动的业务自动化验证</p></li>
 <li>
  <p style="">和工程团队共建的 AI Codereview 基于对代码的分析和理解，结合线上 Runtime 的识别和分析，自动化发现问题、定位问题，提升 Codereview 的效率和质量</p></li>
</ul>
<p style=""></p>
<p style="">datacook</p>
<ul>
 <li>
  <p style="">社区化运营开源项目，合并 Denfo.js 同其作者共同设立 Datacook 项目，全链路、端到端解决 AI 领域数据采集、存储、处理问题，尤其在海量数据、数据集组织、数据质量评估等深度学习和机器学习领域的能力比肩 HDF5、Pandas……等 Python 专业 LIbrary</p></li>
 <li>
  <p style="">Google Tensorflow.js 团队合作开发维护 TFData library ，作为 Datacook 的核心技术和基础，共同构建数据集生态和数据集易用性</p></li>
</ul>
<p style=""><br>
 pipcook</p>
<ul>
 <li>
  <p style="">开源了 pipcook[3]&nbsp;纯前端机器学习框架</p></li>
 <li>
  <p style="">利用 Boa 打通 Python 技术生态，原生支持 import Python 流行的包和库，原生支持 Python 的数据类型和数据结构，方便跨语言共享数据和调用 API</p></li>
 <li>
  <p style="">利用 Pipcook Cloud 打通流行的云计算平台，帮助前端智能化实现 CDML，形成数据和算法工程闭环，帮助开发者打造工业级可用的服务和在线、离线算法能力</p></li>
</ul>
<p style=""></p>
<h4 style="" id="2-%E6%9C%89%E5%93%AA%E4%BA%9B%E6%88%90%E7%86%9F%E7%9A%84%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E5%B9%B3%E5%8F%B0%EF%BC%9F">2 &nbsp;有哪些成熟的低代码/无代码开发平台？</h4>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">平台类型</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">名称及网址</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="19" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">设计稿生成代码相关</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">yotako</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">zecoda</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">avocode</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">codefun</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Quest AI</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Uizard</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Zeplin</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Supernova Studio</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Inspect</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Hadron</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Teleport</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Clutch</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Anima app</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Figma’s integration with Framer Web</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Visly</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Handoff</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Relate</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">Modulz</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">蓝湖</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="25" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">面向 PD 的需求生成代码相关</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">xiaopiu.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">墨刀</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">&nbsp;iH5</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">thunkable.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.shoutem.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.applandr.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.appmachine.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">buildfire.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.appsheet.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.dropsource.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">bubble.is</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">webflow.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">1.shopifytrack.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">carrd.co</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.webydo.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">ekko.site</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">my.readymag.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.pixpa.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.berta.me</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">mbsy.co</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">tilda.cc</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.squarespace.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: justify; "><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">www.sheet2site.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">cn.strikingly.com</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">https://docs.aws.amazon.com/zh_cn/step-functions/latest/dg/welcome.html</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="2" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">代码推荐相关</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">https://tabnine.com/</span></a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd"><span fontsize="" color="rgb(62, 62, 62)" style="color: rgb(62, 62, 62)">https://github.com/taoyds/typesql</span></a></p></td>
  </tr>
 </tbody>
</table>
<p style=""></p>
<h4 style="" id="3-%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E8%83%BD%E5%A4%9F%E5%9C%A8%E5%A4%9A%E5%A4%A7%E7%A8%8B%E5%BA%A6%E4%B8%8A%E6%94%B9%E5%8F%98%E5%BD%93%E5%89%8D%E7%9A%84%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E6%96%B9%E5%BC%8F%EF%BC%9F">3 &nbsp;低代码/无代码开发能够在多大程度上改变当前的软件开发方式？</h4>
<p style="">随着计算机算力和功能的不断发展，随着社会的数字化和信息化，今天的人们越来越难以被提前定制好的软件所满足。低代码/无代码开发则赋予人们创造软件的能力，进而帮助人们低成本、即时、高效的直接生产符合自己需求的软件，进而操作众多复杂的电子设备和数字世界建立联结。我认为，这是不可逆的趋势，也是低代码/无代码开发的大方向。最终，软件开发势必从专业程序员手里转向普罗大众，成为今天操作计算机一样的基本生存技能之一。因此，软件开发方式将带来本质变化，从完整的交付转向局部交付、从业务整体交付转向业务能力交付……</p>
<h3 style="" id="%E4%BA%94-%E5%B1%95%E6%9C%9B%E6%9C%AA%E6%9D%A5">五&nbsp; 展望未来</h3>
<h4 style="" id="1-%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E6%9C%AA%E6%9D%A5%E5%8F%91%E5%B1%95%E7%9A%84%E6%96%B9%E5%90%91%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">1 &nbsp;低代码/无代码开发未来发展的方向是什么？</h4>
<p style="">要我说，低代码/无代码开发未来发展的方向一定是：AI 驱动的“人机协同编程”，将完整开发一个软件变成提供局部的软件功能，类似 Apple 的“捷径”一样，由用户决定这些局部软件功能如何组装成适合用户的软件并交付最终用户。AI 驱动提供两个方面的价值：</p>
<p style="">降低开发成本</p>
<p style="">以往开发软件的时候，要有 PRD、交互稿、设计稿、设计文档……等一系列需求规格说明，然后，根据这些需求规格利用技术和工程手段进行实现。然而，低代码/无代码开发交付的是局部功能和半成品，会被无法枚举的目的和环境所使用，既然无法枚举，就不能用 Swith……Case 的方式编写代码，否则会累死。</p>
<p style="">AI 的特点就是基于特征和环境进行预测，预测的基础是对模式和本质的理解。就像 AI 识别一只猫，不管这个猫在什么环境、什么光照条件下，也不管这只猫是什么品种，AI 都能够以超过人类的准确度识别。试想，作为一个程序员用程序判断一只猫的开发成本何其高？</p>
<p style="">降低使用成本</p>
<p style="">今天的搭建体系，本质上是把编程过程用搭建的思想重构了一遍，工作的内容并没有发生变化，成本从程序员转嫁到运营、产品、设计师的身上。这还是其次，今天的搭建平台都是技术视角出发，充斥着运营、产品、设计等非技术人员一脸懵逼的概念，花在答疑解惑和教他们如何在页面上定制一个搜索框的时间，比自己和他们沟通后源码实现的时间还要长，而且经常在撸代码的时候被打断……</p>
<p style="">基于 AI 的“人机协同编程”不需要透出任何技术概念，运营、产品、设计……等非技术人员也不改变其工作习惯，都用自己熟悉的工具和自己熟悉的概念描述自己的需求，AI 负责对这些需求进行识别和理解，再转换成编程和技术工程领域的概念，进而生成代码并交付，从而大幅度降低使用成本。</p>
<p style="">举个例子：如果你英文写作能力不好，你拿着朗道词典一边翻译一边拼凑单词写出来的英文文章质量高呢？还是用中文把文章写好，再使用 Google 翻译整篇转换成英文的文章质量高？你自己试试就知道了。究其原因，你在自己熟悉的语言和概念领域内，才能够把自己的意思表达清楚。</p>
<h4 style="" id="2-%E5%9B%B4%E7%BB%95%E4%BD%8E%E4%BB%A3%E7%A0%81%2F%E6%97%A0%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E5%AD%98%E5%9C%A8%E5%93%AA%E4%BA%9B%E6%8A%80%E6%9C%AF%E9%9A%BE%E9%A2%98%E9%9C%80%E8%A6%81%E5%AD%A6%E6%9C%AF%E7%95%8C%E5%92%8C%E5%B7%A5%E4%B8%9A%E7%95%8C%E5%85%B1%E5%90%8C%E6%8E%A2%E7%B4%A2%EF%BC%9F">2 &nbsp;围绕低代码/无代码开发存在哪些技术难题需要学术界和工业界共同探索？</h4>
<p style="">最初在 D2 上提出并分享“前端智能化”这个概念的时候，我就提出：识别、理解、表达 这个核心过程。我始终认为，达成 AI 驱动的“人机协同编程”关键路径就是：识别、理解、表达。因此，围绕 AI 识别、 AI 理解、 AI 表达我们和国内外知名大学展开了广泛的合作。<br>
 识别</p>
<ul>
 <li>
  <p style="">需求的识别：通过 NLP 、知识图谱、图神经网络、结构化机器学习……等 AI 技术，识别用户需求、产品需求、设计需求、运营需求、营销需求、研发需求、工程需求……等，识别出其中的概念和概念之间的关系</p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style="">设计稿的识别：通过 CV、GAN、对象识别、语义分割……等 AI 技术，识别设计稿中的元素、元素之间的关系、设计语言、设计系统、设计意图</p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style="">UI 的识别：通过用户用脚投票的结果进行回归，后验的分析识别出 UI 对用户行为的影响程度、影响效果、影响频率、影响时间……等，并识别出 UI 的可变性和这些用户行为影响之间的关系</p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style="">计算机程序的识别：通过对代码、AST ……等 Raw Data 分析，借助 NLP 技术识别计算机程序中，语言的表达能力、语言的结构、语言中的逻辑、语言和外部系统通过 API 的交互等</p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style="">日志和数据的识别：通过对日志和数据进行 NLP、回归、统计分析等方式，识别出程序的可用性、性能、易用性等指标情况，并识别出影响这些指标的日志和数据出自哪里，找出其间的关系</p></li>
</ul>
<p style=""><br>
 理解</p>
<ul>
 <li>
  <p style="">横向跨领域的理解：对识别出的概念进行降维，从而在底层更抽象的维度上找出不同领域之间概念的映射关系，从而实现用不同领域的概念进行类比，进而在某领域内理解其它领域的概念</p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style="">纵向跨层次的理解：利用机器学习和深度学习的 AI 算法能力，放宽不同层次间概念的组成关系，对低层次概念实现跨层次的理解，进而形成更加丰富的技术、业务能力供给和使用机会</p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style="">常识、通识的理解：以常识、通识构建的知识图谱为基础，将 AI 所面对的开放性问题领域化，将领域内的常识和通识当做理解的基础，不是臆测和猜想，而是实实在在构建在理论基础上的理解</p></li>
</ul>
<p style=""><br>
 表达</p>
<ul>
 <li>
  <p style="">个性化：借助大数据和算法实现用户和软件功能间的匹配，利用 AI 的生成能力降低千人前面的研发成本，从而真正实现个性化的软件服务能力，把软件即服务推向极致</p></li>
</ul>
<p style=""></p>
<ul>
 <li>
  <p style="">共情：利用端智能在用户侧部署算法模型，既可以解决用户隐私保护的问题，又可以对用户不断变化的情绪、诉求、场景及时学习并及时做出响应，从而让软件从程序功能的角度急用户之所急、想用户之所想，与用户共情、让用户共鸣。举个例子：我用 iPhone 在进入地铁站的时候，因为现在要检查健康码，每次进入地铁站 iOS 都会给我推荐支付宝快捷方式，我不用自己去寻找支付宝打开展示健康码，这就让我感觉 iOS 很智能、很贴心，这就是共情。</p></li>
</ul>
<p style=""></p>
<h2 style="" id="%E5%85%AD-%E5%90%8E%E8%AE%B0">六&nbsp; 后记</h2>
<p style="">从提出前端智能化这个概念到现在已历三年，最初，保持着“让前端跟上 AI 发展的浪潮”的初心上路，到“解决一线研发问题”发布[4]，再到“给前端靠谱的机器学习框架”开源[3] ，这一路走来，几乎日日夜不能寐。真正想从本质上颠覆现在的编程模式和研发模式谈何容易？这个过程中，我们从一群纯前端变成前端和 AI 的跨界程序员，开发方式从写代码到机器生成，周围的人从作壁上观到积极参与，正所谓：念念不忘，必有回响。低代码/无代码开发方兴未艾，广大技术、科研人员在这个方向上厉兵秣马，没有哪个方法是 Silverbullet ，也没有哪个理论是绝对正确的，只要找到你心中所爱，坚持研究和实践，终会让所有人都能够自定义软件来操作日益复杂和强大的硬件设备，终能让所有人更加便捷、直接、有效的接入数字世界，终于在本质上将软件开发和软件工程领域重新定义！共勉！</p>
<p style=""></p>
<p style="">相关链接</p>
<p style="">[1]<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd">https://juejin.cn/post/6844904116708196365</a></p>
<p style="">[2]<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd">https://juejin.cn/post/6844904104448393223</a></p>
<p style="">[3]<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd">https://github.com/alibaba/pipcook</a></p>
<p style="">[4]<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247503126&amp;amp;idx=1&amp;amp;sn=cdc9a859de191375542e08e52fdcd27c&amp;amp;chksm=e92af019de5d790f98a52cbbd9e4b351272c13188797e6cb12a9c91cfd280dbc8ca935f4f90f&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0416xi0PCky4Lqm8Ee5Wtqqd&amp;amp;sharer_sharetime=1618548954166&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd">http://imgcook.com</a>&nbsp;</p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/cong-qian-duan-zhi-neng-hua-kan-di-dai-ma-wu-dai-ma</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fno-code.jpeg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:04:23 GMT</pubDate></item><item><title><![CDATA[如何搭建一支拖垮公司的技术团队]]></title><link>https://xiaoming728.com/archives/ru-he-da-jian-yi-zhi-tuo-kua-gong-si-de-ji-shu-tuan-dui</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%A6%82%E4%BD%95%E6%90%AD%E5%BB%BA%E4%B8%80%E6%94%AF%E6%8B%96%E5%9E%AE%E5%85%AC%E5%8F%B8%E7%9A%84%E6%8A%80%E6%9C%AF%E5%9B%A2%E9%98%9F&amp;url=/archives/ru-he-da-jian-yi-zhi-tuo-kua-gong-si-de-ji-shu-tuan-dui" width="1" height="1" alt="" style="opacity:0;">
<p style="">作者&nbsp;l&nbsp;&nbsp;Mr.K&nbsp;&nbsp;</p>
<p style="">来源 l 技术领导力（ID：jishulingdaoli）&nbsp;&nbsp;</p>
<p style="">链接 l <a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzAwMDAwNjk4Ng==&amp;amp;mid=2657234527&amp;amp;idx=2&amp;amp;sn=c552733e82122e20651f23b09025f088&amp;amp;chksm=8179007ab60e896c0005d154608d5a91919800118fec2f16b20e3ddbb385f9818d997c0bc504&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=1219AqfkqNukmxtA4kex2yRI&amp;amp;sharer_sharetime=1608361834040&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd">https://mp.weixin.qq.com/s?__biz=MzAwMDAwNjk4Ng==&amp;mid=2657234527&amp;idx=2&amp;sn=c552733e82122e20651f23b09025f088&amp;chksm=8179007ab60e896c0005d154608d5a91919800118fec2f16b20e3ddbb385f9818d997c0bc504&amp;mpshare=1&amp;scene=23&amp;srcid=1219AqfkqNukmxtA4kex2yRI&amp;sharer_sharetime=1608361834040&amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd</a></p>
<p style=""></p>
<p style="">尼采说过，"What does not kill me,makes me stronger."，突然来句英文，是为了显得高端，意思就是：杀不死你的，终将使你强大。</p>
<p style=""></p>
<p style="">俗话说，一朝被蛇咬，十年里就学会了剥蛇皮、吃蛇肉。老K也逐渐学会了吊打业务方、折磨产品经理。</p>
<p style=""></p>
<p style="">言归正传，本文聊一个硬核的话题：如何搭建一支拖垮公司的技术团队？</p>
<p style=""></p>
<p style="">你没看错，是搭建一支拖垮公司的技术团队。仔细研究后发现，这太有技术含量了：老板不傻、业务方猴精似的，想在他们眼皮底下使坏，没点真本事还真不行。</p>
<p style=""></p>
<p style="">搞技术的，最喜欢做有技术含量的事情，就像鲨鱼闻到了血腥、干柴遇上烈火。</p>
<p style=""></p>
<p style="">闲话少说，进入正题。</p>
<p style=""></p>
<p style="">首先明确下定义，这里的公司指的是中小型公司。因为大公司有的是钱，即使错了也有足够的时间慢慢调整，用不着我们操心。</p>
<p style=""></p>
<p style="">中小型公司就不一样了，业务规模小、发展不稳定、甚至连生存都成问题，总结下来就是一个字：穷。所以根本经不起折腾。</p>
<p style=""></p>
<p style="">老K见过不少中小型公司，就因为瞎折腾技术团队，结果倒在了去敲钟的路上，非常可惜。</p>
<p style=""></p>
<p style="">从这些血和泪的教训中，总结了拖垮公司的技术团队常用的7个操作。招式歹毒，请谨慎使用：</p>
<p style=""></p>
<h3 style="" id="%E4%B8%80%E3%80%81%E5%8E%BBbat%E6%8C%96%E6%8A%80%E6%9C%AF%E7%89%9B%E4%BA%BA">一、去BAT挖技术牛人</h3>
<p style="">许多刚融了资的中小企业，创始人有了几个臭钱就开始膨胀，去大厂挖了些螺丝钉回来，想给自家公司造航母。不仅挖来的人发挥不了作用，还激怒了一起奋斗的老员工：舍不得给自家兄弟加工资，倒是给外人吃香喝辣的。</p>
<p style=""></p>
<p style="">创始人这就叫：偷鸡不成，还被鸡鄙视，得不偿失啊。</p>
<p style=""></p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E6%90%9E%E6%95%8F%E6%8D%B7%E5%BC%80%E5%8F%91">二、搞敏捷开发</h3>
<p style="">搞敏捷开发没有问题，但是要搞清楚一点，敏捷开发对团队成员的能力素质是有要求的，除非你的团队里有经验丰富的Scrum Master，否则不建议轻易尝试。</p>
<p style=""></p>
<p style="">中小型公司的研发流程很简单：</p>
<p style="">1）产品经理有一个月的需求计划（倒逼业务方做计划，别想一出是一出）；</p>
<p style="">2）Leader每两天检查一次项目进度；</p>
<p style="">3）每两周做一次Code Review；</p>
<p style="">4）每周发一次版，紧急情况发两次。</p>
<p style=""></p>
<p style="">按这个节奏来，不瞎造通常问题都不大。</p>
<p style=""></p>
<h3 style="" id="%E4%B8%89%E3%80%81%E4%B8%A5%E6%A0%BC%E9%81%B5%E5%BE%AA%E8%A7%92%E8%89%B2%E9%85%8D%E6%AF%94">三、严格遵循角色配比</h3>
<p style="">中小型公司资源匮乏，非要按照1：5：1的配比来配置产品、开发、测试人员，除了浪费成本之外，对研发效率没有任何帮助。</p>
<p style=""></p>
<p style="">开发人员要求全栈，前后端都搞，能不分工的就不要分工，一个萝卜一个坑，中小型公司更养不起闲人。</p>
<p style=""></p>
<h3 style="" id="%E5%9B%9B%E3%80%81%E7%8B%A0%E6%8A%93%E6%8A%80%E6%9C%AF%E7%AE%A1%E7%90%86">四、狠抓技术管理</h3>
<p style="">二三十号人，你搞个屁技术管理啊？如果Leader有闲工夫做技术管理，就说明他没在干活。</p>
<p style=""></p>
<p style="">中小型技术团队的管理很简单：</p>
<p style="">1）组织：按模块分好开发小组；</p>
<p style="">2）管控：每周开周会；</p>
<p style="">3）执行：不出活的员工警告两次，不行就干掉，</p>
<p style="">4）激励：表现好的年底发双薪，给他加薪；</p>
<p style="">5）氛围：Leader要写代码。</p>
<p style=""></p>
<h3 style="" id="%E4%BA%94%E3%80%81%E9%BC%93%E5%90%B9%E5%88%9B%E4%B8%9A%E6%96%87%E5%8C%96">五、鼓吹创业文化</h3>
<p style="">除非技术人员都是创始团队成员，否则别瞎鼓吹创业文化，你和员工之间就是利益关系，不是合伙人关系。</p>
<p style=""></p>
<p style="">别老劝技术人员静下心来写代码，没有钱包的充实，哪来内心的宁静？</p>
<p style=""></p>
<p style="">提醒各位程序员，跟你谈钱的老板，才是好老板；跟你谈理想的都TM不想给你钱！</p>
<p style=""></p>
<h3 style="" id="%E5%85%AD%E3%80%81%E6%8C%89%E4%BB%A3%E7%A0%81%E8%A1%8C%E6%95%B0%EF%BC%8C%E5%AE%9E%E8%A1%8C%E7%BB%A9%E6%95%88%E8%80%83%E6%A0%B8">六、按代码行数，实行绩效考核</h3>
<p style="">中小型团队不建议实行复杂的绩效考核方式，更别异想天开的按代码行数计工资，否则程序员会让你怀疑人生。就那么几十个人，谁出多少活，自己心里还没点B Tree吗？</p>
<p style=""></p>
<p style="">中小型团队激励员工的方式：干得好的给他升职加薪，干不好的卷铺盖走人。都是职场成年人，简单点对大家都好。</p>
<p style=""></p>
<h3 style="" id="%E4%B8%83%E3%80%81%E4%B8%8A%E4%B8%AD%E5%8F%B0">七、上中台</h3>
<p style="">屁大点业务，就别整啥中台、微服务架构，那TM不是你能玩的得起的。往商业计划里写写，骗骗资本方就算了，自己别当真。</p>
<p style=""></p>
<p style="">头两年的业务系统，怎么简单怎么来，以堆功能为主。</p>
<p style=""></p>
<p style="">各位老板也别追求什么极致产品体验，就那几杆破枪，没法极致。如果老板连这点智商都没有，你也别跟着瞎折腾了，前面是死路一条。</p>
<p style=""></p>
<p style="">最后，奉劝各位中小型公司的技术人，假如生活欺骗了你了，不要伤心不要放弃，因为明天生活还会继续诈骗你。</p>]]></description><guid isPermaLink="false">/archives/ru-he-da-jian-yi-zhi-tuo-kua-gong-si-de-ji-shu-tuan-dui</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E5%25A6%2582%25E4%25BD%2595%25E6%258B%2596%25E5%259E%25AE%25E5%259B%25A2%25E9%2598%259F.jpg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:04:00 GMT</pubDate></item><item><title><![CDATA[七种开源许可证]]></title><link>https://xiaoming728.com/archives/qi-zhong-kai-yuan-xu-ke-zheng</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E4%B8%83%E7%A7%8D%E5%BC%80%E6%BA%90%E8%AE%B8%E5%8F%AF%E8%AF%81&url=/archives/qi-zhong-kai-yuan-xu-ke-zheng" width="1" height="1" alt="" style="opacity:0;" /><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">相信大家在玩 node ，webpack，npm，bower ..etc 时，都会在它们的 package.json 文件中，或者 init 过程中都会遇到这样的一个键值对：</span></p><p style=""><span style="font-size: 12px; color: rgb(199, 37, 78)">"license": "MIT",</span></p><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">这个</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">license</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">就是开源许可说明，各大组织设立了为代码开源许可的规范文档，当作者声明此文档类型时，他人必须遵守该文档类型的规范。（当然啦这是君子条款）</span></p><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">一张图说解释所有（采用请标明作者）：</span></p><p style=""><img src="https://cdn.nlark.com/yuque/0/2021/jpg/550960/1618471235514-a0c5c81f-9d43-49bb-bd1d-107e17319409.jpg" width="2425" style="display: inline-block"></p><p style=""><span style="font-size: 13px; color: rgb(153, 153, 153)">七种开源许可.jpg</span></p><h2 style="" id="apache%E8%AE%B8%E5%8F%AF"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">Apache许可</span></h2><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">Apache许可证(Apache License)，是一个在Apache软件基金会发布的自由软件许可证，最初为Apache http服务器而撰写。Apache许可证要求被授权者保留版权和放弃权利的申明，但它不是一个反版权的许可证。</span></p><p style=""><img src="https://cdn.nlark.com/yuque/0/2021/png/550960/1618471235650-7cb67960-96f5-4e2f-a0e9-4a8529dd9e0f.png" width="429" style="display: inline-block"></p><p style=""><span style="font-size: 13px; color: rgb(153, 153, 153)">Apache许可标志</span></p><p style=""></p><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">当前版本</span> Apache <span style="font-size: 16px">License, Version 2.0</span></p><h2 style="" id="mit%E8%AE%B8%E5%8F%AF">MIT许可</h2><ul><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">MIT许可证之名源自麻省理工学院（Massachusetts Institute of Technology, MIT），又称“X条款”（X License）或“X11条款”（X11 License）。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">MIT是和BSD一样宽范的许可协议，作者只想保留版权,而无任何其他了限制。也就是说，你必须在你的发行版里包含原许可协议的声明，无论你是以二进制发布的还是以源代码发布的。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">当前版本</span> <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://opensource.org/licenses/mit-license.php">The MIT License</a></p></li></ul><h2 style="" id="isc%E8%AE%B8%E5%8F%AF"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">ISC许可</span></h2><ul><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">ISC许可证是一种开放源代码许可证，在功能上与两句版的BSD许可证相同。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">这份许可证是由ISC（Internet Systems Consortium）所发明，在ISC释出软件时所使用的。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">当前版本</span> <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href=" https://opensource.org/licenses/ISC">ISC License (ISC)</a></p></li></ul><h2 style="" id="bsd%E8%AE%B8%E5%8F%AF"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">BSD许可</span></h2><ul><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">BSD开源协议（original BSD license、FreeBSD license、Original BSD license）是一个给于使用者很大自由的协议，BSD 代码鼓励代码共享，但需要尊重代码作者的著作权。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">BSD由于允许使用者修改和重新发布代码，也允许使用或在BSD代码上开发商业软件发布和销售，因此是对商业集成很友好的协议。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">当前版本</span> <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href=" https://opensource.org/licenses/BSD-2-Clause">The 2-Clause BSD License</a></p></li></ul><h2 style="" id="gpl%E8%AE%B8%E5%8F%AF"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">GPL许可</span></h2><ul><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">GPL，是GNU General Public License的缩写，是GNU通用公共授权非正式的中文翻译。它并非由自由软件基金会所发表，亦非使用GNU通用公共授权的软件的法定发布条款</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">只有GNU通用公共授权英文原文的版本始具有此等效力。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">当前版本</span> <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://links.jianshu.com/go?to=http%3A%2F%2Fwww.gnu.org%2Fcopyleft%2Fgpl.html">GNU General Public License</a></p></li></ul><h2 style="" id="mozilla%E8%AE%B8%E5%8F%AF"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">Mozilla许可</span></h2><ul><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">MPL是The Mozilla Public License的简写，是1998年初Netscape的 Mozilla小组为其开源软件项目设计的软件许可证。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">MPL许可证出现的最重要原因就是，Netscape公司认为GPL许可证没有很好地平衡开发者对 源代码的需求和他们利用源代码获得的利益。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">当前版本</span> <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://links.jianshu.com/go?to=https%3A%2F%2Fwww.mozilla.org%2Fen-US%2FMPL%2F">Mozilla Public License</a></p></li></ul><h2 style="" id="lgpl%E8%AE%B8%E5%8F%AF"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">LGPL许可</span></h2><ul><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">LGPL是 GNU Lesser General Public License (GNU 宽通用公共许可证)的缩写形式，旧称GNU Library General Public License (GNU 库通用公共许可证),后来改称作Lesser GPL，即为更宽松的GPL，在宽松程度上与BSD, Apache,XFree86 许可证相似。</span></p></li><li><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">当前版本</span> <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://links.jianshu.com/go?to=https%3A%2F%2Fwww.gnu.org%2Fcopyleft%2Flesser.html">Lesser General Public License</a></p></li></ul><h2 style="" id="%E5%85%B6%E4%BB%96%E7%B1%BB%E5%9E%8B"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">其他类型</span></h2><p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">点击进入</span> <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://links.jianshu.com/go?to=https%3A%2F%2Fspdx.org%2Flicenses%2F">SPDX License List</a> <span style="font-size: 16px; color: rgb(64, 64, 64)">看到更多协议类型～</span></p><p style=""></p>]]></description><guid isPermaLink="false">/archives/qi-zhong-kai-yuan-xu-ke-zheng</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E5%25BC%2580%25E6%25BA%2590%25E8%25AE%25B8%25E5%258F%25AF%25E8%25AF%2581.jpg&amp;size=m" type="image/jpeg" length="0"/><category>开源工具</category><category>技术杂文</category><pubDate>Fri, 12 Jan 2024 13:03:00 GMT</pubDate></item><item><title><![CDATA[Maven依赖范围scope详解]]></title><link>https://xiaoming728.com/archives/mavenyi-lai-fan-wei-scopexiang-jie</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Maven%E4%BE%9D%E8%B5%96%E8%8C%83%E5%9B%B4scope%E8%AF%A6%E8%A7%A3&amp;url=/archives/mavenyi-lai-fan-wei-scopexiang-jie" width="1" height="1" alt="" style="opacity:0;">
<p style="">来源：CSDN - <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://liaowenxiong.blog.csdn.net/">liaowenxiong</a></p>
<p style="">发布： 2022-12-10 10:38:28</p>
<p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://liaowenxiong.blog.csdn.net/">https://blog.csdn.net/liaowenxiong/article/details/122350988</a></p>
<h3 style="" id="%E4%B8%80%E3%80%81%E4%BE%9D%E8%B5%96%E8%8C%83%E5%9B%B4">一、依赖范围</h3>
<p style="">maven 项目不同的阶段引入到classpath中的依赖是不同的。</p>
<p style="">例如：<br>
 编译时，maven 会将与编译相关的依赖引入classpath中；<br>
 测试时，maven会将测试相关的的依赖引入到classpath中；<br>
 运行时，maven会将与运行相关的依赖引入classpath中。</p>
<p style="">依赖范围就是用来控制依赖与三种classpath(编译classpath、测试classpath、运行classpath)的关系。 依赖范围更为通俗的理解，其实就是给依赖包打标记，例如：将 A 依赖包标记为“compile”，Maven 就知道 A 依赖包在项目编译的时候要被引入到 classpath 中。</p>
<p style="">依赖范围必须在 pom.xml 文件中的 &lt;scope&gt; 标签中设定，如下所示：</p>
<pre><code>&lt;dependency&gt;
  &lt;groupId&gt;junit&lt;/groupId&gt;
  &lt;artifactId&gt;junit&lt;/artifactId&gt;
  &lt;version&gt;4.7&lt;/version&gt;
  &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;</code></pre>
<p style="">上述 &lt;scope&gt; 标签就是用来指定被依赖资源的依赖范围，可选配置有 compile、test、provided、runtime、system、import，若不指定则默认 compile。</p>
<p style="">在 pom.xml 中设定好后，你需要点击“Load Maven Changes” 才会生效。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1692931286195-c06a29e1-0a69-4a76-8ff5-bdb186e67f46.png&amp;size=m" width="992" style="display: inline-block"></p>
<p style="">比方说，你修改了标签 &lt;scope&gt; 的取值，你需要 Load Maven Changes，在模块的依赖管理中才会出现新的取值，如下所示：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1692931305632-ee64386a-540c-4337-aab6-392320321219.png&amp;size=m" width="1005" style="display: inline-block"></p>
<p style="">你如果只是在上述依赖管理的界面中改变 scope 的取值是没有效果的，只能通过 pom.xml 中的 scope 标签来设定。</p>
<p style="">说明：</p>
<ol>
 <li>
  <p style="">编译是指将整个项目（其实是模块）的 src/main/java 目录下的源代码文件以及 resources 目录下的资源文件编译输出到 classes 目录下。在这个编译过程中编译器会到 classpath 指定的目录下查找字节码文件（jar包、class文件等）</p></li>
 <li>
  <p style="">测试是指编译测试的代码和运行测试的代码，通常使用 Junit 工具进行代码的测试</p></li>
 <li>
  <p style="">运行是指项目部署到服务器，并且启动了服务器，客户端可以正常访问应用</p></li>
</ol>
<h4 style="" id="1%E3%80%81compile">1、compile</h4>
<p style="">编译依赖范围。如果没有指定，就会默认使用该依赖范围。使用此依赖范围的Maven 依赖，对于编译、测试、运行三种classpath 都有效。典型的例子是spring-core,在编译、测试和运行的时候都需要使用该依赖。</p>
<p style="">既然运行时也要使用 scope 设为 compile 的依赖，所以 scope 为 compile 的依赖在项目打部署包的时候（即构建 artifact）会被一起打包，会放在 WEB-INF/lib 目录下。</p>
<h4 style="" id="2%E3%80%81test">2、test</h4>
<p style="">测试依赖范围。使用此依赖范围的Maven依赖，只对于测试classpath有效，在编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是JUnit,它只有在编译测试代码及运行测试的时候才需要。</p>
<p style="">scope 为 test 的依赖只在测试时使用，用于编译和运行测试代码，不会参与项目的打包。</p>
<h4 style="" id="3%E3%80%81provided">3、provided</h4>
<p style="">已提供依赖范围。使用此依赖范围的Maven依赖，对于编译和测试classpath有效，但在运行时无效（对运行的classpath无效）。典型的例子是 servlet-api, 编译和测试项目的时候需要该依赖，但在运行项目的时候，由于容器已经提供，就不需要Maven重复地引人一遍。既然运行时容器会提供，所以 scope 为 provided 的依赖不会参与项目的打包。</p>
<h4 style="" id="4%E3%80%81runtime">4、runtime</h4>
<p style="">运行时依赖范围。使用此依赖范围的Maven依赖，对于测试和运行class-path有效，但在编译主代码时无效（对编译的classpath无效）。典型的例子是JDBC驱动实现，项目主代码的编译只需要JDK提供的JDBC接口，只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。</p>
<p style="">既然运行时也要使用 scope 设为 runtime 的依赖，所以 scope 为 runtime 的依赖在项目打部署包的时候（即构建 artifact）会被一起打包，会放在 WEB-INF/lib 目录下。</p>
<h4 style="" id="5%E3%80%81system">5、system</h4>
<p style="">系统依赖范围。该依赖与三种 classpath 的关系，和 provided 依赖范围完全一致。但是，system 范围的依赖不会从 maven 仓库下载，而是从本地文件系统获取，使用 system 范围的依赖时必须通过 &lt;systemPath&gt; 元素显式地指定依赖文件的路径。</p>
<p style="">由于此类依赖不是通过 Maven 仓库解析的，而且往往与本机系统绑定，可能造成构建（构建的产物有：classes和artifact）的不可移植，因此应该谨慎使用。</p>
<p style="">元素 &lt;systemPath&gt; 可以引用环境变量，如下：</p>
<pre><code>&lt;dependency&gt;
	&lt;groupId&gt;javax.sql&lt;/groupId&gt;
	&lt;artifactId&gt;jdbc-stext&lt;/artifactId&gt;
	&lt;version&gt;2.0&lt;/version&gt;
	&lt;scope&gt;system&lt;/scope&gt;
	&lt;systemPath&gt;${java.home}/lib/rt.jar&lt;/systemPath&gt; 
&lt;/dependency&gt;</code></pre>
<h4 style="" id="6%E3%80%81import">6、import</h4>
<p style="">导入依赖范围，该依赖范围不会对三种 classpath 产生影响，该依赖范围只能与 &lt;dependencyManagement&gt; 元素配合使用，其功能为将目标pom.xml 文件中元素 &lt;dependencyManagement&gt; 的配置导入合并到当前 pom.xml 文件的元素 &lt;dependencyManagement&gt; 中。有关元素 &lt;dependencyManagement&gt; 的功能请了解 Maven 继承特性。</p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E4%BE%9D%E8%B5%96%E4%BC%A0%E9%80%92">二、依赖传递</h3>
<p style="">依赖传递就是假设你有一个 Maven 项目叫 A，你在这个项目中添加了一个依赖，这个依赖是你的另一个 Maven 项目 B，你在这个被依赖的项目 B 中添加一些依赖，这些依赖也会自动地添加到 A 中，这就是依赖传递。简单来说就是项目的依赖的依赖也会成为该项目的依赖。</p>
<p style="">&lt;scope&gt; 标签的取值对依赖传递有什么影响呢？这个影响就是，只有当依赖的 scope 标签被定义为 compile 时才会发生依赖传递，而定义为 test 或者 provided 都不会发生依赖传递。</p>
<p style="">依赖范围与 classpath 的关系表，如下：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1692931612008-ef2fd645-e743-4b22-ac56-d6ecdfbc1ae8.png&amp;size=m" width="917" style="display: inline-block"></p>
<h3 style="" id="%E4%B8%89%E3%80%81%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">三、参考资料</h3>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://liaowenxiong.blog.csdn.net/">https://www.pianshen.com/article/90681857024/</a></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://liaowenxiong.blog.csdn.net/">https://www.liaoxuefeng.com/wiki/1252599548343744/1309301178105890</a></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://liaowenxiong.blog.csdn.net/">https://www.cnblogs.com/sanshisiniao/articles/12068847.html</a></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://liaowenxiong.blog.csdn.net/">https://www.cnblogs.com/tuyang1129/p/10741558.html</a></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://liaowenxiong.blog.csdn.net/">https://blog.csdn.net/seasonsbin/article/details/79093647</a></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://liaowenxiong.blog.csdn.net/">https://blog.csdn.net/lishuoboy/article/details/100554751</a></p>]]></description><guid isPermaLink="false">/archives/mavenyi-lai-fan-wei-scopexiang-jie</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmaven.png&amp;size=m" type="image/jpeg" length="0"/><category>maven</category><pubDate>Fri, 12 Jan 2024 13:01:00 GMT</pubDate></item><item><title><![CDATA[分布式Netty技术架构图]]></title><link>https://xiaoming728.com/archives/fen-bu-shi-nettyji-shu-jia-gou-tu</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%88%86%E5%B8%83%E5%BC%8FNetty%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84%E5%9B%BE&amp;url=/archives/fen-bu-shi-nettyji-shu-jia-gou-tu" width="1" height="1" alt="" style="opacity:0;">
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F1705063664095.png&amp;size=m" width="991px" height="743px" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-wngf.png&amp;size=m" style="display: inline-block"></p>]]></description><guid isPermaLink="false">/archives/fen-bu-shi-nettyji-shu-jia-gou-tu</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fnetty.png&amp;size=m" type="image/jpeg" length="0"/><category>netty</category><category>Java技术</category><pubDate>Fri, 12 Jan 2024 12:48:00 GMT</pubDate></item><item><title><![CDATA[Docker安装Umami]]></title><link>https://xiaoming728.com/archives/dockeran-zhuang-umami</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E5%AE%89%E8%A3%85Umami&amp;url=/archives/dockeran-zhuang-umami" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">umami: <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/umami-software/umami">https://github.com/umami-software/umami</a></p>
</blockquote>
<h1 style="" id="%E7%AC%AC%E4%B8%80%E6%AD%A5">第一步</h1>
<h3 style="text-align: start; " id="%E5%AE%89%E8%A3%85-yarn">安装 Yarn</h3>
<pre><code>npm install -g yarn</code></pre>
<h3 style="text-align: start; " id="%E4%B8%8B%E8%BD%BDgit%E4%BB%A3%E7%A0%81%E5%B9%B6%E8%BF%9B%E5%85%A5%E7%9B%AE%E5%BD%95">下载git代码并进入目录</h3>
<pre><code>git clone https://github.com/umami-software/umami.git</code></pre>
<pre><code>cd umami</code></pre>
<h3 style="text-align: start; " id="%E4%BF%AE%E6%94%B9next.config.js%E5%B0%86%E7%AC%AC13%E8%A1%8C%E5%88%A0%E9%99%A4">修改next.config.js将第13行删除</h3>
<p style="text-align: start; ">可以不删除但是会导致shareUrl无法被iframe嵌套</p>
<pre><code>`frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS || ''}`,</code></pre>
<h3 style="text-align: start; " id="%E4%BF%AE%E6%94%B9docker-compose.yml">修改docker-compose.yml</h3>
<pre><code>version: '3'
services:
  umami:
    image: umami:latest
    network_mode: "host"
    environment:
      DATABASE_TYPE: mysql
      DATABASE_URL: mysql://username:password@ip:3306/umami
      APP_SECRET: replace-me-with-a-random-string
    restart: always
    container_name: umami</code></pre>
<h3 style="text-align: start; " id="%E4%BD%BF%E7%94%A8dockerfile%E5%88%9B%E5%BB%BA%E5%AE%B9%E5%99%A8%E5%B9%B6%E8%BF%90%E8%A1%8C">使用DockerFile创建容器并运行</h3>
<pre><code>docker build --build-arg DATABASE_TYPE=mysql -t  umami .</code></pre>
<h3 style="text-align: start; " id="%E4%BD%BF%E7%94%A8docker-compose%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">使用docker-compose运行容器</h3>
<pre><code>docker-compose up -d</code></pre>
<h3 style="text-align: start; " id="%E8%AE%BF%E9%97%AE%E5%9C%B0%E5%9D%80">访问地址</h3>
<pre><code>https://localhost:3000/</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-spek.png&amp;size=m" style="display: inline-block"></p>
<h3 style="text-align: start; " id="%E9%BB%98%E8%AE%A4%E8%B4%A6%E5%8F%B7%E5%AF%86%E7%A0%81">默认账号密码</h3>
<p style="text-align: start; "><code>username: admin</code><br><code>password: umami</code></p>
<h3 style="text-align: start; " id="%E6%BC%94%E7%A4%BA%E9%A1%B5%E9%9D%A2">演示页面</h3>
<h3 style="" id="%E5%A6%82%E6%9E%9C%E4%BD%BF%E7%94%A8nginx%E4%BB%A3%E7%90%86%E9%9C%80%E8%A6%81%E8%AF%B7%E6%B1%82%E9%A1%B5%E9%9D%A2%E6%AF%94%E8%BE%83%E5%A4%9A%E7%9A%84%E6%83%85%E5%86%B5%E4%B8%8B%E4%BF%AE%E6%94%B9%E5%B9%B6%E5%8F%91%E6%95%B0%E9%87%8F"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ourl.png&amp;size=m" width="817px" height="281px" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-dyij.png&amp;size=m" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-vscx.png&amp;size=m" style="display: inline-block">如果使用nginx代理需要请求页面比较多的情况下修改并发数量</h3>
<pre><code>events {
    worker_connections 4096;
}</code></pre>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/dockeran-zhuang-umami</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fumami.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Thu, 11 Jan 2024 08:42:00 GMT</pubDate></item><item><title><![CDATA[Mcmc使用Nginx去掉默认html目录]]></title><link>https://xiaoming728.com/archives/mcmcshi-yong-nginxqu-diao-mo-ren-htmlmu-lu</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Mcmc%E4%BD%BF%E7%94%A8Nginx%E5%8E%BB%E6%8E%89%E9%BB%98%E8%AE%A4html%E7%9B%AE%E5%BD%95&amp;url=/archives/mcmcshi-yong-nginxqu-diao-mo-ren-htmlmu-lu" width="1" height="1" alt="" style="opacity:0;">
<pre><code>server {
        listen 80;
        server_name xiaoming728.com;
        # index.html fallback
        absolute_redirect off;
        gzip on;
        gzip_comp_level 5;
        gzip_min_length 256;
        gzip_proxied any;
        gzip_vary on;
        gzip_types application/javascript application/json application/xml text/css text/plain text/xml text/javascript;

        location ~ ^/(ms|login|cms|basic|mdiy|template|static|upload) {
                client_max_body_size 1000m;
                proxy_pass http://127.0.0.1:8081;
                proxy_redirect default;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection 'upgrade';
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_cache_bypass $http_upgrade;
                proxy_next_upstream error timeout invalid_header http_500;
        }
        location /zh {
                alias /opt/soft/docker/mcms/zh;
                autoindex on;
                try_files $uri $uri /zh/index.html;
        }

        location ^~ /en {
                rewrite ^/en(/.*)?$ https://xiaoming728.com$1 permanent;
        }

        location = /sitemap.xml {
            alias /opt/soft/docker/mcms/config/sitemap.xml;
        }

        location = /robots.txt {
            alias /opt/soft/docker/mcms/config/robots.txt;
        }

        location / {
                alias /opt/soft/docker/mcms/en/;
                autoindex on;
                try_files $uri $uri/ /en/index.html =404;
        }
}</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/mcmcshi-yong-nginxqu-diao-mo-ren-htmlmu-lu</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fnginx.png&amp;size=m" type="image/jpeg" length="0"/><category>Nginx技术</category><pubDate>Thu, 11 Jan 2024 01:58:00 GMT</pubDate></item><item><title><![CDATA[跨境第三方支付平台对比]]></title><link>https://xiaoming728.com/archives/1704424296339</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E8%B7%A8%E5%A2%83%E7%AC%AC%E4%B8%89%E6%96%B9%E6%94%AF%E4%BB%98%E5%B9%B3%E5%8F%B0%E5%AF%B9%E6%AF%94&amp;url=/archives/1704424296339" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="%E8%B7%A8%E5%A2%83%E6%94%B6%E5%8D%95%E5%B9%B3%E5%8F%B0"><strong><span fontsize="" color="">跨境收单平台</span></strong></h1>
<h2 style="" id="1.-%E8%BF%9E%E8%BF%9E%E5%85%A8%E7%90%83%E6%94%B6%E5%8D%95"><strong>1. <span fontsize="" color="">连连全球收单</span></strong></h2>
<p style=""><span fontsize="" color="">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://acquiring.lianlianpay.com/"><u><span fontsize="" color="rgb(0, 0, 255)" style="color: rgb(0, 0, 255)">https://acquiring.lianlianpay.com/</span></u></a></p>
<h3 style="" id="%E6%94%B6%E8%B4%B9"><strong><span fontsize="" color="">收费</span></strong></h3>
<p style=""><span fontsize="" color="">5000人民币开户，3000年费，3.9% + 0.3$手续费（根据品类而定）</span></p>
<h3 style="" id="%E6%94%B6%E5%8D%95%E6%B5%81%E7%A8%8B%EF%BC%9A"><strong><span fontsize="" color="">收单流程：</span></strong></h3>
<p style=""><span fontsize="" color="">1.iFrame形式接入连连全球收单：</span></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://acquiring.lianlianpay.com/"><u><span fontsize="" color="rgb(128, 0, 128)" style="color: rgb(128, 0, 128)">https://acquiring.lianlianpay.com/help/details/EMk4ujJ61.html</span></u></a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qutj.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span></p>
<p style="">2. <span fontsize="" color="">收银台形式接入连连全球收单：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://acquiring.lianlianpay.com/"><u><span fontsize="" color="rgb(0, 0, 255)" style="color: rgb(0, 0, 255)">https://acquiring.lianlianpay.com/help/details/tXH8cANhP.html</span></u></a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-jmbt.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span></p>
<p style=""><span fontsize="" color="">3.api形式接入连连全球收单（需要icp证书不推荐）</span></p>
<p style=""><span fontsize="" color="">&nbsp;</span></p>
<p style=""><span fontsize="" color="">&nbsp;</span></p>
<h2 style="" id="3.-pingpangx%E5%85%A8%E7%90%83%E6%94%B6%E5%8D%95"><strong>3. <span fontsize="" color="">pingpangx全球收单</span></strong></h2>
<h3 style="" id="%E4%BB%8B%E7%BB%8D"><strong><span fontsize="" color="">介绍</span></strong></h3>
<p style=""><span fontsize="" color="">PingPongCheckout 跨境支付的 API 接口文档,商户服务器和 PingPongCheckout 服务器进行交互。 供商户/平台服务方的技术开发及测试相关人员使用。 本文档分别从交互流程、通讯方式、签名方 案、交易接口、注意事项等⻆度详细介绍了 PingPongCheckout 跨境支付 API 接口的工作方式和开发过 程，可以帮助开发人员快速接入支付系统，同时也可以作为后续接口参数以及参数类型的速查手册。</span></p>
<h3 style="" id="%E8%A6%81%E6%B1%82"><strong><span fontsize="" color="">要求</span></strong></h3>
<p style=""><span fontsize="" color="">建站3个月以上，月流水50000$</span></p>
<h3 style="" id="%E6%94%B6%E8%B4%B9"><strong><span fontsize="" color="">收费</span></strong></h3>
<p style=""><span fontsize="" color="">3.4%手续费，0.3$基础服务费，开户300$, 技术服务费300$，年费150$,后续还有其他收费如退款等。4000人民币，1000年费</span></p>
<h4 style="" id="pingpong%E5%95%86%E6%88%B7%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97"><strong><span fontsize="" color="">Pingpong商户接入指南</span></strong></h4>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://acquiring.lianlianpay.com/"><u><span fontsize="" color="rgb(0, 0, 255)" style="color: rgb(0, 0, 255)">https://acquirer-api-docs-v3.pingpongx.com/pages/aa82c4/</span></u></a></p>
<h3 style="" id="%E6%BC%94%E7%A4%BA%E5%9C%B0%E5%9D%80"><strong><span fontsize="" color="">演示地址</span></strong></h3>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://acquiring.lianlianpay.com/"><u><span fontsize="" color="rgb(128, 0, 128)" style="color: rgb(128, 0, 128)">https://checkout.pingpongx.com/aq/pay/demoStore?accessToken=l949CNVDrFuCC7ZHYb1faxOJr5fY5H9H6XtGV5qwgyqUbjQfQYb37lBcmQzg4ID9</span></u></a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-vndj.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="">支付流程图</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-zfwr.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span></p>
<p style=""><span fontsize="" color="">步骤</span></p>
<p style=""><span fontsize="" color="">&nbsp;</span></p>
<p style="">1. <span fontsize="" color="">客户端提交订单给商户服务端处理</span></p>
<p style="">2. <span fontsize="" color="">商户服务端返回PingPongCheckout JS-SDK需要的参数(包含订单信息，签名和JS-SDK初始化需要的参数)</span></p>
<p style="">3. <span fontsize="" color="">客户端初始化PingPongCheckout SDK,并且调用createPayment，传入订单信息。</span></p>
<p style="">4. <span fontsize="" color="">SDK自动开始和PingPongCheckout服务端交互，成功之后将会渲染PingPongCheckout收银台</span></p>
<p style="">5. <span fontsize="" color="">买家填写卡号和cvv等支付信息</span></p>
<p style="">6. <span fontsize="" color="">提交支付信息</span></p>
<p style="">7. <span fontsize="" color="">如果是3D交易，还需要3D验证，否则直接展示交易结果</span></p>
<p style="">8. <span fontsize="" color="">异步通知详见如何获取交易状态</span></p>
<p style="">9. <span fontsize="" color="">收到异步通知需要响应OK给PingPongCheckout</span></p>
<p style=""><span fontsize="" color="">此次对接的是“统一下单-本地支付&amp;信用卡”方式支付，他与“获取跳转收银台”方式两者都是在请求完pingpong支付之后生成收银台的跳转链接，但是后者是由我们开发者控制界面跳转只返回支付的界面URL，而后者是由pingpong直接跳转过去</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-eubu.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span></p>
<p style=""><span fontsize="" color="">相关开发文档链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://acquiring.lianlianpay.com/"><u><span fontsize="" color="rgb(0, 0, 255)" style="color: rgb(0, 0, 255)">https://blog.csdn.net/qq_42910468/article/details/130586124</span></u></a></p>
<h1 style="" id="3.%E4%B8%87%E9%87%8C%E6%B1%87"><strong><span fontsize="" color="">3.万里汇</span></strong></h1>
<p style=""><span fontsize="" color="">仅支持，进口付款，不支持出口收款，如果需要需要先注册万里汇收款，再联系客户经理进行对接。</span></p>
<h1 style="" id="4.-paypal">4. <strong><span fontsize="" color="">PayPal</span></strong></h1>
<h2 style="" id="%E6%96%87%E6%A1%A3"><strong><span fontsize="" color="">文档</span></strong></h2>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://acquiring.lianlianpay.com/"><u><span fontsize="" color="rgb(0, 0, 255)" style="color: rgb(0, 0, 255)">https://developer.paypal.com/docs/checkout/?_ga=2.118289929.1432925841.1704349585-406986344.1704349584</span></u></a></p>
<h2 style="" id="%E8%B4%B9%E7%94%A8"><strong><span fontsize="" color="">费用</span></strong></h2>
<p style=""><span fontsize="" color="">具体费用</span></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://acquiring.lianlianpay.com/"><span fontsize="" color="">https://www.paypal.com/c2/webapps/mpp/merchant-fees#fixed-fees-commercialtrans</span></a></p>
<p style=""><span fontsize="" color="rgb(44, 46, 47)" style="color: rgb(44, 46, 47)">中国大陆（CN）境外&nbsp;4.40% +&nbsp;</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://xiaoming728.com#fixed-fees-commercialtrans"><strong><span fontsize="" color="rgb(0, 112, 186)" style="color: rgb(0, 112, 186)">固定费用</span></strong></a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ieja.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span></p>
<h2 style="" id="%E9%80%80%E5%8D%95%E8%B4%B9"><strong><span fontsize="" color="">退单费</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-kunk.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span></p>
<h2 style="" id="%E4%BA%89%E8%AE%AE%E8%A7%A3%E5%86%B3%E8%B4%B9"><strong><span fontsize="" color="">争议解决费</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xpbw.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span></p>
<h2 style="" id="%E9%AB%98%E9%A2%9D%E4%BA%89%E8%AE%AE%E8%A7%A3%E5%86%B3%E8%B4%B9"><strong><span fontsize="" color="">高额争议解决费</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-navp.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span></p>
<h2 style="" id="%E5%B8%81%E7%A7%8D%E5%85%91%E6%8D%A2"><strong><span fontsize="" color="">币种兑换</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-hpwr.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xhnh.png&amp;size=m" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-mxbc.png&amp;size=m" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-gqjw.png&amp;size=m" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-rgap.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="">&nbsp;&nbsp;</span></p>
<h2 style="" id="%E6%8F%90%E6%AC%BE%E8%B4%B9"><strong><span fontsize="" color="">提款费</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-cyyz.png&amp;size=m" style="display: inline-block"><span fontsize="" color="">&nbsp;</span></p>
<h2 style="" id="%E6%95%88%E6%9E%9C%E6%88%AA%E5%9B%BE"><strong><span fontsize="" color="">效果截图</span></strong><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-dipf.png&amp;size=m" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-zaox.png&amp;size=m" width="652px" height="754px" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ldno.png&amp;size=m" style="display: inline-block"></h2>
<h1 style="" id="%E5%85%B6%E4%BB%96"><strong><span fontsize="" color="">其他</span></strong></h1>
<h2 style="" id="%E7%9B%88%E5%BA%97%E9%80%9A"><strong><span fontsize="" color="">盈店通</span></strong></h2>
<p style=""><span fontsize="" color="">开店不收费，主打获客，支付无业务使用第三方paypal等平台。</span></p>
<p style=""><strong><span fontsize="" color="">Shopify</span></strong></p>
<p style=""><span fontsize="" color="">信用卡费率：</span></p>
<p style=""><span fontsize="" color="">Shopify、Shopify 和高级 Shopify 计划将分别收取 3.3%、3.2% 或 3.1% 的额外费用和固定0.3 美金。</span></p>
<p style=""><span fontsize="" color="">第三方支付费率：</span></p>
<p style=""><span fontsize="" color="">Shopify、Shopify 和高级 Shopify 计划将分别收取 2%、1% 或 0.5% 的额外费用。</span></p>
<h2 style="" id="wix"><strong><span fontsize="" color="">Wix</span></strong></h2>
<p style=""><span fontsize="" color="">具体收费链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://acquiring.lianlianpay.com/"><span fontsize="" color="">https://support.wix.com/en/article/wix-payments-service-fees</span></a></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">每个地区的处理费有所不同，如下：&nbsp;</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">欧盟国家：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的1.9% + 0.30 欧元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">瑞士：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.3% + 0.30瑞士法郎</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">英国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.1% + 0.20 英镑</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">美国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的2.9% + 0.30 美元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加拿大：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.9% + 0.30 加元</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">Wix 付款销售点 (POS)</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加工费差异如下：&nbsp;</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">美国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的2.6% + 0 美元</span></p>
<p style="margin-left: 72px!important;"><strong>o <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">触碰支付：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的 2.6% + 0.20 美元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加拿大：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的2.7%+0加元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加拿大 Interac</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">：每笔交易 0.15 加元<br></span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.wix.com/pos"><u><span fontsize="" color="rgb(17, 109, 255)" style="color: rgb(17, 109, 255)">Wix POS</span></u></a><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">目前仅适用于部分加拿大和美国的 Wix 商户。要处理付款，Wix POS 需要经过完全验证的 Wix Payments 帐户。&nbsp;</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">Wix 付款手动卡输入</span></strong></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://support.wix.com/en/article/accepting-payments-by-manual-card-entry"><u><span fontsize="" color="rgb(17, 109, 255)" style="color: rgb(17, 109, 255)">手动卡输入</span></u></a><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">付款（无论是在桌面上输入还是通过 Wix POS 输入）的处理费用如下：</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">欧盟国家：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的 2.3% + 0.30 欧元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">瑞士：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.8% + 0.30瑞士法郎</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">英国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.5% + 0.20 英镑</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">美国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的3.5% + 0.30 美元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加拿大：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的3.5%+0.30加元</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">苹果支付</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">每个地区的处理费有所不同，如下：&nbsp;</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">欧盟国家：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的1.9% + 0.30 欧元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">瑞士：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.3% + 0.30瑞士法郎</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">英国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.1% + 0.20 英镑</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">美国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的2.9% + 0.30 美元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加拿大：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.9% + 0.30 加元</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">谷歌支付</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">每个地区的处理费有所不同，如下：&nbsp;</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">欧盟国家：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的1.9% + 0.30 欧元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">瑞士：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.3% + 0.30瑞士法郎</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">英国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.1% + 0.20 英镑</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">美国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的2.9% + 0.30 美元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加拿大：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;交易金额的2.9% + 0.30 加元</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">后付费</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加工费如下：</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">美国</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">：交易金额的 6% + 0.30 美元</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">确认</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加工费如下：</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">美国</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">：交易金额的 6% + 0.30 美元</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">在移动设备上点击支付</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">手续费如下：<br></span><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">美国</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">：交易金额的2.6% + 0.20 USD</span></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)"><br></span><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">注意</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">：移动端点击支付目前仅在美国可用。</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">立即付款（网上银行转账）</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">每个地区的处理费有所不同，如下：&nbsp;</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">奥地利、比利时、德国：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的2.5% + 0.30 欧元</span></p>
<p style="margin-left: 36px!important;"><strong>· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">瑞士：</span></strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的2.5% + 0.40瑞士法郎</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">iDEAL</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加工费如下：</span></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的 2.5% + 0.30 欧元</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">Giropay</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加工费如下：</span></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">交易金额的 2.5% + 0.30 欧元</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">退款费用</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">如果您的 Wix Payments 账户收到退款，您可能需要支付</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://support.wix.com/en/article/wix-payments-about-chargeback-fees"><u><span fontsize="" color="rgb(17, 109, 255)" style="color: rgb(17, 109, 255)">退款费用</span></u></a><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">。可能有例外情况。</span></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">&nbsp;</span></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">区域退款费用按相应货币细分如下：</span></p>
<p style="margin-left: 36px!important;">· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">加拿大：15.00 加元</span></p>
<p style="margin-left: 36px!important;">· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">欧盟：15.00 欧元&nbsp;</span></p>
<p style="margin-left: 36px!important;">· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">瑞士：15.00 瑞士法郎</span></p>
<p style="margin-left: 36px!important;">· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">英国：15.00 英镑</span></p>
<p style="margin-left: 36px!important;">· <span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">美国：15.00 美元</span></p>
<p style=""><strong><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">退还付款费用</span></strong></p>
<p style=""><span fontsize="" color="rgb(0, 6, 36)" style="color: rgb(0, 6, 36)">对于退款，需支付标准 Wix Payments 处理费。例如，在美国，如果您向客户发放 100 美元退款，则该客户购买的原始处理费 2.90 美元 (2.9%) + 0.30 美元将不会退还到您的 Wix Payments 账户。不会收取额外的退款费用。</span></p>
<p style=""><span fontsize="" color="">&nbsp;</span></p>]]></description><guid isPermaLink="false">/archives/1704424296339</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E6%2594%25AF%25E4%25BB%2598.jpeg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Fri, 5 Jan 2024 03:15:00 GMT</pubDate></item><item><title><![CDATA[EDI许可证国内外分析]]></title><link>https://xiaoming728.com/archives/1704346563376</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=EDI%E8%AE%B8%E5%8F%AF%E8%AF%81%E5%9B%BD%E5%86%85%E5%A4%96%E5%88%86%E6%9E%90&amp;url=/archives/1704346563376" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="%E5%9B%BD%E5%86%85edi%E8%AE%B8%E5%8F%AF%E8%AF%81">国内EDI许可证</h1>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;width:81.00pt;">
    <p style="">许可证类型</p></td>
   <td colspan="1" rowspan="1" colwidth="633" style="width:69.95pt;">
    <p style="">适用范围</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:161.00pt;">
    <p style="">申请条件</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:173.00pt;">
    <p style="">办理流程</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:139.00pt;">
    <p style="">有效期</p></td>
   <td colspan="1" rowspan="1" colwidth="631" style="width:377.50pt;">
    <p style="">资格</p></td>
   <td colspan="1" rowspan="1" colwidth="207" style="width:377.50pt;">
    <p style="">相关链接</p></td>
   <td colspan="1" rowspan="1" colwidth="262" style="width:377.50pt;">
    <p style="">许可内容分</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="3" colwidth="100" style="height:51.00pt;width:81.00pt;">
    <p style="">EDI许可证</p></td>
   <td colspan="1" rowspan="3" colwidth="633" style="width:69.95pt;">
    <p style="">国内</p></td>
   <td colspan="1" rowspan="3" colwidth="100" style="width:161.00pt;">
    <p style="">公司合法成立、有专业人员和资金、场地设施符合要求等</p></td>
   <td colspan="1" rowspan="3" colwidth="100" style="width:173.00pt;">
    <p style="">向当地通信管理部门提交申请材料，审核通过后颁发许可证</p></td>
   <td colspan="1" rowspan="3" colwidth="100" style="width:139.00pt;">
    <p style="">根据不同地区要求而定</p></td>
   <td colspan="1" rowspan="3" colwidth="631" style="width:377.50pt;">
    <p style="">1.在省、自治区、直辖市范围内经营的，注册资本低限额为100万元人民币;在全国或者跨省、自治区、直辖市范围经营的，注册资本低限额为1000万元人民币。<br>
     2.需要申请企业三个人一个月社保记录<br>
     3.不分区域要求特定的企业营业执照经营范围</p></td>
   <td colspan="1" rowspan="1" colwidth="207" style="width:377.50pt;">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://lvlin.baidu.com/question/1935033131561914987.html">edi许可证如何办理，办理流程是什么?</a></p></td>
   <td colspan="1" rowspan="3" colwidth="262" style="width:377.50pt;">
    <p style="">根据业务范围和服务种类等进行评分，例如在线支付、电子商务平台、虚拟商品交易等，根据评分结果确定许可证级别和经营范围</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="207" style="width:377.50pt;">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://baijiahao.baidu.com/s?id=1758057356886927473&amp;wfr=spider&amp;for=pc">edi许可证办理条件</a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="207" style="width:377.50pt;">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.zhihu.com/question/604677458?utm_id=0">浙江EDI许可证怎么办理?申请需要什么材料?</a></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:81.00pt;">
    <p style="">其他许可证</p></td>
   <td colspan="1" rowspan="1" colwidth="633" style="width:69.95pt;">
    <p style="">其他特定行业或领域</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:161.00pt;">
    <p style="">根据具体行业或领域的要求而定</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:173.00pt;">
    <p style="">根据不同行业或领域的要求而定</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:139.00pt;">
    <p style="">根据具体行业或领域的要求而定</p></td>
   <td colspan="1" rowspan="1" colwidth="631" style="width:377.50pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="207" style="width:377.50pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="262" style="width:377.50pt;">
    <p style="">根据业务范围和服务种类等进行评分，例如在线教育、医疗保健、金融科技等，根据评分结果确定许可证级别和经营范围</p></td>
  </tr>
  <tr>
   <td colspan="8" rowspan="1" colwidth="100,633,100,100,100,631,207,262" style="height:31.00pt;width:1756.45pt;">
    <p style="">请注意，以上表格仅为示例，具体申请条件、办理流程、有效期和许可内容分可能因不同地区、行业或领域而有所差异。在实际操作中，建议根据当地法律法规和相关机构的要求进行申请。</p></td>
  </tr>
 </tbody>
</table>
<h1 style="" id="%E5%88%9D%E5%88%9B%E7%94%B5%E5%95%86%E9%83%BD%E9%9C%80%E8%A6%81%E7%94%B3%E8%AF%B7%E5%93%AA%E4%BA%9B%E8%B5%84%E8%B4%A8%E8%AF%81%E4%B9%A6">初创电商都需要申请哪些资质证书</h1>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://wap.peopleapp.com/article/rmh18348689/rmh18348689">https://wap.peopleapp.com/article/rmh18348689/rmh18348689</a></p>
<h1 style="" id="%E5%85%AC%E5%8F%B8%E5%9C%A8%E4%BB%A5%E4%B8%8B%E6%83%85%E5%86%B5%E4%B8%8B%E9%9C%80%E8%A6%81%E5%8A%9E%E7%90%86edi%E8%AE%B8%E5%8F%AF%E8%AF%81">公司在以下情况下需要办理EDI许可证</h1>
<p style="">1. 办理各种经营类电子商务、银行业务、股票买卖、票务买卖、拍卖商品买卖、费用支付等业务，有线上支付交易的需要办理。</p>
<p style="">2. 电商平台涉及到第三方商家入驻就需要持有edi许可证，不过需要注意，电商平台如果是自营模式，也就是自己卖自己的产品，只需要持有一个icp许可证就可以，但如果会有其他商家入驻平台进行销售或者后续会有，那就必须持有edi许可证。</p>
<p style="">3. 公司有通过通信网络传送，对连接到通信网络的电子设备进行控制和数据处理的业务，也需要办理edi许可证，比如说M2M和数据处理和管理平台，如消费电子设备、可穿戴设备、阿里智能、京东智能、华为智能等平台</p>
<h1 style="" id="%E5%93%AA%E4%BA%9B%E4%BC%81%E4%B8%9A%E9%9C%80%E8%A6%81%E5%8A%9E%E7%90%86edi%E8%AE%B8%E5%8F%AF%E8%AF%81%E5%91%A2%EF%BC%9Fedi%E8%AE%B8%E5%8F%AF%E8%AF%81%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F">哪些企业需要办理EDI许可证呢？EDI许可证有什么用？</h1>
<p style="">一般我们说的EDI许可证主要用于网上发生交易的时候，那么就需要办理这个许可证的。在线数据处理与交易处理业务，最典型的就是电商平台。这个就是需要办理EDI经营许可证了。</p>
<p style="">在线数据处理与交易处理业务包括交易处理业务、电子数据交换业务和网络电子设备数据处理业务三类子业务。业务举例：</p>
<p style="">交易处理业务：在互联网上从事交易业务的，典型的经营类电子商务平台。其中有几点要注意：</p>
<p style="">1、原在实体店销售的商品转为线上销售属于销售渠道的延伸，不属于该业务范畴。</p>
<p style="">2、平台提供商只是为买卖双方提供交易平台，不直接收取买方购买商品或服务的费用，只收取平台占用费或佣金。</p>
<p style="">3、神州专车的运营主体与所有司机都签署了劳动合同，仅需要网站备案即可。</p>
<p style="">4、《关于促进互联网金融健康发展的指导意见》（银发[2015]221号）中将互联网金融分为互联网支付、网络借贷、股权众筹融资、互联网基金销售、互联网保险、互联网信托、互联网消费金融等，其中网络借贷股权众筹融资应申请在线数据处理与交易处理业务，其余进行网站备案即可。</p>
<p style="">【案例枚举】抖音小店、美团外卖、天猫、京东商城、淘宝、携程、团购、滴滴出行、众筹、P2P网络借贷、家政服务APP等都属于该业务范畴。</p>
<p style="">电子数据交换业务：自建第三方电子数据交换平台，贸易或其他行政事务双方在这个平台完成电子数据交换，“贸易或其他行政事务”的适用对象和行业范围不作明确限制；数据交换和处理应符合国际公认的标准格式。</p>
<p style="">【案例枚举】海关报税EDI。</p>
<p style=""></p>
<p style="">网络/电子设备数据处理业务：自建第三方网络/电子设备数据处理平台，为其他企业或个人提供服务；用户可以通过终端软件自动激活和配置个人数据，对连接设备进行实时诊断、管理和使用控制。【案例枚举】如M2M和消费电子设备、可穿戴设备等数据处理和管理平台。如阿里智能、京东智能、华为智能（针对不限定品牌的设备开放的APP）。不过设备制造商针对自有设备提供的在线服务不属于该业务范畴。</p>
<p style="">据了解，一般网站上涉及到在线支付，并有现金交易是需要办理EDI许可证，但也存在不需要办理的情况，如部分地区通信管理部门如浙江省通信管理局对于经营许可证常见问题解答就有提及，如果企业在网上销售自己的商品，是销售渠道的一种拓展，不属于经营电信业务，无需申请电信业务经营许可证，只需做好网站备案即可。换句话说，通过互联网自产自销的平台不需要办理EDI许可证，如果有第三方商家入驻的电商平台就需要办理EDI经营许可证，如天猫、拼多多等。</p>
<p style=""></p>
<h1 style="" id="%E5%9B%BD%E5%A4%96">国外</h1>
<p style="">国外只需要符合营业执照范围，和特定品类许可证即可。</p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1704346563376</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fedi.png&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Thu, 4 Jan 2024 05:40:00 GMT</pubDate></item><item><title><![CDATA[国际支付方式统计]]></title><link>https://xiaoming728.com/archives/1704270374889</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%9B%BD%E9%99%85%E6%94%AF%E4%BB%98%E6%96%B9%E5%BC%8F%E7%BB%9F%E8%AE%A1&amp;url=/archives/1704270374889" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="%E5%9B%BD%E9%99%85%E6%94%AF%E4%BB%98%E6%96%B9%E5%BC%8F%E7%BB%9F%E8%AE%A1">国际支付方式统计</h1>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:18.00pt;width:223.00pt;">
    <p style="">支持支付方式</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">支持的币种</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:51.00pt;width:223.00pt;">
    <p style="">Credit/Debit Card</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">1.国际信用卡VISA\MasterCard支持20个币种的交易：USD/EUR/CAD/AUD/GBP/SGD/JPY/KRW/MXN/INR/PHP/MYR/AED/BRL/HKD/ILS/PLN/SAR/THB/ZAR/NZD</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:223.00pt;">
    <p style="">(信用卡/借记卡)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">2.国际银联信用卡支持17个币种的交易：USD, AED, AUD, BRL, CAD, EUR, GBP, HKD, INR, JPY, KRW, MYR, PHP, PLN, SAR, SGD, ZAR</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">3、法国本地CB卡仅支持欧元EUR支付</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">4、韩国本地借记卡/信用卡仅支持韩元KRW本币支付</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="3" colwidth="100" style="height:68.00pt;width:223.00pt;">
    <p style="">Apple Pay</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">Apple Pay当前支持的卡品牌：Visa、Mastercard 、Interac，</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">支持币种: USD、AUD、CAD、EUR、GBP、JPY、SGD、HKD、PLN、AED、BRL、SAR</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">加拿大本地借记卡Interac卡仅支持加币CAD支付</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:101.00pt;width:223.00pt;">
    <p style="">Google Pay</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">Google Pay当前支持的卡品牌：Visa、Mastercard、Amex，仅支持12,000USD以下的订单，暂不支持印度客户；美元(USD)、澳大利亚元(AUD)、加拿大元(CAD)、欧元(EUR)、英镑(GBP)、日元(JPY)、新加坡元(SGD)、港币(HKD)、南非兰特(ZAR)、墨西哥比索(MXN)、菲律宾比索(PHP)、马来西亚元(MYR)、泰铢(THB)、波兰兹罗提(PLN)、阿联酋迪拉姆(AED)、巴西雷亚尔(BRL)、以色列谢克尔(ILS)、沙特里亚尔(SAR)</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:68.00pt;width:223.00pt;">
    <p style="">Paypal</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">PayPal当前支持的账户内绑定信用卡支付和账余额支付，最低1USD 最高12,000USD，当前支持的币种美元(USD)、欧元(EUR)、英镑(GBP)、加拿大元(CAD)、新加坡元(SGD)、澳大利亚元(AUD)、港币(HKD)、日元(JPY)；样品单不提供paypal支付方式.</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:223.00pt;">
    <p style="">Afterpay/Clearpay</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">支持本地币种: 美元(USD)、澳大利亚元(AUD)、加拿大元(CAD)、英镑(GBP)、新西兰元(NZD)</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:68.00pt;width:223.00pt;">
    <p style="">英国/欧洲电子TT</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">欧洲Trustly：国际站平台合作的第三方本地支付渠道，仅支持英国和欧洲地区国家的买家线上网银转账支付，买家需要有本地银行的个人账户和开通线上网银，英国仅支持英镑支付。欧洲支持本币：DKK, EUR, NOK, PLN, SEK</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:223.00pt;">
    <p style="">(Trustly、iDeal、Sofort、Giropay、PayU、P24、Bancontact、EPS)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">欧洲地区支持国家：丹麦，爱沙尼亚，芬兰，德国，立陶宛，拉脱维亚，荷兰，挪威，西班牙，瑞典</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;width:223.00pt;">
    <p style="">(英国和欧洲地区)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">欧洲Ideal：仅支持EUR 支持国家：荷兰</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">欧洲Sofort:：支持EUR and CHF 支持国家：奥地利，比利时，德国，意大利，荷兰，瑞士</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">欧洲PayU:仅支持PLN 支持国家：波兰</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">EPS:仅支持EUR 支持国家：奥地利</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">欧洲P24:：仅支持PLN 支持国家：波兰</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">Bancontact: 仅支持EUR 支持国家：比利时</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">欧洲Giropay:仅支持EUR 支持国家：德国</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:223.00pt;">
    <p style="">Pay-easy(日本)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">Pay-easy是日本电子TT的第三方渠道，几乎覆盖日本所有银行，仅支持日元（JPY）支付</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:223.00pt;">
    <p style="">MOLpay(马来西亚)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">Molpay是马来西亚电子TT的第三方渠道，仅支持个人网银转账，仅支持马币（MYR）支付</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:223.00pt;">
    <p style="">MOLpay(泰国)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">Molpay是泰国电子TT的第三方渠道，仅支持个人网银转账，仅支持泰铢（THB）支付</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:68.00pt;width:223.00pt;">
    <p style="">菲律宾本地支付</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">DragonPay是菲律宾电子TT的第三方渠道，线上支持在线网银转账（online bank transfer）；线下支持通过阿里巴巴为商家生成的支付号码（payment reference)，通过银行柜台支付（over the counter bank）和便利店支付（convenience store）；仅支持菲律宾比索（PHP）支付</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;width:223.00pt;">
    <p style="">SPEI</p></td>
   <td colspan="1" rowspan="2" colwidth="100" style="width:366.00pt;">
    <p style="">仅支持墨西哥比索(MXN)支付，需要判断收银台国别是否切换为墨西哥</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;width:223.00pt;">
    <p style="">(墨西哥本地支付）</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;width:223.00pt;">
    <p style="">PIX</p></td>
   <td colspan="1" rowspan="2" colwidth="100" style="width:366.00pt;">
    <p style="">仅支持巴西BRL支付，需要判断收银台国别以及物流目的地国别是否切换为巴西</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;width:223.00pt;">
    <p style="">（巴西本地支付）</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:51.00pt;width:223.00pt;">
    <p style="">印度尼西亚本地支付</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">DOKU是印尼电子TT的第三方渠道，线上支持在线网银转账（online bank transfer）；线下支持通过阿里巴巴生成的付款号码（payment reference)，通过银行柜台或ATM支付；仅支持印尼卢比（IDR）支付</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:223.00pt;">
    <p style="">Wire Transfer</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">新加坡花旗银行跨境T/T账户；新加坡摩根大通银行跨境T/T账户；支持绝大部分国家买家跨境TT支付（制裁国家除外）：</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:223.00pt;">
    <p style="">(跨境TT)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">1.美元订单：9个币种USD/SGD/JPY/EUR/GBP/CAD/AUD/HKD/CNY，入账后卖家可自行汇兑成美金挂账。其他币种不可支付，银行直接退汇</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:51.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">2.人民币订单：仅支持该种TT支付。支持8个币CNY/SGD/JPY/EUR/GBP/CAD/AUD/HKD支付，入账后卖家自行汇兑成境外人民币挂账；不支持美金及以上8个币种以外的币种支付。</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">香港花旗跨境T/T账户；香港摩根大通跨境T/T账户；韩国、印度、巴基斯坦、沙特、中国台湾等地买家跨境TT支付：</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:68.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">1.美元订单：9个币种 人民币(CNY)、美元(USD)、新加坡元(SGD)、日元(JPY)、欧元(EUR)、英镑(GBP)、加拿大元(CAD)、澳大利亚元(AUD)、港币(HKD)，入账后卖家可自行汇兑成美金挂账。其他币种可支付，但银行入账后自动兑为美金</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:51.00pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">2.人民币订单：仅支持该种TT支付，支持8个币种 人民币(CNY)、新加坡元(SGD)、日元(JPY)、欧元(EUR)、英镑(GBP)、加拿大元(CAD)、澳大利亚元(AUD)、港币(HKD)，入账后请卖家自行汇兑成境外人民币挂账</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;width:223.00pt;">
    <p style="">PayPal PayLater</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">仅支持美、英、德、法、西、意地区的本币支付</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.00pt;width:223.00pt;">
    <p style="">Wire Transfer</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">1.买家国际站账号注册地址或订单收货地址为本地TT账号所在国；</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:34.00pt;width:223.00pt;">
    <p style="">(本地TT)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:366.00pt;">
    <p style="">2.本地TT汇款只限本地国家本地币种支付（例如欧洲本地TT仅支持欧元支付）；</p></td>
  </tr>
 </tbody>
</table>
<h1 style="" id="%E7%94%B5%E5%95%86%E5%B9%B3%E5%8F%B0%E6%94%AF%E4%BB%98%E6%96%B9%E5%BC%8F%E7%BB%9F%E8%AE%A1">电商平台支付方式统计</h1>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;width:90.80pt;">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:36.00pt;">
    <p style="">apple</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:45.60pt;">
    <p style="">chatgpt</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:48.40pt;">
    <p style="">youtube</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:47.20pt;">
    <p style="">amazon</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:37.60pt;">
    <p style="">steam</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:39.20pt;">
    <p style="">twitch</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:57.20pt;">
    <p style="">aliexpress</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:41.20pt;">
    <p style="">lazada</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:34.40pt;">
    <p style="">shein</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:49.20pt;">
    <p style="">takealot</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:31.60pt;">
    <p style="">tesla</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:77.20pt;">
    <p style="">globalsources</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:31.60pt;">
    <p style="">ebay</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:30.80pt;">
    <p style="">wish</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="width:44.40pt;">
    <p style="">shopify</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">支付宝</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">微信</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">银行卡/信用卡</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">paypal</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">Alipay</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">Octopus Wallet</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">Google Pay</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">worldfirst</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">Venmo</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="height:17.60pt;">
    <p style="">Boleto</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style="">✔️</p></td>
   <td colspan="1" rowspan="1" colwidth="100">
    <p style=""></p></td>
  </tr>
 </tbody>
</table>
<h1 style="" id="%E6%8A%80%E6%9C%AF%E6%8E%A8%E8%8D%90">技术推荐</h1>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/xiaoming728/IJPay/tree/dev">https://github.com/xiaoming728/IJPay/tree/dev</a></p>]]></description><guid isPermaLink="false">/archives/1704270374889</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2F%25E6%2594%25AF%25E4%25BB%2598.jpeg&amp;size=m" type="image/jpeg" length="0"/><category>技术杂文</category><pubDate>Wed, 3 Jan 2024 08:27:00 GMT</pubDate></item><item><title><![CDATA[2023年JRebel最新激活方式]]></title><link>https://xiaoming728.com/archives/1703816059911</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=2023%E5%B9%B4JRebel%E6%9C%80%E6%96%B0%E6%BF%80%E6%B4%BB%E6%96%B9%E5%BC%8F&amp;url=/archives/1703816059911" width="1" height="1" alt="" style="opacity:0;">
<h4 style="" id="%E4%BA%B2%E6%B5%8B%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%951%E6%BF%80%E6%B4%BB%E6%88%90%E5%8A%9F%E3%80%82"><strong>亲测使用方法1激活成功。</strong></h4>
<p style=""></p>
<h4 style="" id="jrebel%E4%BB%8B%E7%BB%8D%23"><strong>JRebel介绍</strong><a target="_blank" rel="nofollow" href="https://www.cnblogs.com/sansui6/p/17043448.html#rebel%E4%BB%8B%E7%BB%8D"><strong>#</strong></a></h4>
<blockquote>
 <p style="">JRebel是一款JVM插件，它使得Java代码修改后不用重启系统，立即生效。IDEA上原生是不支持热部署的，一般更新了 Java 文件后要手动重启 Tomcat 服务器，修改才能生效；所以推荐使用 JRebel 插件进行热部署。</p>
</blockquote>
<h4 style="" id="jrebel%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F%23"><strong>JRebel安装方式</strong><a target="_blank" rel="nofollow" href="https://www.cnblogs.com/sansui6/p/17043448.html#jrebel%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F"><strong>#</strong></a></h4>
<h5 style="" id="1.%E6%8F%92%E4%BB%B6%E4%BB%93%E5%BA%93%E5%AE%89%E8%A3%85%EF%BC%88%E6%8E%A8%E8%8D%90%EF%BC%89%23"><strong>1.插件仓库安装（推荐）</strong><a target="_blank" rel="nofollow" href="https://www.cnblogs.com/sansui6/p/17043448.html#1%E6%8F%92%E4%BB%B6%E4%BB%93%E5%BA%93%E5%AE%89%E8%A3%85%E6%8E%A8%E8%8D%90"><strong>#</strong></a></h5>
<blockquote>
 <p style="">1、打开IDEA，选择File—&gt;Settings—&gt;Plugins—&gt;在右侧选择Marketplace，<br>
  2、在搜索框输入jrebel—&gt;选择搜索结果—&gt;点击Install（安装），如下图。</p>
</blockquote>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F212a9ef9fb0098449147e141d8f100cb-ybpy.png&amp;size=m" style="display: inline-block"></p>
<h5 style="" id="2.%E4%B8%8B%E8%BD%BDzip%E6%96%87%E4%BB%B6%E5%8C%85%E8%BF%9B%E8%A1%8C%E5%AE%89%E8%A3%85%23"><strong>2.下载zip文件包进行安装</strong><a target="_blank" rel="nofollow" href="https://www.cnblogs.com/sansui6/p/17043448.html#2%E4%B8%8B%E8%BD%BDzip%E6%96%87%E4%BB%B6%E5%8C%85%E8%BF%9B%E8%A1%8C%E5%AE%89%E8%A3%85"><strong>#</strong></a></h5>
<p style=""><strong>下载地址：</strong><a target="_blank" rel="nofollow" href="https://plugins.jetbrains.com/plugin/4441-jrebel-and-xrebel/versions"><strong>官网下载</strong></a></p>
<p style=""><strong>按照以下操作步骤进行安装：</strong></p>
<blockquote>
 <p style="">1、下载插件。<br>
  2、下载后，打开IDEA，选择File—&gt;Settings—&gt;Plugins—&gt;设置按钮—&gt;Installed Plugin from Disk（从文件夹选择已下载的插件安装）。</p>
</blockquote>
<p style=""><strong>安装完成后根据提示重新启动IDEA.</strong></p>
<h4 style="" id="jrebel%E6%BF%80%E6%B4%BB%23"><strong>JRebel激活</strong><a target="_blank" rel="nofollow" href="https://www.cnblogs.com/sansui6/p/17043448.html#jrebel%E6%BF%80%E6%B4%BB"><strong>#</strong></a></h4>
<p style=""><strong>安装之后需要重启IDEA，JRebel插件会提示需要激活，点击Jrebel Activation进行激活。</strong></p>
<p style=""></p>
<p style="text-align: center; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F7ee5cdfd5b06bde8f0c743884a1f9551.png&amp;size=m" style="display: inline-block"></p>
<h5 style="" id="%E6%BF%80%E6%B4%BB%E6%96%B9%E5%BC%8F%E6%9C%89%E4%B8%A4%E7%A7%8D%EF%BC%9A%23"><strong>激活方式有两种：</strong><a target="_blank" rel="nofollow" href="https://www.cnblogs.com/sansui6/p/17043448.html#%E6%BF%80%E6%B4%BB%E6%96%B9%E5%BC%8F%E6%9C%89%E4%B8%A4%E7%A7%8D"><strong>#</strong></a></h5>
<blockquote>
 <p style="">1、注册地址填写激活网址 + 生成的GUID（不支持最新4.2版本） 激活版本 &lt; jrebel版本 2022.4.2<br>
  2、本地地址 + 生成的GUID 支持 jrebel版本 2022.4.2</p>
</blockquote>
<h6 style="" id="%E6%96%B9%E6%B3%951%EF%BC%9A"><strong>方法1：</strong></h6>
<blockquote>
 <p style=""><strong>1. 安装JRebel插件后，注册地址填写激活网址 + 生成的GUID，邮箱随便填写，然后直接激活即可</strong></p>
 <p style=""><strong>激活网址列表</strong></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/sansui6/p/17043448.html#rebel%E4%BB%8B%E7%BB%8D">https://jrebel.com.cn{GUID}</a></p>
 <p style=""><strong>GUID可以使用</strong><a target="_blank" rel="nofollow" href="https://www.guidgen.com/"><strong>在线GUID地址</strong></a><strong>在线生成，然后替换{GUID}就行。</strong></p>
 <p style=""><strong>2.下面邮箱地址可随便输入。</strong><br><strong>3.选择我同意</strong><br><strong>4.提交</strong></p>
</blockquote>
<p style=""></p>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-litj.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style=""><strong>出现下方提示则激活成功，如果没有出现下面提示，请确定自己的网络通畅，然后再次点击激活，如果还不行：</strong></p>
<blockquote>
 <p style="">1、检查插件版本<br>
  2、更换uuid重新尝试<br>
  3、使用激活方法2 重新尝试。</p>
</blockquote>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-tpar.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h6 style="" id="%E6%96%B9%E6%B3%952%EF%BC%9A"><strong><span fontsize="" color="rgb(254, 44, 36)" style="color: rgb(254, 44, 36)">方法2：</span></strong></h6>
<p style=""><strong>方法2与方法1类似，只不过最新版的2022.4.2 这个版本的jrebel应该是更改了激活方式 qekang方式激活不成功。</strong></p>
<p style=""><strong>1.前置步骤均一致，在填入Team URL时，填入以下内容</strong></p>
<blockquote>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="http://127.0.0.1:8888/%7BGUID%7D">http://127.0.0.1:8888/{GUID}</a></p>
 <p style="">GUID可以使用<a target="_blank" rel="nofollow" href="https://www.guidgen.com/">在线GUID地址</a>在线生成，然后替换{GUID}就行。</p>
</blockquote>
<p style=""><strong>2.下面邮箱地址可随便输入。</strong></p>
<p style=""><strong>3.选择我同意</strong></p>
<p style=""><strong>4.提交</strong></p>
<blockquote>
 <p style=""><strong>如果报Unable to connect to license server.Check your network connnection and/or VPN settings.</strong></p>
 <p style=""><strong>需要在</strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/ilanyu/ReverseProxy/releases/tag/v1.4"><strong>此处下载</strong></a><strong>自己机器系统相对应的工具，如图：</strong></p>
 <p style=""></p>
 <p style="text-align: center; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.com%2Fupload%2F4167b0c72e0dded4bd43837cd9a1374b.png&amp;size=m" style="display: inline-block"></p>
 <p style=""><strong>下载好了后，进行安装打开（激活插件时，程序保持启动，激活成功后可关闭），如图：</strong></p>
 <p style=""></p>
 <p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-fmra.png&amp;size=m" style="display: inline-block"></p>
 <p style=""></p>
 <p style=""><strong>再次执行上面的操作步骤即可。</strong></p>
</blockquote>
<p style=""></p>
<blockquote>
 <p style="">参考</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/truelove12358/article/details/131904268">https://blog.csdn.net/truelove12358/article/details/131904268</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1703816059911</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fidea.png&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Fri, 29 Dec 2023 02:21:00 GMT</pubDate></item><item><title><![CDATA[vuex安装]]></title><link>https://xiaoming728.com/archives/1703554497857</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=vuex%E5%AE%89%E8%A3%85&amp;url=/archives/1703554497857" width="1" height="1" alt="" style="opacity:0;">
<h1 id="vuex">vuex</h1>
<h2 id="安装">安装</h2>
<h3 id="直接下载-CDN引用">直接下载/CDN引用</h3>
<p><a href="https://unpkg.com/vuex@4">https://unpkg.com/vuex@4</a></p>
<p><a href="https://unpkg.com/">Unpkg.com</a> 提供了基于 npm 的 CDN 链接。以上的链接会一直指向 npm 上发布的最新版本。您也可以通过 <code>https://unpkg.com/vuex@4.0.0/dist/vuex.global.js</code> 这样的方式指定特定的版本。</p>
<p>在 Vue 之后引入 <code>vuex</code> 会进行自动安装：</p>
<pre><code>&lt;script src="/path/to/vue.js"&gt;&lt;/script&gt;
&lt;script src="/path/to/vuex.js"&gt;&lt;/script&gt;
</code></pre>
<h2 id="npm-">npm<a href="https://vuex.vuejs.org/zh/installation.html#npm">#</a></h2>
<pre><code>npm install vuex@next --save
</code></pre>
<h2 id="Yarn-">Yarn<a href="https://vuex.vuejs.org/zh/installation.html#yarn">#</a></h2>
<pre><code>yarn add vuex@next --save
</code></pre>
<h2 id="自己构建-">自己构建<a href="https://vuex.vuejs.org/zh/installation.html#%E8%87%AA%E5%B7%B1%E6%9E%84%E5%BB%BA">#</a></h2>
<p>如果需要使用 dev 分支下的最新版本，您可以直接从 GitHub 上克隆代码并自己构建。</p>
<pre><code>git clone https://github.com/vuejs/vuex.git node_modules/vuex
cd node_modules/vuex
yarn
yarn build
</code></pre>]]></description><guid isPermaLink="false">/archives/1703554497857</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fvuex.jpg&amp;size=m" type="image/jpeg" length="0"/><category>vue3</category><pubDate>Tue, 26 Dec 2023 01:37:00 GMT</pubDate></item><item><title><![CDATA[Spring Boot 可以同时处理多少请求]]></title><link>https://xiaoming728.com/archives/1702345403545</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Spring%20Boot%20%E5%8F%AF%E4%BB%A5%E5%90%8C%E6%97%B6%E5%A4%84%E7%90%86%E5%A4%9A%E5%B0%91%E8%AF%B7%E6%B1%82&amp;url=/archives/1702345403545" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">作者：微信公众号-码猿技术专栏</p>
 <p style="">日期： 2023-03-07 08:50</p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s/ZUSH3uVOyIIxyDhv_Ttbxw">https://mp.weixin.qq.com/s/ZUSH3uVOyIIxyDhv_Ttbxw</a></p>
</blockquote>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">我们都知道，SpringBoot默认的内嵌容器是Tomcat，也就是我们的程序实际上是运行在Tomcat里的。所以与其说SpringBoot可以处理多少请求，倒不如说Tomcat可以处理多少请求。</span></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">关于Tomcat的默认配置，都在</span><span fontsize="" color="rgb(255, 93, 108)" style="color: rgb(255, 93, 108)">spring-configuration-metadata.json</span><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">文件中，对应的配置类则是</span><span fontsize="" color="rgb(255, 93, 108)" style="color: rgb(255, 93, 108)">org.springframework.boot.autoconfigure.web.ServerProperties</span><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F32da4828582ee16cbe2cb48d99dce64a.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">和处理请求数量相关的参数有四个：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F3729d0f04e5641311a843e4f701f991f.png&amp;size=m" style="display: inline-block"></p>
<ul>
 <li>
  <p style=""><strong><span fontsize="" color="rgb(248, 57, 41)" style="color: rgb(248, 57, 41)">server.tomcat.threads.min-spare</span></strong><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">：最少的工作线程数，默认大小是10。该参数相当于长期工，如果并发请求的数量达不到10，就会依次使用这几个线程去处理请求。</span></p></li>
 <li>
  <p style=""><strong><span fontsize="" color="rgb(248, 57, 41)" style="color: rgb(248, 57, 41)">server.tomcat.threads.max</span></strong><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">：最多的工作线程数，默认大小是200。该参数相当于临时工，如果并发请求的数量在10到200之间，就会使用这些临时工线程进行处理。</span></p></li>
 <li>
  <p style=""><strong><span fontsize="" color="rgb(248, 57, 41)" style="color: rgb(248, 57, 41)">server.tomcat.max-connections</span></strong><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">：最大连接数，默认大小是8192。表示Tomcat可以处理的最大请求数量，超过8192的请求就会被放入到等待队列。</span></p></li>
 <li>
  <p style=""><strong><span fontsize="" color="rgb(248, 57, 41)" style="color: rgb(248, 57, 41)">server.tomcat.accept-count</span></strong><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">：等待队列的长度，默认大小是100。</span></p></li>
</ul>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">举个例子说明一下这几个参数之间的关系：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F4db381e51b900bb01a5f31366b05b97d.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">如果把Tomcat比作一家饭店的话，那么一个请求其实就相当于一位客人。min-spare就是厨师(长期工)；max是厨师总数(长期工+临时工)；max-connections就是饭店里的座位数量；accept-count是门口小板凳的数量。来的客人优先坐到饭店里面，然后厨师开始忙活，如果长期工可以干得完，就让长期工干，如果长期工干不完，就再让临时工干。图中画的厨师一共15人，饭店里有30个座位，也就是说，如果现在来了20个客人，那么就会有5个人先在饭店里等着。如果现在来了35个人，饭店里坐不下，就会让5个人先到门口坐一下。如果来了50个人，那么饭店座位+门口小板凳一共40个，所以就会有10人离开。关注公z号：码猿技术专栏，回复关键词：1111 获取阿里内部性能调优手册</span></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">也就是说，SpringBoot同所能处理的最大请求数量是</span><span fontsize="" color="rgb(255, 93, 108)" style="color: rgb(255, 93, 108)">max-connections+accept-count</span><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">，超过该数量的请求直接就会被丢掉。</span></p>
<p style=""><strong><span fontsize="" color="rgb(248, 57, 41)" style="color: rgb(248, 57, 41)">纸上得来终觉浅，绝知此事要躬行。</span></strong></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">上面只是理论结果，现在通过一个实际的小例子来演示一下到底是不是这样：</span></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">创建一个SpringBoot的项目，在application.yml里配置一下这几个参数，因为默认的数量太大，不好测试，所以配小一点：</span></p>
<pre><code>server:
&nbsp;&nbsp;tomcat:
&nbsp;&nbsp;&nbsp;&nbsp;threads:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;最少线程数
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;min-spare:&nbsp;10
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;最多线程数
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;max:&nbsp;15
&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;最大连接数
&nbsp;&nbsp;&nbsp;&nbsp;max-connections:&nbsp;30
&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;最大等待数
&nbsp;&nbsp;&nbsp;&nbsp;accept-count:&nbsp;10</code></pre>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">再来写一个简单的接口：</span></p>
<pre><code>@GetMapping("/test")
public&nbsp;Response&nbsp;test1(HttpServletRequest&nbsp;request)&nbsp;throws&nbsp;Exception&nbsp;{
    log.info("ip:{},线程:{}",&nbsp;request.getRemoteAddr(),&nbsp;Thread.currentThread().getName());
    Thread.sleep(500);
    return&nbsp;Response.buildSuccess();
}</code></pre>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">代码很简单，只是打印了一下线程名，然后休眠0.5秒，这样肯定会导致部分请求处理一次性处理不了而进入到等待队列。</span></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">然后我用Apifox创建了一个测试用例，去模拟100个请求：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fb9b877f55208609931826bc3b4a88964.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">观察一下测试结果：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F26458df05cafa7e46a7512c7f19c5d4c.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">从结果中可以看出，由于设置的 </span><strong><span fontsize="" color="rgb(248, 57, 41)" style="color: rgb(248, 57, 41)">max-connections+accept-count</span></strong><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)"> 的和是40，所以有60个请求会被丢弃，这和我们的预期是相符的。由于最大线程是15，也就是有25个请求会先等待，等前15个处理完了再处理15个，最后在处理10个，也就是将40个请求分成了15,15,10这样三批进行处理。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F4a69d5b177deeb7b537d275281ce8d94.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">再从控制台的打印日志可以看到，线程的最大编号是15，这也印证了前面的想法。</span></p>
<p style=""><strong><span fontsize="" color="rgb(248, 57, 41)" style="color: rgb(248, 57, 41)">总结一下</span></strong><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">：如果并发请求数量低于</span><strong><span fontsize="" color="rgb(248, 57, 41)" style="color: rgb(248, 57, 41)">server.tomcat.threads.max</span></strong><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">，则会被立即处理，超过的部分会先进行等待，如果数量超过max-connections与accept-count之和，则多余的部分则会被直接丢弃。</span></p>
<p style=""><strong><span fontsize="" color="rgb(234, 84, 41)" style="color: rgb(234, 84, 41)">延伸：并发问题是如何产生的</span></strong></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">到目前为止，就已经搞明白了SpringBoot同时可以处理多少请求的问题。但是在这里我还想基于上面的例子再延伸一下，就是为什么并发场景下会出现一些值和我们预期的不一样？</span></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">设想有以下场景：厨师们用一个账本记录一共做了多少道菜，每个厨师做完菜都记录一下，每次记录都是将账本上的数字先抄到草稿纸上，计算x+1等于多少，然后将计算的结果写回到账本上。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fc2bf9fa2537d2986f857aef7f954381a.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">Spring容器中的Bean默认是单例的，也就是说，处理请求的Controller、Service实例就只有一份。在并发场景下，将cookSum定义为全局变量，是所有线程共享的，当一个线程读到了cookSum=20，然后计算，写回前另一个线程也读到是20，两个线程都加1后写回，最终cookSum就变成了21，但是实际上应该是22，因为加了两次。</span></p>
<pre><code>private&nbsp;int&nbsp;cookSum&nbsp;=&nbsp;0;

@GetMapping("/test")
public&nbsp;Response&nbsp;test1(HttpServletRequest&nbsp;request)&nbsp;throws&nbsp;Exception&nbsp;{
    //&nbsp;做菜。。。。。。
    cookSum&nbsp;+=&nbsp;1;
    log.info("做了{}道菜",&nbsp;cookSum);
    Thread.sleep(500);
    return&nbsp;Response.buildSuccess();
}</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F8b8b9fadb84a1f7b2cdfa016df2ba1f7.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(53, 53, 53)" style="color: rgb(53, 53, 53)">如果要避免这样的情况发生，就涉及到加锁的问题了，就不在这里讨论了。</span></p>]]></description><guid isPermaLink="false">/archives/1702345403545</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fspringboot-1.webp&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Tue, 12 Dec 2023 01:50:00 GMT</pubDate></item><item><title><![CDATA[JVM调优的正确姿势]]></title><link>https://xiaoming728.com/archives/1702345190041</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=JVM%E8%B0%83%E4%BC%98%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF&amp;url=/archives/1702345190041" width="1" height="1" alt="" style="opacity:0;">
<p style="">本文简单说一说JVM应如何调优。</p>
<p style="">Java语言本身的成功，除了天时地利人和，JVM功不可没。</p>
<p style="">毫不夸张地说，JVM是现代软件工程最成功的案例之一。它规模庞大，代码极其复杂，但运行极其稳定可靠，所以，许多厂商的核心业务系统，才敢放心地用Java编写，运行在JVM之上。</p>
<p style="">因为JVM自带GC，又有无数可以微调的参数，所以，JVM调优，现在已经被当作Java面试的必考知识点，精通JVM调优参数的童鞋，可以冠名微操小王子。</p>
<p style="">写了这么多年的Java程序，很遗憾，我迄今为止只会用两个参数：XMS和XMX，能正确写出如下启动脚本：</p>
<pre><code>java -Xms1g -Xmx2g -jar abc.jar</code></pre>
<p style="">如果<code>-server</code>也算的话，一共会三，估计面试通过的概率不大。</p>
<p style="">我承认我对JVM调优几乎一无所知，原因在于，还没有遇到过性能问题必须通过JVM调优才能解决。</p>
<p style="">我发现喜欢研究JVM调优的两类人：</p>
<ul>
 <li>
  <p style="">准备面试的；</p></li>
 <li>
  <p style="">自己写的烂代码想甩锅给JVM的。</p></li>
</ul>
<p style="">绝大多数情况下，如果程序出现了性能问题，比如TPS上不去，内存撑爆了，最好自己冷静一下，先监控一下自己程序的日志和性能数据，如果这两个都没有，就一口咬定JVM有问题，有问题的很可能不是JVM，而是态度。</p>
<p style="">这并不是说JVM不会出问题，或者说JVM就肯定没有bug，而是说，软件领域，bug能不能尽可能地被发现然后修复，很大程度上取决于用的人是否足够多。这个世界上用JVM的人多还是用自己写的程序的人多？很明显，能被某个人发现的JVM的bug可能性很低，尤其是公司线上运行的JVM并不会追求最新版本。</p>
<p style="">那么JVM正确的调优方式是啥？我个人推荐四步走：</p>
<ol>
 <li>
  <p style="">记录好日志；</p></li>
 <li>
  <p style="">对程序做好性能监控；</p></li>
 <li>
  <p style="">根据日志和性能监控数据修改程序；</p></li>
 <li>
  <p style="">使用专业工具通过不同的JVM参数进行压测并获得最佳配置。</p></li>
</ol>
<p style="">根据我的个人经验，走完前三步，就可以高质量交付并下班回家了，第四步那是在有摸鱼时间的情况下才做。</p>
<p style="">这么多年我一共遇到过两次因为JVM参数引发的问题：</p>
<p style="">一次是某公司的超大型Java程序，导致PermGen OutOfMemoryError，那是JDK 1.6平台，原因很简单，编写的Java类数量太多了，撑爆了默认的128M的PermGen。解决方法也很简单，改成更大的512M（参数叫啥已经忘了，因为新版JVM没有PermGen限制了）。但是根本问题不是出在JVM，而是代码太垃圾，Java类的数量超多造成的。</p>
<p style="">另一次是因为TPS超高引起内存不足崩溃，但实际上内存有32G非常大，分配给JVM有30G，不可能用完。现实情况是EC2直接被干掉连日志都看不到了。如果手动把TPS降下来（每次sleep 1ms），就能以一定概率成功启动。后咨询AWS技术支持发现，原来是Kafka这货为了提高速度，用了大量的堆外内存结果在高TPS下爆了。解决方法也很简单，把JVM内存限制在系统内存的一半，给操作系统留出足够的内存。这次根本问题是代码性能太高但错误地设定了XMS和XMX造成的。</p>
<p style="">所以，绝大部分情况下，并不需要特意去调优JVM，因为那是最后一步的优化手段。即使真的需要，到时候再研究也不迟，因为时间是宝贵的，在解决自己程序的性能问题之前，不必在意JVM的性能。</p>
<p style=""></p>
<blockquote>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.liaoxuefeng.com/user/916549967804896">廖雪峰</a><span fontsize="" color="rgb(102, 102, 102)" style="color: rgb(102, 102, 102)">&nbsp;/&nbsp;</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.liaoxuefeng.com/category/895882450960192">编程</a><span fontsize="" color="rgb(102, 102, 102)" style="color: rgb(102, 102, 102)">&nbsp;/&nbsp;2020/3/12 12:39</span></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.liaoxuefeng.com/user/916549967804896">https://www.liaoxuefeng.com/article/1336345083510818</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702345190041</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fjvm.png&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Tue, 12 Dec 2023 01:40:00 GMT</pubDate></item><item><title><![CDATA[JVM 调优最详情方案]]></title><link>https://xiaoming728.com/archives/1702345070391</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=JVM%20%E8%B0%83%E4%BC%98%E6%9C%80%E8%AF%A6%E6%83%85%E6%96%B9%E6%A1%88&amp;url=/archives/1702345070391" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">文章作者：CSDN-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://javafamily.blog.csdn.net/">不才陈某</a></p>
 <p style="">发布日期： <span style="font-size: 14px">2023-02-23 08:50:40</span></p>
 <p style="">原文链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://javafamily.blog.csdn.net/">https://blog.csdn.net/qq_34162294/article/details/129187869</a></p>
</blockquote>
<p style=""></p>
<p style="">问题：假设一个每天100w次登陆请求的平台，一个服务节点 8G 内存，该如何设置JVM参数？</p>
<p style="">下面以问题的形式给大家梳理出来，做到一箭双雕：</p>
<ul>
 <li>
  <p style="">既供大家实操参考</p></li>
 <li>
  <p style="">又供大家面试参考</p></li>
</ul>
<p style="">大家要学习的，除了 JVM 配置方案 之外，是其分析问题的思路、思考问题的视角。这些思路和视角，能帮助大家走更远、更远。接下来，进入正题。</p>
<h2 style="" id="%E6%AF%8F%E5%A4%A9100w%E6%AC%A1%E7%99%BB%E9%99%86%E8%AF%B7%E6%B1%82%2C-8g-%E5%86%85%E5%AD%98%E8%AF%A5%E5%A6%82%E4%BD%95%E8%AE%BE%E7%BD%AEjvm%E5%8F%82%E6%95%B0%EF%BC%9F">每天100w次登陆请求, 8G 内存该如何设置JVM参数？</h2>
<p style="">每天100w次登陆请求, 8G 内存该如何设置JVM参数，大概可以分为以下8个步骤。</p>
<h3 style="" id="step1%EF%BC%9A%E6%96%B0%E7%B3%BB%E7%BB%9F%E4%B8%8A%E7%BA%BF%E5%A6%82%E4%BD%95%E8%A7%84%E5%88%92%E5%AE%B9%E9%87%8F%EF%BC%9F">Step1：新系统上线如何规划容量？</h3>
<h4 style="" id="1.%E5%A5%97%E8%B7%AF%E6%80%BB%E7%BB%93">1.套路总结</h4>
<p style="">任何新的业务系统在上线以前都需要去估算服务器配置和JVM的内存参数，这个容量与资源规划并不仅仅是系统架构师的随意估算的，需要根据系统所在业务场景去估算，推断出来一个系统运行模型，评估JVM性能和GC频率等等指标。以下是我结合大牛经验以及自身实践来总结出来的一个建模步骤：</p>
<ul>
 <li>
  <p style="">计算业务系统每秒钟创建的对象会佔用多大的内存空间，然后计算集群下的每个系统每秒的内存佔用空间（对象创建速度）</p></li>
 <li>
  <p style="">设置一个机器配置，估算新生代的空间，比较不同新生代大小之下，多久触发一次MinorGC。</p></li>
 <li>
  <p style="">为了避免频繁GC，就可以重新估算需要多少机器配置，部署多少台机器，给JVM多大内存空间，新生代多大空间。</p></li>
 <li>
  <p style="">根据这套配置，基本可以推算出整个系统的运行模型，每秒创建多少对象，1s以后成为垃圾，系统运行多久新生代会触发一次GC，频率多高。</p></li>
</ul>
<h4 style="" id="2.%E5%A5%97%E8%B7%AF%E5%AE%9E%E6%88%98">2.套路实战</h4>
<p style="">以登录系统为例</p>
<p style="">有些同学看到这些步骤还是发憷，说的好像是那么回事，一到实际项目中到底怎麽做我还是不知道！</p>
<p style="">光说不练假把式，以登录系统为例模拟一下推演过程：</p>
<ul>
 <li>
  <p style="">假设每天100w次登陆请求，登陆峰值在早上，预估峰值时期每秒100次登陆请求。</p></li>
 <li>
  <p style="">假设部署3台服务器，每台机器每秒处理30次登陆请求，假设一个登陆请求需要处理1秒钟，JVM新生代里每秒就要生成30个登陆对象，1s之后请求完毕这些对象成为了垃圾。</p></li>
 <li>
  <p style="">一个登陆请求对象假设20个字段，一个对象估算500字节，30个登陆佔用大约15kb，考虑到RPC和DB操作，网络通信、写库、写缓存一顿操作下来，可以扩大到20-50倍，大约1s产生几百k-1M数据。</p></li>
 <li>
  <p style="">假设2C4G机器部署，分配2G堆内存，新生代则只有几百M，按照1s1M的垃圾产生速度，几百秒就会触发一次MinorGC了。</p></li>
 <li>
  <p style="">假设4C8G机器部署，分配4G堆内存，新生代分配2G，如此需要几个小时才会触发一次MinorGC。</p></li>
</ul>
<p style="">所以，可以粗略的推断出来一个每天100w次请求的登录系统，按照4C8G的3实例集群配置，分配4G堆内存、2G新生代的JVM，可以保障系统的一个正常负载。</p>
<p style="">基本上把一个新系统的资源评估了出来，所以搭建新系统要每个实例需要多少容量多少配置，集群配置多少个实例等等这些，并不是拍拍脑袋和胸脯就可以决定的下来的。</p>
<h3 style="" id="step2%EF%BC%9A%E8%AF%A5%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E7%9A%84%E9%80%89%E6%8B%A9%EF%BC%9F">Step2：该如何进行垃圾回收器的选择？</h3>
<h4 style="" id="%E5%90%9E%E5%90%90%E9%87%8F%E8%BF%98%E6%98%AF%E5%93%8D%E5%BA%94%E6%97%B6%E9%97%B4">吞吐量还是响应时间</h4>
<p style="">首先引入两个概念：吞吐量和低延迟</p>
<p style="">吞吐量 = CPU在用户应用程序运行的时间 / （CPU在用户应用程序运行的时间 + CPU垃圾回收的时间）</p>
<p style="">响应时间 = 平均每次的GC的耗时</p>
<p style="">通常，吞吐优先还是响应优先这个在JVM中是一个两难之选。</p>
<p style="">堆内存增大，gc一次能处理的数量变大，吞吐量大；但是gc一次的时间会变长，导致后面排队的线程等待时间变长；相反，如果堆内存小，gc一次时间短，排队等待的线程等待时间变短，延迟减少，但一次请求的数量变小（并不绝对符合）。</p>
<p style="">无法同时兼顾，是吞吐优先还是响应优先，这是一个需要权衡的问题。</p>
<h4 style="" id="%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E8%AE%BE%E8%AE%A1%E4%B8%8A%E7%9A%84%E8%80%83%E9%87%8F">垃圾回收器设计上的考量</h4>
<ul>
 <li>
  <p style="">JVM在GC时不允许一边垃圾回收，一边还创建新对象（就像不能一边打扫卫生，还在一边扔垃圾）。</p></li>
 <li>
  <p style="">JVM需要一段Stop the world的暂停时间，而STW会造成系统短暂停顿不能处理任何请求；</p></li>
 <li>
  <p style="">新生代收集频率高，性能优先，常用复制算法；老年代频次低，空间敏感，避免复制方式。</p></li>
 <li>
  <p style="">所有垃圾回收器的涉及目标都是要让GC频率更少，时间更短，减少GC对系统影响！</p></li>
</ul>
<h4 style="" id="cms%E5%92%8Cg1">CMS和G1</h4>
<p style="">目前主流的垃圾回收器配置是新生代采用ParNew，老年代采用CMS组合的方式，或者是完全采用G1回收器，</p>
<p style="">从未来的趋势来看，G1是官方维护和更为推崇的垃圾回收器。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F26d01397a9c079e0116ea46d67fe9bbf.png&amp;size=m" style="display: inline-block"></p>
<p style="">业务系统:</p>
<ul>
 <li>
  <p style="">延迟敏感的推荐CMS；</p></li>
 <li>
  <p style="">大内存服务，要求高吞吐的，采用G1回收器！</p></li>
</ul>
<h4 style="" id="cms%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6">CMS垃圾回收器的工作机制</h4>
<p style="">CMS主要是针对老年代的回收器，老年代是标记-清除，默认会在一次FullGC算法后做整理算法，清理内存碎片。</p>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">CMS GC</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">描述</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">Stop the world</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">速度</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">1.开始标记</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">初始标记仅标记GCRoots能直接关联到的对象，速度很快</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">Yes</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">很快</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">2.并发标记</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">并发标记阶段就是进行GCRoots Tracing的过程</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">No</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">慢</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">3.重新标记</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">Yes</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">很快</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">4.垃圾回收</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">并发清理垃圾对象(标记清除算法)</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">No</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">慢</p></td>
  </tr>
 </tbody>
</table>
<ul>
 <li>
  <p style="">优点：并发收集、主打“低延时” 。在最耗时的两个阶段都没有发生STW，而需要STW的阶段都以很快速度完成。</p></li>
 <li>
  <p style="">缺点：1、消耗CPU；2、浮动垃圾；3、内存碎片</p></li>
 <li>
  <p style="">适用场景：重视服务器响应速度，要求系统停顿时间最短。</p></li>
</ul>
<p style="">总之：</p>
<p style="">业务系统，延迟敏感的推荐CMS；</p>
<p style="">大内存服务，要求高吞吐的，采用G1回收器！</p>
<h3 style="" id="step3%EF%BC%9A%E5%A6%82%E4%BD%95%E5%AF%B9%E5%90%84%E4%B8%AA%E5%88%86%E5%8C%BA%E7%9A%84%E6%AF%94%E4%BE%8B%E3%80%81%E5%A4%A7%E5%B0%8F%E8%BF%9B%E8%A1%8C%E8%A7%84%E5%88%92">Step3：如何对各个分区的比例、大小进行规划</h3>
<p style="">一般的思路为:</p>
<p style="">首先，JVM最重要最核心的参数是去评估内存和分配，第一步需要指定堆内存的大小，这个是系统上线必须要做的，-Xms 初始堆大小，-Xmx 最大堆大小，后台Java服务中一般都指定为系统内存的一半，过大会佔用服务器的系统资源，过小则无法发挥JVM的最佳性能。</p>
<p style="">其次，需要指定-Xmn新生代的大小，这个参数非常关键，灵活度很大，虽然sun官方推荐为3/8大小，但是要根据业务场景来定，针对于无状态或者轻状态服务（现在最常见的业务系统如Web应用）来说，一般新生代甚至可以给到堆内存的3/4大小；关注公z号：码猿技术专栏，回复关键词：1111 获取阿里内部java性能调优手册！而对于有状态服务（常见如IM服务、网关接入层等系统）新生代可以按照默认比例1/3来设置。服务有状态，则意味著会有更多的本地缓存和会话状态信息常驻内存，应为要给老年代设置更大的空间来存放这些对象。</p>
<p style="">最后，是设置-Xss栈内存大小，设置单个线程栈大小，默认值和JDK版本、系统有关，一般默认512~1024kb。一个后台服务如果常驻线程有几百个，那麽栈内存这边也会佔用了几百M的大小。</p>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">JVM参数</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">描述</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">默认</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">推荐</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">-Xms</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">Java堆内存的大小</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">OS内存64/1</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">OS内存一半</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">-Xmx</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">Java堆内存的最大大小</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">OS内存4/1</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">OS内存一半</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">-Xmn</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">Java堆内存中的新生代大小，扣除新生代剩下的就是老年代的内存大小了</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">跌认堆的1/3</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">sun推荐3/8</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">-Xss</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">每个线程的栈内存大小</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">和idk有关</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">sun</p></td>
  </tr>
 </tbody>
</table>
<p style="">对于8G内存，一般分配一半的最大内存就可以了,因为机器本上还要占用一定内存，一般是分配4G内存给JVM，</p>
<p style="">引入性能压测环节，测试同学对登录接口压至1s内60M的对象生成速度，采用ParNew+CMS的组合回收器，</p>
<p style="">正常的JVM参数配置如下：</p>
<pre><code>-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8</code></pre>
<p style="">这样设置可能会由于动态对象年龄判断原则导致频繁full gc。为啥呢？</p>
<p style="">压测过程中，短时间（比如20S后）Eden区就满了，此时再运行的时候对象已经无法分配，会触发MinorGC，</p>
<p style="">假设在这次GC后S1装入100M，马上过20S又会触发一次MinorGC，多出来的100M存活对象+S1区的100M已经无法顺利放入到S2区，此时就会触发JVM的动态年龄机制，将一批100M左右的对象推到老年代保存，持续运行一段时间，系统可能一个小时候内就会触发一次FullGC。</p>
<p style="">按照默认8:1:1的比例来分配时, survivor区只有 1G的 10%左右，也就是几十到100M，</p>
<p style="">如果 每次minor GC垃圾回收过后进入survivor对象很多，并且survivor对象大小很快超过 Survivor 的 50% ， 那么会触发动态年龄判定规则，让部分对象进入老年代.</p>
<p style="">而一个GC过程中，可能部分WEB请求未处理完毕, 几十兆对象，进入survivor的概率，是非常大的，甚至是一定会发生的.</p>
<p style="">如何解决这个问题呢？为了让对象尽可能的在新生代的eden区和survivor区, 尽可能的让survivor区内存多一点,达到200兆左右,</p>
<p style="">于是我们可以更新下JVM参数设置：</p>
<pre><code>-Xms3072M&nbsp;-Xmx3072M&nbsp;-Xmn2048M&nbsp;-Xss1M&nbsp;-XX:MetaspaceSize=256M&nbsp;-XX:MaxMetaspaceSize=256M&nbsp;&nbsp;-XX:SurvivorRatio=8&nbsp;&nbsp;
说明：
‐Xmn2048M&nbsp;‐XX:SurvivorRatio=8&nbsp;
年轻代大小2g，eden与survivor的比例为8:1:1，也就是1.6g:0.2g:0.2g</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F20ab5b01d5783e832102a36726874c2d.png&amp;size=m" style="display: inline-block"></p>
<p style="">survivor达到200m，如果几十兆对象到底survivor， survivor 也不一定超过 50%</p>
<p style="">这样可以防止每次垃圾回收过后，survivor对象太早超过 50% ,</p>
<p style="">这样就降低了因为对象动态年龄判断原则导致的对象频繁进入老年代的问题，</p>
<h4 style="" id="%E4%BB%80%E4%B9%88%E6%98%AFjvm%E5%8A%A8%E6%80%81%E5%B9%B4%E9%BE%84%E5%88%A4%E6%96%AD%E8%A7%84%E5%88%99%E5%91%A2%EF%BC%9F">什么是JVM动态年龄判断规则呢？</h4>
<p style="">对象进入老年代的动态年龄判断规则（动态晋升年龄计算阈值）：Minor GC 时，Survivor 中年龄 1 到 N 的对象大小超过 Survivor 的 50% 时，则将大于等于年龄 N 的对象放入老年代。</p>
<p style="">核心的优化策略是：是让短期存活的对象尽量都留在survivor里，不要进入老年代，这样在minor gc的时候这些对象都会被回收，不会进到老年代从而导致full gc。</p>
<h4 style="" id="%E5%BA%94%E8%AF%A5%E5%A6%82%E4%BD%95%E5%8E%BB%E8%AF%84%E4%BC%B0%E6%96%B0%E7%94%9F%E4%BB%A3%E5%86%85%E5%AD%98%E5%92%8C%E5%88%86%E9%85%8D%E5%90%88%E9%80%82%EF%BC%9F">应该如何去评估新生代内存和分配合适？</h4>
<p style="">这里特别说一下，JVM最重要最核心的参数是去评估内存和分配，</p>
<p style="">第一步需要指定堆内存的大小，这个是系统上线必须要做的，-Xms 初始堆大小，-Xmx 最大堆大小，</p>
<p style="">后台Java服务中一般都指定为系统内存的一半，过大会佔用服务器的系统资源，过小则无法发挥JVM的最佳性能。</p>
<p style="">其次需要指定-Xmn新生代的大小，这个参数非常关键，灵活度很大，虽然sun官方推荐为3/8大小，但是要根据业务场景来定：</p>
<ul>
 <li>
  <p style="">针对于无状态或者轻状态服务（现在最常见的业务系统如Web应用）来说，一般新生代甚至可以给到堆内存的3/4大小；</p></li>
 <li>
  <p style="">而对于有状态服务（常见如IM服务、网关接入层等系统）新生代可以按照默认比例1/3来设置。</p></li>
</ul>
<p style="">服务有状态，则意味著会有更多的本地缓存和会话状态信息常驻内存，应为要给老年代设置更大的空间来存放这些对象。</p>
<h3 style="" id="step4%EF%BC%9A%E6%A0%88%E5%86%85%E5%AD%98%E5%A4%A7%E5%B0%8F%E5%A4%9A%E5%B0%91%E6%AF%94%E8%BE%83%E5%90%88%E9%80%82%EF%BC%9F">step4：栈内存大小多少比较合适？</h3>
<p style="">-Xss栈内存大小，设置单个线程栈大小，默认值和JDK版本、系统有关，一般默认512~1024kb。一个后台服务如果常驻线程有几百个，那麽栈内存这边也会佔用了几百M的大小。</p>
<h3 style="" id="step5%EF%BC%9A%E5%AF%B9%E8%B1%A1%E5%B9%B4%E9%BE%84%E5%BA%94%E8%AF%A5%E4%B8%BA%E5%A4%9A%E5%B0%91%E6%89%8D%E7%A7%BB%E5%8A%A8%E5%88%B0%E8%80%81%E5%B9%B4%E4%BB%A3%E6%AF%94%E8%BE%83%E5%90%88%E9%80%82%EF%BC%9F">step5：对象年龄应该为多少才移动到老年代比较合适？</h3>
<p style="">假设一次minor gc要间隔二三十秒，并且，大多数对象一般在几秒内就会变为垃圾，</p>
<p style="">如果对象这么长时间都没被回收，比如2分钟没有回收，可以认为这些对象是会存活的比较长的对象，从而移动到老年代，而不是继续一直占用survivor区空间。</p>
<p style="">所以，可以将默认的15岁改小一点，比如改为5，</p>
<p style="">那么意味着对象要经过5次minor gc才会进入老年代，整个时间也有一两分钟了（5*30s= 150s），和几秒的时间相比，对象已经存活了足够长时间了。</p>
<p style="">所以：可以适当调整JVM参数如下：</p>
<pre><code>‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5</code></pre>
<h3 style="" id="step6%EF%BC%9A%E5%A4%9A%E5%A4%A7%E7%9A%84%E5%AF%B9%E8%B1%A1%EF%BC%8C%E5%8F%AF%E4%BB%A5%E7%9B%B4%E6%8E%A5%E5%88%B0%E8%80%81%E5%B9%B4%E4%BB%A3%E6%AF%94%E8%BE%83%E5%90%88%E9%80%82%EF%BC%9F">step6：多大的对象，可以直接到老年代比较合适？</h3>
<p style="">对于多大的对象直接进入老年代(参数-XX:PretenureSizeThreshold)，一般可以结合自己系统看下有没有什么大对象 生成，预估下大对象的大小，一般来说设置为1M就差不多了，很少有超过1M的大对象，</p>
<p style="">所以：可以适当调整JVM参数如下：</p>
<pre><code>‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M</code></pre>
<h3 style="" id="step7%EF%BC%9A%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8cms%E8%80%81%E5%B9%B4%E4%BB%A3%E7%9A%84%E5%8F%82%E6%95%B0%E4%BC%98%E5%8C%96">step7：垃圾回收器CMS老年代的参数优化</h3>
<p style="">JDK8默认的垃圾回收器是<code>-XX:+UseParallelGC</code>(年轻代)和<code>-XX:+UseParallelOldGC</code>(老年代)，</p>
<p style="">如果内存较大(超过4个G，只是经验 值)，还是建议使用G1.</p>
<p style="">这里是4G以内，又是主打“低延时” 的业务系统，可以使用下面的组合：</p>
<pre><code>ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)</code></pre>
<p style="">新生代的采用ParNew回收器，工作流程就是经典复制算法，在三块区中进行流转回收，只不过采用多线程并行的方式加快了MinorGC速度。</p>
<p style="">老生代的采用CMS。再去优化老年代参数：比如老年代默认在标记清除以后会做整理，还可以在CMS的增加GC频次还是增加GC时长上做些取舍，</p>
<p style="">如下是响应优先的参数调优：</p>
<pre><code>XX:CMSInitiatingOccupancyFraction=70</code></pre>
<p style="">设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC)</p>
<pre><code>XX:+UseCMSInitiatinpOccupancyOnly</code></pre>
<p style="">和上面搭配使用，否则只生效一次</p>
<pre><code>-XX:+AlwaysPreTouch</code></pre>
<p style="">强制操作系统把内存真正分配给IVM，而不是用时才分配。</p>
<p style="">综上，只要年轻代参数设置合理，老年代CMS的参数设置基本都可以用默认值，如下所示：</p>
<pre><code>‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8  ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=70 ‐XX:+UseCMSInitiatingOccupancyOnly ‐XX:+AlwaysPreTouch</code></pre>
<p style="">参数解释</p>
<ol>
 <li>
  <p style="">‐Xms3072M ‐Xmx3072M 最小最大堆设置为3g，最大最小设置为一致防止内存抖动</p></li>
 <li>
  <p style="">‐Xss1M 线程栈1m</p></li>
 <li>
  <p style="">‐Xmn2048M ‐XX:SurvivorRatio=8 年轻代大小2g，eden与survivor的比例为8:1:1，也就是1.6g:0.2g:0.2g</p></li>
 <li>
  <p style="">-XX:MaxTenuringThreshold=5 年龄为5进入老年代 5.‐XX:PretenureSizeThreshold=1M 大于1m的大对象直接在老年代生成</p></li>
 <li>
  <p style="">‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC 使用ParNew+cms垃圾回收器组合</p></li>
 <li>
  <p style="">‐XX:CMSInitiatingOccupancyFraction=70 老年代中对象达到这个比例后触发fullgc</p></li>
 <li>
  <p style="">‐XX:+UseCMSInitiatinpOccupancyOnly 老年代中对象达到这个比例后触发fullgc，每次</p></li>
 <li>
  <p style="">‐XX:+AlwaysPreTouch 强制操作系统把内存真正分配给IVM，而不是用时才分配。</p></li>
</ol>
<h3 style="" id="step8%EF%BC%9A%E9%85%8D%E7%BD%AEoom%E6%97%B6%E5%80%99%E7%9A%84%E5%86%85%E5%AD%98dump%E6%96%87%E4%BB%B6%E5%92%8Cgc%E6%97%A5%E5%BF%97">step8：配置OOM时候的内存dump文件和GC日志</h3>
<p style="">额外增加了GC日志打印、OOM自动dump等配置内容，帮助进行问题排查</p>
<pre><code>-XX:+HeapDumpOnOutOfMemoryError</code></pre>
<p style="">在Out Of Memory，JVM快死掉的时候，输出Heap Dump到指定文件。</p>
<p style="">不然开发很多时候还真不知道怎么重现错误。</p>
<p style="">路径只指向目录，JVM会保持文件名的唯一性，叫<code>java_pid${pid}.hprof</code>。</p>
<pre><code>-XX:+HeapDumpOnOutOfMemoryError&nbsp;
-XX:HeapDumpPath=${LOGDIR}/</code></pre>
<p style="">因为如果指向特定的文件，而文件已存在，反而不能写入。</p>
<p style="">输出4G的HeapDump，会导致IO性能问题，在普通硬盘上，会造成20秒以上的硬盘IO跑满，</p>
<p style="">需要注意一下，但在容器环境下，这个也会影响同一宿主机上的其他容器。</p>
<p style="">GC的日志的输出也很重要：</p>
<pre><code>-Xloggc:/dev/xxx/gc.log&nbsp;
-XX:+PrintGCDateStamps&nbsp;
-XX:+PrintGCDetails</code></pre>
<p style="">GC的日志实际上对系统性能影响不大，打日志对排查GC问题很重要。</p>
<h4 style="" id="%E4%B8%80%E4%BB%BD%E9%80%9A%E7%94%A8%E7%9A%84jvm%E5%8F%82%E6%95%B0%E6%A8%A1%E6%9D%BF">一份通用的JVM参数模板</h4>
<p style="">一般来说，大企业或者架构师团队，都会为项目的业务系统定制一份较为通用的JVM参数模板，但是许多小企业和团队可能就疏于这一块的设计，如果老板某一天突然让你负责定制一个新系统的JVM参数，你上网去搜大量的JVM调优文章或博客，结果发现都是零零散散的、不成体系的JVM参数讲解，根本下不了手，这个时候你就需要一份较为通用的JVM参数模板了，不能保证性能最佳，但是至少能让JVM这一层是稳定可控的，</p>
<p style="">在这里给大家总结了一份模板：</p>
<p style="">基于4C8G系统的ParNew+CMS回收器模板（响应优先），新生代大小根据业务灵活调整！</p>
<pre><code>-Xms4g
-Xmx4g
-Xmn2g
-Xss1m
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=10
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+AlwaysPreTouch
-XX:+HeapDumpOnOutOfMemoryError
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:gc.log</code></pre>
<h4 style="" id="%E5%A6%82%E6%9E%9C%E6%98%AFgc%E7%9A%84%E5%90%9E%E5%90%90%E4%BC%98%E5%85%88%EF%BC%8C%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8g1%EF%BC%8C%E5%9F%BA%E4%BA%8E8c16g%E7%B3%BB%E7%BB%9F%E7%9A%84g1%E5%9B%9E%E6%94%B6%E5%99%A8%E6%A8%A1%E6%9D%BF%EF%BC%9A">如果是GC的吞吐优先，推荐使用G1，基于8C16G系统的G1回收器模板：</h4>
<p style="">G1收集器自身已经有一套预测和调整机制了，因此我们首先的选择是相信它，</p>
<p style="">即调整-XX:MaxGCPauseMillis=N参数，这也符合G1的目的——让GC调优尽量简单！</p>
<p style="">同时也不要自己显式设置新生代的大小（用-Xmn或-XX:NewRatio参数），</p>
<p style="">如果人为干预新生代的大小，会导致目标时间这个参数失效。</p>
<pre><code>-Xms8g
-Xmx8g
-Xss1m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=150
-XX:InitiatingHeapOccupancyPercent=40
-XX:+HeapDumpOnOutOfMemoryError
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:gc.log</code></pre>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">G1参数</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">描述</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(239, 243, 245)">
    <p style="">默认值</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">XX:MaxGCPauseMillis=N</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">最大GC停顿时间。柔性目标，JVM满足90%，不保证100%。</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">200</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">-XX:nitiatingHeapOccupancyPercent=n</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">当整个堆的空间使用百分比超过这个值时，就会融发MixGC</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(247, 247, 247)">
    <p style="">45</p></td>
  </tr>
 </tbody>
</table>
<p style="">针对<code>-XX:MaxGCPauseMillis</code>来说，参数的设置带有明显的倾向性：调低↓：延迟更低，但MinorGC频繁，MixGC回收老年代区减少，增大Full GC的风险。调高↑：单次回收更多的对象，但系统整体响应时间也会被拉长。</p>
<p style="">针对<code>InitiatingHeapOccupancyPercent</code>来说，调参大小的效果也不一样：调低↓：更早触发MixGC，浪费cpu。调高↑：堆积过多代回收region，增大FullGC的风险。</p>
<h3 style="" id="%E8%B0%83%E4%BC%98%E6%80%BB%E7%BB%93">调优总结</h3>
<p style="">系统在上线前的综合调优思路：</p>
<p style="">1、业务预估：根据预期的并发量、平均每个任务的内存需求大小，然后评估需要几台机器来承载，每台机器需要什么样的配置。</p>
<p style="">2、容量预估：根据系统的任务处理速度，然后合理分配Eden、Surivior区大小，老年代的内存大小。</p>
<p style="">3、回收器选型：响应优先的系统，建议采用ParNew+CMS回收器；吞吐优先、多核大内存(heap size≥8G)服务，建议采用G1回收器。</p>
<p style="">4、优化思路：让短命对象在MinorGC阶段就被回收（同时回收后的存活对象&lt;Survivor区域50%，可控制保留在新生代），长命对象尽早进入老年代，不要在新生代来回复制；尽量减少Full GC的频率，避免FGC系统的影响。</p>
<p style="">5、到目前为止，总结到的调优的过程主要基于上线前的测试验证阶段，所以我们尽量在上线之前，就将机器的JVM参数设置到最优！</p>
<p style="">JVM调优只是一个手段，但并不一定所有问题都可以通过JVM进行调优解决，大多数的Java应用不需要进行JVM优化，我们可以遵循以下的一些原则：</p>
<ul>
 <li>
  <p style="">上线之前，应先考虑将机器的JVM参数设置到最优；</p></li>
 <li>
  <p style="">减少创建对象的数量（代码层面）；</p></li>
 <li>
  <p style="">减少使用全局变量和大对象（代码层面）；</p></li>
 <li>
  <p style="">优先架构调优和代码调优，JVM优化是不得已的手段（代码、架构层面）；</p></li>
 <li>
  <p style="">分析GC情况优化代码比优化JVM参数更好（代码层面）；</p></li>
</ul>
<p style="">通过以上原则，我们发现，其实最有效的优化手段是架构和代码层面的优化，而JVM优化则是最后不得已的手段，也可以说是对服务器配置的最后一次“压榨”。</p>
<h2 style="" id="%E9%97%AE%E9%A2%98%E6%B1%87%E6%80%BB">问题汇总</h2>
<h3 style="" id="1%E3%80%81%E4%BB%80%E4%B9%88%E6%98%AFzgc">1、什么是ZGC</h3>
<p style="">ZGC （Z Garbage Collector）是一款由Oracle公司研发的，以低延迟为首要目标的一款垃圾收集器。</p>
<p style="">它是基于动态Region内存布局，（暂时）不设年龄分代，使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。</p>
<p style="">在 JDK 11 新加入，还在实验阶段，</p>
<p style="">主要特点是：回收TB级内存（最大4T），停顿时间不超过10ms。</p>
<p style="">优点：低停顿，高吞吐量， ZGC 收集过程中额外耗费的内存小</p>
<p style="">缺点：浮动垃圾</p>
<p style="">目前使用的非常少，真正普及还是需要写时间的。</p>
<h3 style="" id="2%E3%80%81%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8">2、如何选择垃圾收集器</h3>
<p style="">在真实场景中应该如何去选择呢，下面给出几种建议，希望对你有帮助：</p>
<p style="">1、如果你的堆大小不是很大（比如 100MB ），选择串行收集器一般是效率最高的。参数：<code>-XX:+UseSerialGC</code> 。</p>
<p style="">2、如果你的应用运行在单核的机器上，或者你的虚拟机核数只有 单核，选择串行收集器依然是合适的，这时候启用一些并行收集器没有任何收益。参数：<code>-XX:+UseSerialGC</code>。</p>
<p style="">3、如果你的应用是“吞吐量”优先的，并且对较长时间的停顿没有什么特别的要求。选择并行收集器是比较好的。参数：<code>-XX:+UseParallelGC</code> 。</p>
<p style="">4、如果你的应用对响应时间要求较高，想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失败，那么选择 G1 、ZGC 、CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短，但它需要一些额外的资源去处理这些工作，通常吞吐量会低一些。参数：<code>-XX:+UseConcMarkSweepGC</code>、<code>-XX:+UseG1GC </code>、<code> -XX:+UseZGC</code> 等。从上面这些出发点来看，我们平常的 Web 服务器，都是对响应性要求非常高的。</p>
<p style="">选择性其实就集中在 CMS、G1、ZGC 上。而对于某些定时任务，使用并行收集器，是一个比较好的选择。</p>
<h3 style="" id="3%E3%80%81hotspot%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A8%E5%85%83%E7%A9%BA%E9%97%B4%E6%9B%BF%E6%8D%A2%E4%BA%86%E6%B0%B8%E4%B9%85%E4%BB%A3">3、Hotspot为什么使用元空间替换了永久代</h3>
<p style="">什么是元空间？什么是永久代？为什么用元空间代替永久代？</p>
<p style="">我们先回顾一下方法区吧,看看虚拟机运行时数据内存图，如下:</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F550960%2F1678350428514-ffd52e94-5337-4ed8-93c3-b22a33adc5fc.png&amp;size=m" width="654" style="display: inline-block"></p>
<p style="">方法区和堆一样，是各个线程共享的内存区域，它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。</p>
<p style="">什么是永久代？它和方法区有什么关系呢？</p>
<p style="">如果在HotSpot虚拟机上开发、部署，很多程序员都把方法区称作永久代。</p>
<p style="">可以说方法区是规范，永久代是Hotspot针对该规范进行的实现。</p>
<p style="">在Java7及以前的版本，方法区都是永久代实现的。</p>
<p style="">什么是元空间？它和方法区有什么关系呢？</p>
<p style="">对于Java8，HotSpots取消了永久代，取而代之的是元空间(Metaspace)。</p>
<p style="">换句话说，就是方法区还是在的，只是实现变了，从永久代变为元空间了。</p>
<p style="">为什么使用元空间替换了永久代？</p>
<p style="">永久代的方法区，和堆使用的物理内存是连续的。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F5ff94e033e58d3dae2bee1aa135856e4.png&amp;size=m" style="display: inline-block"></p>
<p style="">永久代是通过以下这两个参数配置大小的~</p>
<ul>
 <li>
  <p style=""><code>-XX:PremSize</code>：设置永久代的初始大小</p></li>
 <li>
  <p style=""><code>-XX:MaxPermSize</code>: 设置永久代的最大值，默认是64M</p></li>
</ul>
<p style="">对于永久代，如果动态生成很多class的话，就很可能出现<code>java.lang.OutOfMemoryError:PermGen space</code>错误，因为永久代空间配置有限嘛。最典型的场景是，在web开发比较多jsp页面的时候。</p>
<p style="">JDK8之后，方法区存在于元空间(Metaspace)。</p>
<p style="">物理内存不再与堆连续，而是直接存在于本地内存中，理论上机器内存有多大，元空间就有多大。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F89686cf0a04575795829ee2fb16bd998.png&amp;size=m" style="display: inline-block"></p>
<p style="">可以通过以下的参数来设置元空间的大小：</p>
<ul>
 <li>
  <p style=""><code>-XX:MetaspaceSize</code>，初始空间大小，达到该值就会触发垃圾收集进行类型卸载，同时GC会对该值进行调整：如果释放了大量的空间，就适当降低该值；如果释放了很少的空间，那么在不超过<code>MaxMetaspaceSize</code>时，适当提高该值。</p></li>
 <li>
  <p style=""><code>-XX:MaxMetaspaceSize</code>，最大空间，默认是没有限制的。</p></li>
 <li>
  <p style=""><code>-XX:MinMetaspaceFreeRatio</code>，在GC之后，最小的<code>Metaspace</code>剩余空间容量的百分比，减少为分配空间所导致的垃圾收集</p></li>
 <li>
  <p style=""><code>-XX:MaxMetaspaceFreeRatio</code>，在GC之后，最大的<code>Metaspace</code>剩余空间容量的百分比，减少为释放空间所导致的垃圾收集</p></li>
</ul>
<p style="">所以，为什么使用元空间替换永久代？</p>
<p style="">表面上看是为了避免OOM异常。</p>
<p style="">因为通常使用PermSize和<code>MaxPermSize</code>设置永久代的大小就决定了永久代的上限，但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。</p>
<p style="">当使用元空间时，可以加载多少类的元数据就不再由<code>MaxPermSize</code>控制, 而由系统的实际可用空间来控制啦。</p>
<h3 style="" id="4%E3%80%81%E4%BB%80%E4%B9%88%E6%98%AFstop-the-world-%E4%BB%80%E4%B9%88%E6%98%AFoopmap-%E4%BB%80%E4%B9%88%E6%98%AF%E5%AE%89%E5%85%A8%E7%82%B9">4、什么是Stop The World 什么是OopMap 什么是安全点</h3>
<p style="">进行垃圾回收的过程中，会涉及对象的移动。</p>
<p style="">为了保证对象引用更新的正确性，必须暂停所有的用户线程，像这样的停顿，虚拟机设计者形象描述为Stop The World。也简称为STW。</p>
<p style="">在HotSpot中，有个数据结构（映射表）称为OopMap。</p>
<p style="">一旦类加载动作完成的时候，HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来，记录到OopMap。</p>
<p style="">在即时编译过程中，也会在特定的位置生成 OopMap，记录下栈上和寄存器里哪些位置是引用。</p>
<p style="">这些特定的位置主要在：</p>
<ol>
 <li>
  <p style="">循环的末尾（非 counted 循环）</p></li>
 <li>
  <p style="">方法临返回前 / 调用方法的call指令后</p></li>
 <li>
  <p style="">可能抛异常的位置</p></li>
</ol>
<p style="">这些位置就叫作安全点(safepoint)。</p>
<p style="">用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集，而是必须是执行到安全点才能够暂停。</p>]]></description><guid isPermaLink="false">/archives/1702345070391</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fjvm.png&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Tue, 12 Dec 2023 01:39:00 GMT</pubDate></item><item><title><![CDATA[Tomcat配置JVM参数优化]]></title><link>https://xiaoming728.com/archives/1702345037578</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Tomcat%E9%85%8D%E7%BD%AEJVM%E5%8F%82%E6%95%B0%E4%BC%98%E5%8C%96&amp;url=/archives/1702345037578" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">文章作者：CSDN-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/ldx891113">ldongxu</a></p>
 <p style="">发布日期：2022-06-23 20:43:38</p>
 <p style="">原文链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/ldx891113">https://blog.csdn.net/ldx891113/article/details/51735171</a></p>
</blockquote>
<p style="">Tomcat 的缺省配置是不能稳定长期运行的，也就是不适合生产环境，它会死机，让你不断重新启动，甚至在午夜时分唤醒你。对于操作系统优化来说，是尽可能的增大可使用的内存容量、提高CPU 的频率，保证文件系统的读写速率等。经过压力测试验证，在并发连接很多的情况下，CPU 的处理能力越强，系统运行速度越快。</p>
<p style="">Tomcat 的优化不像其它软件那样，简简单单的修改几个参数就可以了，它的优化主要有三方面，分为系统优化，Tomcat 本身的优化，Java 虚拟机（JVM）调优。系统优化就不在介绍了，接下来就详细的介绍一下 Tomcat 本身与 JVM 优化，以 Tomcat 7 为例。</p>
<h3 style="" id="%E4%B8%80%E3%80%81tomcat-%E6%9C%AC%E8%BA%AB%E4%BC%98%E5%8C%96">一、Tomcat 本身优化</h3>
<p style="">Tomcat 的自身参数的优化，这块很像 ApacheHttp Server。修改一下 xml 配置文件中的参数，调整最大连接数，超时等。此外，我们安装 Tomcat 时，优化就已经开始了。</p>
<h4 style="" id="1%E3%80%81%E5%B7%A5%E4%BD%9C%E6%96%B9%E5%BC%8F%E9%80%89%E6%8B%A9">1、工作方式选择</h4>
<p style="">为了提升性能，首先就要对代码进行动静分离，让 Tomcat 只负责 jsp 文件的解析工作。如采用 Apache 和 Tomcat 的整合方式，他们之间的连接方案有三种选择，JK、http_proxy 和 ajp_proxy。相对于 JK 的连接方式，后两种在配置上比较简单的，灵活性方面也一点都不逊色。但就稳定性而言不像JK 这样久经考验，所以建议采用 JK 的连接方式。</p>
<h4 style="" id="2%E3%80%81connector-%E8%BF%9E%E6%8E%A5%E5%99%A8%E7%9A%84%E9%85%8D%E7%BD%AE">2、Connector 连接器的配置</h4>
<p style="">之前文件介绍过的 Tomcat 连接器的三种方式： bio、nio 和 apr，三种方式性能差别很大，apr 的性能最优， bio 的性能最差。而 Tomcat 7 使用的 Connector 默认就启用的 Apr 协议，但需要系统安装 Apr 库，否则就会使用 bio 方式。</p>
<h4 style="" id="3%E3%80%81%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E4%BC%98%E5%8C%96">3、配置文件优化</h4>
<p style="">配置文件优化其实就是对 server.xml 优化，可以提大大提高 Tomcat 的处理请求的能力，下面我们来看 Tomcat 容器内的优化。</p>
<p style="">默认配置下，Tomcat 会为每个连接器创建一个绑定的线程池（最大线程数 200），服务启动时，默认创建了 5 个空闲线程随时等待用户请求。</p>
<p style="">首先，打开<code>${TOMCAT_HOME}/conf/server.xml</code>，搜索【&lt;Executor name="tomcatThreadPool"】，开启并调整为</p>
<pre><code>&lt;Executor name = "tomcatThreadPool" 
  namePrefix = "catalina-exec-"
  maxThreads = "500"  
  minSpareThreads = "20" 
  maxSpareThreads = "50"  
  maxIdleTime = "60000" /&gt;</code></pre>
<p style="">注意， Tomcat 7 在开启线程池前，一定要安装好 Apr 库，并可以启用，否则会有错误报出，<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/ldx891113">shutdown.sh</a> 脚本无法关闭进程。</p>
<p style="">然后，修改&lt;Connector …&gt;节点，增加 executor 属性，搜索【port="8080"】，调整为</p>
<pre><code> &lt; Connector  executor = "tomcatThreadPool"
                port = "8080"  protocol = "HTTP/1.1"
                URIEncoding = "UTF-8"
                connectionTimeout = "30000"
                enableLookups = "false"
                disableUploadTimeout = "false"
                connectionUploadTimeout = "150000"
                acceptCount = "300"
                keepAliveTimeout = "120000"
                maxKeepAliveRequests = "1"
                compression = "on"
                compressionMinSize = "2048"
                compressableMimeType = "text/html,text/xml,text/javascript,text/css,text/plain,image/gif,image/jpg,image/png" 
                redirectPort = "8443"  /&gt;</code></pre>
<ul>
 <li>
  <p style="">maxThreads :Tomcat 使用线程来处理接收的每个请求，这个值表示 Tomcat 可创建的最大的线程数，默认值是 200</p></li>
 <li>
  <p style="">minSpareThreads：最小空闲线程数，Tomcat 启动时的初始化的线程数，表示即使没有人使用也开这么多空线程等待，默认值是 10。</p></li>
 <li>
  <p style="">maxSpareThreads：最大备用线程数，一旦创建的线程超过这个值，Tomcat 就会关闭不再需要的 socket 线程。</p></li>
</ul>
<p style="">上边配置的参数，最大线程 500（一般服务器足以），要根据自己的实际情况合理设置，设置越大会耗费内存和 CPU，因为 CPU 疲于线程上下文切换，没有精力提供请求服务了，最小空闲线程数 20，线程最大空闲时间 60 秒，当然允许的最大线程连接数还受制于操作系统的内核参数设置，设置多大要根据自己的需求与环境。当然线程可以配置在“tomcatThreadPool”中，也可以直接配置在“Connector”中，但不可以重复配置。</p>
<ul>
 <li>
  <p style="">URIEncoding：指定 Tomcat 容器的 URL 编码格式，语言编码格式这块倒不如其它 WEB 服务器软件配置方便，需要分别指定。</p></li>
 <li>
  <p style="">connnectionTimeout： 网络连接超时，单位：毫秒，设置为 0 表示永不超时，这样设置有隐患的。通常可设置为 30000 毫秒，可根据检测实际情况，适当修改。</p></li>
 <li>
  <p style="">enableLookups： 是否反查域名，以返回远程主机的主机名，取值为：true 或 false，如果设置为false，则直接返回IP地址，为了提高处理能力，应设置为 false。</p></li>
 <li>
  <p style="">disableUploadTimeout：上传时是否使用超时机制。</p></li>
 <li>
  <p style="">connectionUploadTimeout：上传超时时间，毕竟文件上传可能需要消耗更多的时间，这个根据你自己的业务需要自己调，以使Servlet有较长的时间来完成它的执行，需要与上一个参数一起配合使用才会生效。</p></li>
 <li>
  <p style="">acceptCount：指定当所有可以使用的处理请求的线程数都被使用时，可传入连接请求的最大队列长度，超过这个数的请求将不予处理，默认为100个。</p></li>
 <li>
  <p style="">keepAliveTimeout：长连接最大保持时间（毫秒），表示在下次请求过来之前，Tomcat 保持该连接多久，默认是使用 connectionTimeout 时间，-1 为不限制超时。</p></li>
 <li>
  <p style="">maxKeepAliveRequests：表示在服务器关闭之前，该连接最大支持的请求数。超过该请求数的连接也将被关闭，1表示禁用，-1表示不限制个数，默认100个，一般设置在100~200之间。</p></li>
 <li>
  <p style="">compression：是否对响应的数据进行 GZIP 压缩，off：表示禁止压缩；on：表示允许压缩（文本将被压缩）、force：表示所有情况下都进行压缩，默认值为off，压缩数据后可以有效的减少页面的大小，一般可以减小1/3左右，节省带宽。</p></li>
 <li>
  <p style="">compressionMinSize：表示压缩响应的最小值，只有当响应报文大小大于这个值的时候才会对报文进行压缩，如果开启了压缩功能，默认值就是2048。</p></li>
 <li>
  <p style="">compressableMimeType：压缩类型，指定对哪些类型的文件进行数据压缩。</p></li>
 <li>
  <p style="">noCompressionUserAgents="gozilla, traviata"： 对于以下的浏览器，不启用压缩。</p></li>
</ul>
<p style="">如果已经对代码进行了动静分离，静态页面和图片等数据就不需要 Tomcat 处理了，那么也就不需要配置在 Tomcat 中配置压缩了。</p>
<p style="">以上是一些常用的配置参数属性，当然还有好多其它的参数设置，还可以继续深入的优化，HTTP Connector 与 AJP Connector 的参数属性值，可以参考官方文档的详细说明：</p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://tomcat.apache.org/tomcat-7.0-doc/config/http.html">Apache Tomcat 7 Configuration Reference (7.0.109) - The HTTP Connector</a></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://tomcat.apache.org/tomcat-7.0-doc/config/ajp.html">Apache Tomcat 7 Configuration Reference (7.0.109) - The AJP Connector</a></p>
<h3 style="" id="%E4%BA%8C%E3%80%81jvm-%E4%BC%98%E5%8C%96">二、JVM 优化</h3>
<p style="">Tomcat 启动命令行中的优化参数，就是 JVM 的优化 。Tomcat 首先跑在 JVM 之上的，因为它的启动其实也只是一个 java 命令行，首先我们需要对这个 JAVA 的启动命令行进行调优。不管是 YGC 还是 Full GC，GC 过程中都会对导致程序运行中中断，正确的选择不同的 GC 策略，调整 JVM、GC 的参数，可以极大的减少由于 GC 工作，而导致的程序运行中断方面的问题，进而适当的提高 Java 程序的工作效率。但是调整 GC 是以个极为复杂的过程，由于各个程序具备不同的特点，如：web 和 GUI 程序就有很大区别（Web可以适当的停顿，但GUI停顿是客户无法接受的），而且由于跑在各个机器上的配置不同（主要 cup 个数，内存不同），所以使用的 GC 种类也会不同。</p>
<h4 style="" id="1%E3%80%81jvm-%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95">1、JVM 参数配置方法</h4>
<p style="">Tomcat 的启动参数位于安装目录 ${JAVA_HOME}/bin目录下，Linux 操作系统就是 <a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/ldx891113">catalina.sh</a> 文件。JAVA_OPTS，就是用来设置 JVM 相关运行参数的变量，还可以在 CATALINA_OPTS 变量中设置。关于这 2 个变量，还是多少有些区别的：</p>
<ul>
 <li>
  <p style="">JAVA_OPTS：用于当 Java 运行时选项“start”、“stop”或“run”命令执行。</p></li>
 <li>
  <p style="">CATALINA_OPTS：用于当 Java 运行时选项“start”或“run”命令执行。</p></li>
</ul>
<p style="">为什么有两个不同的变量？它们之间都有什么区别呢？</p>
<p style="">首先，在启动 Tomcat 时，任何指定变量的传递方式都是相同的，可以传递到执行“start”或“run”命令中，但只有设定在 JAVA_OPTS 变量里的参数被传递到“stop”命令中。对于 Tomcat 运行过程，可能没什么区别，影响的是结束程序，而不是启动程序。</p>
<p style="">第二个区别是更微妙，其他应用程序也可以使用 JAVA_OPTS 变量，但只有在 Tomcat 中使用 CATALINA_OPTS 变量。如果你设置环境变量为只使用 Tomcat，最好你会建议使用 CATALINA_OPTS 变量，而如果你设置环境变量使用其它的 Java 应用程序，例如 JBoss，你应该把你的设置放在JAVA_OPTS 变量中。</p>
<h4 style="" id="2%E3%80%81jvm-%E5%8F%82%E6%95%B0%E5%B1%9E%E6%80%A7">2、JVM 参数属性</h4>
<p style="">32 位系统下 JVM 对内存的限制：不能突破 2GB ，那么这时你的 Tomcat 要优化，就要讲究点技巧了，而在 64 位操作系统上无论是系统内存还是 JVM 都没有受到 2GB 这样的限制。</p>
<p style="">针对于 JMX 远程监控也是在这里设置，以下为 64 位系统环境下的配置，内存加入的参数如下：</p>
<pre><code>CATALINA_OPTS="
-server 
-Xms6000M 
-Xmx6000M 
-Xss512k 
-XX:NewSize=2250M 
-XX:MaxNewSize=2250M 
-XX:PermSize=128M
-XX:MaxPermSize=256M  
-XX:+AggressiveOpts 
-XX:+UseBiasedLocking 
-XX:+DisableExplicitGC 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC 
-XX:MaxTenuringThreshold=31 
-XX:+CMSParallelRemarkEnabled 
-XX:+UseCMSCompactAtFullCollection 
-XX:LargePageSizeInBytes=128m 
-XX:+UseFastAccessorMethods 
-XX:+UseCMSInitiatingOccupancyOnly
-Duser.timezone=Asia /Shanghai 
-Djava.awt.headless= true "</code></pre>
<p style="">为了看着方便，将每个参数单独写一行。上面参数好多啊，可能有人写到现在都没见过一个在 Tomcat 的启动命令里加了这么多参数，当然，这些参数只是我机器上的，不一定适合你，尤其是参数后的 value（值）是需要根据你自己的实际情况来设置的。</p>
<p style="">上述这样的配置，基本上可以达到：</p>
<ol>
 <li>
  <p style="">系统响应时间增快；</p></li>
 <li>
  <p style="">JVM回收速度增快同时又不影响系统的响应率；</p></li>
 <li>
  <p style="">JVM内存最大化利用；</p></li>
 <li>
  <p style="">线程阻塞情况最小化。</p></li>
</ol>
<p style="">JVM 常用参数详解：</p>
<ul>
 <li>
  <p style="">-server：一定要作为第一个参数，在多个 CPU 时性能佳，还有一种叫 -client 的模式，特点是启动速度比较快，但运行时性能和内存管理效率不高，通常用于客户端应用程序或开发调试，在 32 位环境下直接运行 Java 程序默认启用该模式。Server 模式的特点是启动速度比较慢，但运行时性能和内存管理效率很高，适用于生产环境，在具有 64 位能力的 JDK 环境下默认启用该模式，可以不配置该参数。</p></li>
 <li>
  <p style="">-Xms：表示 Java 初始化堆的大小，-Xms 与-Xmx 设成一样的值，避免 JVM 反复重新申请内存，导致性能大起大落，默认值为物理内存的 1/64，默认（MinHeapFreeRatio参数可以调整）空余堆内存小于 40% 时，JVM 就会增大堆直到 -Xmx 的最大限制。</p></li>
 <li>
  <p style="">-Xmx：表示最大 Java 堆大小，当应用程序需要的内存超出堆的最大值时虚拟机就会提示内存溢出，并且导致应用服务崩溃，因此一般建议堆的最大值设置为可用内存的最大值的80%。如何知道我的 JVM 能够使用最大值，使用 java -Xmx512M -version 命令来进行测试，然后逐渐的增大 512 的值,如果执行正常就表示指定的内存大小可用，否则会打印错误信息，默认值为物理内存的 1/4，默认（MinHeapFreeRatio参数可以调整）空余堆内存大于 70% 时，JVM 会减少堆直到-Xms 的最小限制。</p></li>
 <li>
  <p style="">-Xss：表示每个 Java 线程堆栈大小，JDK 5.0 以后每个线程堆栈大小为 1M，以前每个线程堆栈大小为 256K。根据应用的线程所需内存大小进行调整，在相同物理内存下，减小这个值能生成更多的线程，但是操作系统对一个进程内的线程数还是有限制的，不能无限生成，经验值在 3000~5000 左右。一般小的应用， 如果栈不是很深， 应该是128k 够用的，大的应用建议使用 256k 或 512K，一般不易设置超过 1M，要不然容易出现out ofmemory。这个选项对性能影响比较大，需要严格的测试。</p></li>
 <li>
  <p style="">-XX:NewSize：设置新生代内存大小。</p></li>
 <li>
  <p style="">-XX:MaxNewSize：设置最大新生代新生代内存大小</p></li>
 <li>
  <p style="">-XX:PermSize：设置持久代内存大小</p></li>
 <li>
  <p style="">-XX:MaxPermSize：设置最大值持久代内存大小，永久代不属于堆内存，堆内存只包含新生代和老年代。</p></li>
 <li>
  <p style="">-XX:+AggressiveOpts：作用如其名（aggressive），启用这个参数，则每当 JDK 版本升级时，你的 JVM 都会使用最新加入的优化技术（如果有的话）。</p></li>
 <li>
  <p style="">-XX:+UseBiasedLocking：启用一个优化了的线程锁，我们知道在我们的appserver，每个http请求就是一个线程，有的请求短有的请求长，就会有请求排队的现象，甚至还会出现线程阻塞，这个优化了的线程锁使得你的appserver内对线程处理自动进行最优调配。</p></li>
 <li>
  <p style="">-XX:+DisableExplicitGC：在 程序代码中不允许有显示的调用“System.gc()”。每次在到操作结束时手动调用 System.gc() 一下，付出的代价就是系统响应时间严重降低，就和关于 Xms，Xmx 里的解释的原理一样，这样去调用 GC 导致系统的 JVM 大起大落。</p></li>
 <li>
  <p style="">-XX:+UseConcMarkSweepGC：设置年老代为并发收集，即 CMS gc，这一特性只有 jdk1.5</p></li>
 <li>
  <p style="">后续版本才具有的功能，它使用的是 gc 估算触发和 heap 占用触发。我们知道频频繁的 GC 会造面 JVM</p></li>
 <li>
  <p style="">的大起大落从而影响到系统的效率，因此使用了 CMS GC 后可以在 GC 次数增多的情况下，每次 GC 的响应时间却很短，比如说使用了 CMS GC 后经过 jprofiler 的观察，GC 被触发次数非常多，而每次 GC 耗时仅为几毫秒。</p></li>
 <li>
  <p style="">-XX:+UseParNewGC：对新生代采用多线程并行回收，这样收得快，注意最新的 JVM 版本，当使用 -XX:+UseConcMarkSweepGC 时，-XX:UseParNewGC 会自动开启。因此，如果年轻代的并行 GC 不想开启，可以通过设置 -XX：-UseParNewGC 来关掉。</p></li>
 <li>
  <p style="">-XX:MaxTenuringThreshold：设置垃圾最大年龄。如果设置为0的话，则新生代对象不经过 Survivor 区，直接进入老年代。对于老年代比较多的应用（需要大量常驻内存的应用），可以提高效率。如果将此值设置为一 个较大值，则新生代对象会在 Survivor 区进行多次复制，这样可以增加对象在新生代的存活时间，增加在新生代即被回收的概率，减少Full GC的频率，这样做可以在某种程度上提高服务稳定性。该参数只有在串行 GC 时才有效，这个值的设置是根据本地的 jprofiler 监控后得到的一个理想的值，不能一概而论原搬照抄。</p></li>
 <li>
  <p style="">-XX:+CMSParallelRemarkEnabled：在使用 UseParNewGC 的情况下，尽量减少 mark 的时间。</p></li>
 <li>
  <p style="">-XX:+UseCMSCompactAtFullCollection：在使用 concurrent gc 的情况下，防止 memoryfragmention，对 live object 进行整理，使 memory 碎片减少。</p></li>
 <li>
  <p style="">-XX:LargePageSizeInBytes：指定 Java heap 的分页页面大小，内存页的大小不可设置过大， 会影响 Perm 的大小。</p></li>
 <li>
  <p style="">-XX:+UseFastAccessorMethods：使用 get，set 方法转成本地代码，原始类型的快速优化。</p></li>
 <li>
  <p style="">-XX:+UseCMSInitiatingOccupancyOnly：只有在 oldgeneration 在使用了初始化的比例后 concurrent collector 启动收集。</p></li>
 <li>
  <p style="">-Duser.timezone=Asia/Shanghai：设置用户所在时区。</p></li>
 <li>
  <p style="">-Djava.awt.headless=true：这个参数一般我们都是放在最后使用的，这全参数的作用是这样的，有时我们会在我们的 J2EE 工程中使用一些图表工具如：jfreechart，用于在 web 网页输出 GIF/JPG 等流，在 winodws 环境下，一般我们的 app server 在输出图形时不会碰到什么问题，但是在linux/unix 环境下经常会碰到一个 exception 导致你在 winodws 开发环境下图片显示的好好可是在 linux/unix 下却显示不出来，因此加上这个参数以免避这样的情况出现。</p></li>
 <li>
  <p style="">-Xmn：新生代的内存空间大小，注意：此处的大小是（eden+ 2 survivor space)。与 jmap -heap 中显示的 New gen 是不同的。整个堆大小 = 新生代大小 + 老生代大小 + 永久代大小。在保证堆大小不变的情况下，增大新生代后，将会减小老生代大小。此值对系统性能影响较大，Sun官方推荐配置为整个堆的 3/8。</p></li>
 <li>
  <p style="">-XX:CMSInitiatingOccupancyFraction：当堆满之后，并行收集器便开始进行垃圾收集，例如，当没有足够的空间来容纳新分配或提升的对象。对于 CMS 收集器，长时间等待是不可取的，因为在并发垃圾收集期间应用持续在运行（并且分配对象）。因此，为了在应用程序使用完内存之前完成垃圾收集周期，CMS 收集器要比并行收集器更先启动。因为不同的应用会有不同对象分配模式，JVM 会收集实际的对象分配（和释放）的运行时数据，并且分析这些数据，来决定什么时候启动一次 CMS 垃圾收集周期。这个参数设置有很大技巧，基本上满足(Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100 &gt;= Xmn 就不会出现 promotion failed。例如在应用中 Xmx 是6000，Xmn 是 512，那么 Xmx-Xmn 是 5488M，也就是老年代有 5488M，CMSInitiatingOccupancyFraction=90 说明老年代到 90% 满的时候开始执行对老年代的并发垃圾回收（CMS），这时还 剩 10% 的空间是 5488*10% = 548M，所以即使 Xmn（也就是新生代共512M）里所有对象都搬到老年代里，548M 的空间也足够了，所以只要满足上面的公式，就不会出现垃圾回收时的 promotion failed，因此这个参数的设置必须与 Xmn 关联在一起。</p></li>
 <li>
  <p style="">-XX:+CMSIncrementalMode：该标志将开启 CMS 收集器的增量模式。增量模式经常暂停 CMS 过程，以便对应用程序线程作出完全的让步。因此，收集器将花更长的时间完成整个收集周期。因此，只有通过测试后发现正常 CMS 周期对应用程序线程干扰太大时，才应该使用增量模式。由于现代服务器有足够的处理器来适应并发的垃圾收集，所以这种情况发生得很少，用于但 CPU情况。</p></li>
 <li>
  <p style="">-XX:NewRatio：年轻代（包括 Eden 和两个 Survivor 区）与年老代的比值（除去持久代），-XX:NewRatio=4 表示年轻代与年老代所占比值为 1:4，年轻代占整个堆栈的 1/5，Xms=Xmx 并且设置了 Xmn 的情况下，该参数不需要进行设置。</p></li>
 <li>
  <p style="">-XX:SurvivorRatio：Eden 区与 Survivor 区的大小比值，设置为 8，表示 2 个 Survivor 区（JVM 堆内存年轻代中默认有 2 个大小相等的 Survivor 区）与 1 个 Eden 区的比值为 2:8，即 1 个 Survivor 区占整个年轻代大小的 1/10。</p></li>
 <li>
  <p style="">-XX:+UseSerialGC：设置串行收集器。</p></li>
 <li>
  <p style="">-XX:+UseParallelGC：设置为并行收集器。此配置仅对年轻代有效。即年轻代使用并行收集，而年老代仍使用串行收集。</p></li>
 <li>
  <p style="">-XX:+UseParallelOldGC：配置年老代垃圾收集方式为并行收集，JDK6.0 开始支持对年老代并行收集。</p></li>
 <li>
  <p style="">-XX:ConcGCThreads：早期 JVM 版本也叫-XX:ParallelCMSThreads，定义并发 CMS 过程运行时的线程数。比如 value=4 意味着 CMS 周期的所有阶段都以 4 个线程来执行。尽管更多的线程会加快并发 CMS 过程，但其也会带来额外的同步开销。因此，对于特定的应用程序，应该通过测试来判断增加 CMS 线程数是否真的能够带来性能的提升。如果还标志未设置，JVM 会根据并行收集器中的 -XX:ParallelGCThreads 参数的值来计算出默认的并行 CMS 线程数。</p></li>
 <li>
  <p style="">-XX:ParallelGCThreads：配置并行收集器的线程数，即：同时有多少个线程一起进行垃圾回收，此值建议配置与 CPU 数目相等。</p></li>
 <li>
  <p style="">-XX:OldSize：设置 JVM 启动分配的老年代内存大小，类似于新生代内存的初始大小 -XX:NewSize。</p></li>
</ul>
<p style="">以上就是一些常用的配置参数，有些参数是可以被替代的，配置思路需要考虑的是 Java 提供的垃圾回收机制。虚拟机的堆大小决定了虚拟机花费在收集垃圾上的时间和频度。收集垃圾能够接受的速度和应用有关，应该通过分析实际的垃圾收集的时间和频率来调整。假如堆的大小很大，那么完全垃圾收集就会很慢，但是频度会降低。假如您把堆的大小和内存的需要一致，完全收集就很快，但是会更加频繁。调整堆大小的的目的是最小化垃圾收集的时间，以在特定的时间内最大化处理客户的请求。在基准测试的时候，为确保最好的性能，要把堆的大小设大，确保垃圾收集不在整个基准测试的过程中出现。</p>
<p style="">假如系统花费很多的时间收集垃圾，请减小堆大小。一次完全的垃圾收集应该不超过 3-5 秒。假如垃圾收集成为瓶颈，那么需要指定代的大小，检查垃圾收集的周详输出，研究垃圾收集参数对性能的影响。当增加处理器时，记得增加内存，因为分配能够并行进行，而垃圾收集不是并行的。</p>
<h4 style="" id="3%E3%80%81%E8%AE%BE%E7%BD%AE%E7%B3%BB%E7%BB%9F%E5%B1%9E%E6%80%A7">3、设置系统属性</h4>
<p style="">之前说过，Tomcat 的语言编码，配置起来很慢，要经过多次设置才可以了，否则中文很有可能出现乱码情况。譬如汉字“中”，以 UTF-8 编码后得到的是 3 字节的值 %E4%B8%AD，然后通过 GET 或者 POST 方式把这 3 个字节提交到 Tomcat 容器，如果你不告诉 Tomcat 我的参数是用 UTF-8编码的，那么 Tomcat 就认为你是用 ISO-8859-1 来编码的，而 ISO8859-1（兼容 URI 中的标准字符集 US-ASCII）是兼容 ASCII 的单字节编码并且使用了单字节内的所有空间，因此 Tomcat 就以为你传递的用 ISO-8859-1 字符集编码过的 3 个字符，然后它就用 ISO-8859-1 来解码。</p>
<p style="">设置起来不难使用“ -D&lt;名称&gt;=&lt;值&gt; ”来设置系统属性：</p>
<pre><code>-Djavax.servlet.request.encoding=UTF-8
-Djavax.servlet.response.encoding=UTF-8 
-Dfile.encoding=UTF-8 
-Duser.country=CN 
-Duser.language=zh</code></pre>
<h4 style="" id="4%E3%80%81%E5%B8%B8%E8%A7%81%E7%9A%84-java-%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E6%9C%89%E4%BB%A5%E4%B8%8B%E4%B8%89%E7%A7%8D">4、常见的 Java 内存溢出有以下三种</h4>
<p style=""><strong>（1） java.lang.OutOfMemoryError: Java heap space —JVM Heap（堆）溢出</strong></p>
<p style="">JVM 在启动的时候会自动设置 JVM Heap 的值，其初始空间（即-Xms）是物理内存的1/64，最大空间（-Xmx）不可超过物理内存。可以利用 JVM提供的 -Xmn -Xms -Xmx 等选项可进行设置。Heap 的大小是 Young Generation 和 Tenured Generaion 之和。在 JVM 中如果 98％ 的时间是用于 GC，且可用的 Heap size 不足 2％ 的时候将抛出此异常信息。</p>
<p style=""><strong>解决方法：</strong>手动设置 JVM Heap（堆）的大小。</p>
<p style=""><strong>（2） java.lang.OutOfMemoryError: PermGen space — PermGen space溢出。</strong></p>
<p style="">PermGen space 的全称是 Permanent Generation space，是指内存的永久保存区域。为什么会内存溢出，这是由于这块内存主要是被 JVM 存放Class 和 Meta 信息的，Class 在被 Load 的时候被放入 PermGen space 区域，它和存放 Instance 的 Heap 区域不同，sun 的 GC 不会在主程序运行期对 PermGen space 进行清理，所以如果你的 APP 会载入很多 CLASS 的话，就很可能出现 PermGen space 溢出。</p>
<p style=""><strong>解决方法：</strong> 手动设置 MaxPermSize 大小</p>
<p style=""><strong>（3） java.lang.StackOverflowError — 栈溢出</strong></p>
<p style="">栈溢出了，JVM 依然是采用栈式的虚拟机，这个和 C 与 Pascal 都是一样的。函数的调用过程都体现在堆栈和退栈上了。调用构造函数的 “层”太多了，以致于把栈区溢出了。通常来讲，一般栈区远远小于堆区的，因为函数调用过程往往不会多于上千层，而即便每个函数调用需要 1K 的空间（这个大约相当于在一个 C 函数内声明了 256 个 int 类型的变量），那么栈区也不过是需要 1MB 的空间。通常栈的大小是 1－2MB 的。</p>
<p style="">通常递归也不要递归的层次过多，很容易溢出。</p>
<p style=""><strong>解决方法：</strong>修改程序。</p>
<h3 style="" id="%E5%9B%9B%E3%80%81%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">四、参考资料</h3>
<p style="">JVM调优总结：典型配置举例：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/ldx891113">https://www.51cto.com/article/311739.html</a></p>
<p style="">JVM基础：JVM参数设置分析：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/ldx891113">https://www.51cto.com/article/312018.html</a></p>
<p style="">JVM 堆内存相关的启动参数：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/ldx891113">http://www.2cto.com/kf/201409/334840.html</a></p>
<p style="">Java 虚拟机–新生代与老年代GC：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/ldx891113">https://my.oschina.net/sunnywu/blog/332870</a></p>
<p style="">JVM（Java虚拟机）优化大全和案例实战：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/ldx891113">https://blog.csdn.net/kthq/article/details/8618052</a></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702345037578</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fjvm.png&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Tue, 12 Dec 2023 01:37:00 GMT</pubDate></item><item><title><![CDATA[Nginx 配置清单]]></title><link>https://xiaoming728.com/archives/1702344911243</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Nginx%20%E9%85%8D%E7%BD%AE%E6%B8%85%E5%8D%95&amp;url=/archives/1702344911243" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><span style="font-size: 15px">来源：微信公众号-JavaCat</span></p>
 <p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzI2NzUxNjIyOA==&amp;amp;mid=2247486402&amp;amp;idx=1&amp;amp;sn=0db1ee4e5667d653932218d4914d0a56&amp;amp;chksm=eafcebe9dd8b62ff7dec6f7a95b517e90d81fed4a0823c823469567a3d27775de3aa9d1274e5&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=1216CRn77ZSDjVbwvDTh56lR&amp;amp;sharer_sharetime=1639668502463&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px">https://mp.weixin.qq.com/s?__biz=MzI2NzUxNjIyOA==&amp;mid=2247486402&amp;idx=1&amp;sn=0db1ee4e5667d653932218d4914d0a56&amp;chksm=eafcebe9dd8b62ff7dec6f7a95b517e90d81fed4a0823c823469567a3d27775de3aa9d1274e5&amp;mpshare=1&amp;scene=23&amp;srcid=1216CRn77ZSDjVbwvDTh56lR&amp;sharer_sharetime=1639668502463&amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd</span></a></p>
 <p style=""><span style="font-size: 15px">日期：2021-12-16 17:47</span></p>
</blockquote>
<p style=""><span style="font-size: 15px; color: rgb(80, 97, 109)">Nginx 是一个高性能的 HTTP 和反向代理 web 服务器，同时也提供了 IMAP/POP3/SMTP 服务，其因丰富的功能集、稳定性、示例配置文件和低系统资源的消耗受到了开发者的欢迎。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(80, 97, 109)">本文，我们总结了一些常用的 Nginx 配置代码，希望对大家有所帮助。</span></p>
<h4 style="" id="%E4%BE%A6%E5%90%AC%E7%AB%AF%E5%8F%A3"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">侦听端口</span></h4>
<pre><code>server {  
  # Standard HTTP Protocol  
  listen 80;  
  # Standard HTTPS Protocol  
  listen 443 ssl;  
  # For http2  
  listen 443 ssl http2;  
  # Listen on 80 using IPv6  
  listen [::]:80;  
  # Listen only on using IPv6  
  listen [::]:80 ipv6only=on;  
}  </code></pre>
<h4 style="" id="%E8%AE%BF%E9%97%AE%E6%97%A5%E5%BF%97"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">访问日志</span></h4>
<pre><code>server {  
  # Relative or full path to log file  
  access_log /path/to/file.log;  
  # Turn 'on' or 'off'    
  access_log on;  
} </code></pre>
<h4 style="" id="%E5%9F%9F%E5%90%8D"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">域名</span></h4>
<pre><code>server {  
  # Listen to yourdomain.com  
  server_name yourdomain.com;  
  # Listen to multiple domains server_name yourdomain.com www.yourdomain.com;  
  # Listen to all domains  
  server_name *.yourdomain.com;  
  # Listen to all top-level domains  
  server_name yourdomain.*;  
  # Listen to unspecified Hostnames (Listens to IP address itself)  
  server_name "";  
}  </code></pre>
<h4 style="" id="%E9%9D%99%E6%80%81%E8%B5%84%E4%BA%A7"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">静态资产</span></h4>
<pre><code>server {  
  listen 80;  
  server_name yourdomain.com;  
  location / {  
  	root /path/to/website;  
  }  
}  </code></pre>
<h4 style="" id="%E9%87%8D%E5%AE%9A%E5%90%91"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">重定向</span></h4>
<pre><code>server {  
  listen 80;  
  server_name www.yourdomain.com;  
  return 301 http://yourdomain.com$request_uri;  
}  
server {  
  listen 80;  
  server_name www.yourdomain.com;  
  location /redirect-url {  
  	return 301 http://otherdomain.com;  
  }  
}  </code></pre>
<h4 style="" id="%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">反向代理</span></h4>
<pre><code>server {  
  listen 80;  
  server_name yourdomain.com;  
  location / {  
    proxy_pass http://0.0.0.0:3000;  
    # where 0.0.0.0:3000 is your application server (Ex: node.js) bound on 0.0.0.0 listening on port 3000  
  }  
}  </code></pre>
<h4 style="" id="%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">负载均衡</span></h4>
<pre><code>upstream node_js {  
  server 0.0.0.0:3000;  
  server 0.0.0.0:4000;  
  server 123.131.121.122;  
}  
server {  
  listen 80;  
  server_name yourdomain.com;  
  location / {  
  	proxy_pass http://node_js;  
  }  
}  </code></pre>
<h4 style="" id="ssl-%E5%8D%8F%E8%AE%AE"><strong><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">SSL 协议</span></strong></h4>
<pre><code>server {  
  listen 443 ssl;  
  server_name yourdomain.com;  
  ssl on;  
  ssl_certificate /path/to/cert.pem;  
  ssl_certificate_key /path/to/privatekey.pem;  
  ssl_stapling on;  
  ssl_stapling_verify on;  
  ssl_trusted_certificate /path/to/fullchain.pem;  
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;  
  ssl_session_timeout 1h;  
  ssl_session_cache shared:SSL:50m;  
  add_header Strict-Transport-Security max-age=15768000;  
}  
# Permanent Redirect for HTTP to HTTPS  
server {  
  listen 80;  
  server_name yourdomain.com;  
  return 301 https://$host$request_uri;  
}  </code></pre>
<p style=""><span style="font-size: 15px; color: rgb(80, 97, 109)">其实可以采用可视化的方式对 Nginx 进行配置，我在 GitHub 上发现了一款可以一键生成 Nginx 配置的神器，相当给力。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(80, 97, 109)">先来看看它都支持什么功能的配置：反向代理、HTTPS、HTTP/2、IPv6, 缓存、WordPress、CDN、Node.js 支持、 Python (Django) 服务器等等。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(80, 97, 109)">如果你想在线进行配置，只需要打开网站：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzI2NzUxNjIyOA==&amp;amp;mid=2247486402&amp;amp;idx=1&amp;amp;sn=0db1ee4e5667d653932218d4914d0a56&amp;amp;chksm=eafcebe9dd8b62ff7dec6f7a95b517e90d81fed4a0823c823469567a3d27775de3aa9d1274e5&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=1216CRn77ZSDjVbwvDTh56lR&amp;amp;sharer_sharetime=1639668502463&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px">https://nginxconfig.io/</span><span style="font-size: 15px; color: rgb(80, 97, 109)">，按照自己的需求进行操作就行了。</span></a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fc028d7315519dc495a7d99be16bd98b8-rtvf.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(80, 97, 109)">选择你的场景，填写好参数，系统就会自动生成配置文件。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(80, 97, 109)">开源地址：</span></p>
<ul>
 <li>
  <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzI2NzUxNjIyOA==&amp;amp;mid=2247486402&amp;amp;idx=1&amp;amp;sn=0db1ee4e5667d653932218d4914d0a56&amp;amp;chksm=eafcebe9dd8b62ff7dec6f7a95b517e90d81fed4a0823c823469567a3d27775de3aa9d1274e5&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=1216CRn77ZSDjVbwvDTh56lR&amp;amp;sharer_sharetime=1639668502463&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px; color: rgb(80, 97, 109)">github.com/digitalocean/nginxconfig.io</span></a></p></li>
</ul>
<p style=""><span style="font-size: 15px; color: rgb(80, 97, 109)">网站：</span></p>
<ul>
 <li>
  <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzI2NzUxNjIyOA==&amp;amp;mid=2247486402&amp;amp;idx=1&amp;amp;sn=0db1ee4e5667d653932218d4914d0a56&amp;amp;chksm=eafcebe9dd8b62ff7dec6f7a95b517e90d81fed4a0823c823469567a3d27775de3aa9d1274e5&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=1216CRn77ZSDjVbwvDTh56lR&amp;amp;sharer_sharetime=1639668502463&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px; color: rgb(80, 97, 109)">digitalocean.com/community/tools/nginx</span></a></p></li>
</ul>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702344911243</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fnginx.png&amp;size=m" type="image/jpeg" length="0"/><category>Nginx技术</category><pubDate>Tue, 12 Dec 2023 01:36:00 GMT</pubDate></item><item><title><![CDATA[Nginx 安全配置]]></title><link>https://xiaoming728.com/archives/1702344850716</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Nginx%20%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE&amp;url=/archives/1702344850716" width="1" height="1" alt="" style="opacity:0;">
<pre><code>log_format  web-archives-manager  '$remote_addr - $remote_user [$time_local] "$request" '
                                  '$status $body_bytes_sent "$http_referer" '
                                  '"$http_user_agent" "$http_x_forwarded_for"';

server {
    server_name dev.web-archives-manager.com;
    listen 8096 ssl;

    ssl_certificate   /home/keysecret/server.crt;
    ssl_certificate_key  /home/keysecret/server.key;

    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers on;

    ssl_protocols TLSv1.2 ;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 5m;

    resolver 8.8.4.4 8.8.8.8 valid=300s;
    resolver_timeout 10s;

    #ssl_dhparam /home/keysecret/server.pem;

    access_log /home/a8096/access_web-archives-manager-dev.log web-archives-manager;
    error_log /home/a8096/error_web-archives-manager-dev.log;

    root /home/a8096/web-archives-manager;

    location /stage-api/ {
      proxy_pass http://192.168.206.182:8090/;
      proxy_set_header Host $host;
      client_max_body_size 30m;
    }

    error_page 403 /403.html;
    add_header X-Content-Type-Options nosniff;
    add_header X-Permitted-Cross-Domain-Policies master-only;
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
    add_header X-Frame-Options 'SAMEORIGIN';
    add_header X-XSS-Protection "1; mode=block";
    add_header Content-Security-Policy  "default-src 'self'; script-src 'self' https://at.alicdn.com/ 'unsafe-inline'; font-src 'self' data:; img-src 'self'  data: 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'; frame-src 'self'; connect-src https:";
    # nginx中增加对Referrer的控制
    add_header  Referrer-Policy  "origin-when-crossorigin";
    add_header 'Referrer-Policy' 'origin';
    add_header 'Referrer-Policy' 'unsafe-url';
    add_header Cache-Control no-cache;
    add_header Cache-Control private;
}
</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702344850716</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fnginx.png&amp;size=m" type="image/jpeg" length="0"/><category>Nginx技术</category><pubDate>Tue, 12 Dec 2023 01:34:00 GMT</pubDate></item><item><title><![CDATA[OpenSSL创建自签名证书]]></title><link>https://xiaoming728.com/archives/1702344792609</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=OpenSSL%E5%88%9B%E5%BB%BA%E8%87%AA%E7%AD%BE%E5%90%8D%E8%AF%81%E4%B9%A6&amp;url=/archives/1702344792609" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><span style="font-size: 15px; color: rgb(73, 73, 73)">来源：</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://ryan4yin.space/"><span style="font-size: 15px">Ryan4Yin'sSpace</span></a></p>
 <p style=""><span style="font-size: 15px; color: rgb(73, 73, 73)">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://ryan4yin.space/"><span style="font-size: 15px">https://ryan4yin.space/posts/about-tls-cert/</span></a></p>
 <p style=""><span style="font-size: 15px; color: rgb(73, 73, 73)">日期：</span><span style="font-size: 15px; color: rgb(169, 169, 179)">2021-01-17</span></p>
</blockquote>
<h3 style="" id=""></h3>
<h3 style="" id="%E5%9F%BA%E4%BA%8E-rsa-%E7%AE%97%E6%B3%95%E7%9A%84-tls-%E8%AF%81%E4%B9%A6"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">基于 </span><span fontsize="" color="rgb(22, 18, 9)" style="color: rgb(22, 18, 9)">RSA </span><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">算法的 TLS 证书</span></h3>
<p style=""><span style="font-size: 15px">到目前为止 RSA 仍然是应用最广泛的非对称加密方案，几乎所有的根证书都是使用的 2048 位或者 4096 位的 RSA 密钥对。</span></p>
<p style=""><span style="font-size: 15px">对于 RSA 算法而言，越长的密钥能提供越高的安全性，当前使用最多的 RSA 密钥长度仍然是 2048 位，但是 2048 位已被一些人认为不够安全了，密码学家更建议使用 3072 位或者 4096 位的密钥。</span></p>
<p style=""><span style="font-size: 15px">生成一个 2048 位的 RSA 证书链的流程如下:</span></p>
<p style=""><span style="font-size: 15px">OpenSSL 的 CSR 配置文件官方文档: </span><a target="_blank" rel="noopener noreferrer nofollow" href="https://ryan4yin.space/"><span style="font-size: 15px">https://www.openssl.org/docs/manmaster/man1/openssl-req.html</span></a></p>
<ol>
 <li>
  <p style=""><span style="font-size: 15px">编写证书签名请求的配置文件 csr.conf</span></p></li>
</ol>
<pre><code>[ req ]
prompt = no
default_md = sha256  # 在签名算法中使用 SHA-256 计算哈希值
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C = CN  #国家
ST = ZJ 
L = hz
O = linewell
OU = archives
CN = 192.168.206.181 # 泛域名，这个字段已经被 chrome/apple 弃用了。

[ alt_names ] # 备用名称，chrome/apple 目前只信任这里面的域名。
IP = 192.168.206.181 # IP地址

[ req_ext ]
subjectAltName = @alt_names

[ v3_ext ]
subjectAltName=@alt_names  # Chrome 要求必须要有 subjectAltName(SAN)
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment,digitalSignature
extendedKeyUsage=serverAuth,clientAuth</code></pre>
<p style="">此文件的详细文档: <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.openssl.org/docs/man1.1.1/man5/">OpenSSL file formats and conventions</a></p>
<ol start="2">
 <li>
  <p style=""><span style="font-size: 15px">生成证书链与服务端证书:</span></p></li>
</ol>
<pre><code># 1. 生成本地 CA 根证书的私钥
openssl genrsa -out ca.key 2048
# 2. 使用私钥签发出 CA 根证书
openssl req -x509 -new -nodes -key ca.key -subj "/CN=MyLocalRootCA" -days 398 -out ca.crt
# 3. 生成服务端证书的 RSA 私钥（2048 位）
openssl genrsa -out server.key 2048
# 4. 通过第一步编写的配置文件，生成证书签名请求（公钥+申请者信息）
openssl req -new -key server.key -out server.csr -config csr.conf
# 5. 使用 CA 根证书直接签发服务端证书，这里指定服务端证书的有效期为 398 天
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out server.crt -days 398 \
  -extensions v3_ext -extfile csr.conf</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(22, 18, 9)">简单起见这里没有生成中间证书，直接使用根证书签发了用于安全通信的服务端证书。</span></p>
<h3 style="" id="%E5%9F%BA%E4%BA%8E-ecc-%E7%AE%97%E6%B3%95%E7%9A%84-tls-%E8%AF%81%E4%B9%A6"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">基于 ECC 算法的 TLS 证书</span></h3>
<p style=""><span style="font-size: 15px; color: rgb(73, 73, 73)">ECC(Elliptic Curve Cryptography) 算法被认为是比 RSA 更优秀的算法。与 RSA 算法相比，ECC 算法使用更小的密钥大小，但可提供同样的安全性，这使计算更快，降低了能耗，并节省了内存和带宽。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(73, 73, 73)">对于 RSA 密钥，可以提供不同的密钥大小（密钥大小越大，加密效果越好）。<br>
  而对于 ECC 密钥，您应选择要用哪种曲线生成密钥对。各个组织（ANSI X9.62、NIST、SECG）命名了多种曲线，可通过如下命名查看 openssl 支持的所有椭圆曲线名称：</span></p>
<pre><code>openssl ecparam -list_curves</code></pre>
<p style=""><span style="font-size: 15px">目前在 TLS 协议以及 JWT 签名算法中，目前应该最广泛的椭圆曲线仍然是 NIST 系列：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">P-256: 在 openssl 中对应的名称为 prime256v1；</span><span style="font-size: 15px; color: rgb(22, 18, 9)">到目前为止P-256是应用最为广泛的椭圆曲线</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">P-384：在 openssl 中对应的名称为 secp384r1</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">P-521：在 openssl 中对应的名称为 secp521r1</span></p></li>
</ul>
<p style=""></p>
<p style=""><span style="font-size: 15px">生成一个使用 P-384 曲线的 ECC 证书的示例如下:</span></p>
<ol>
 <li>
  <p style=""><span style="font-size: 15px">编写证书签名请求的配置文件 ecc-csr.conf:</span></p></li>
</ol>
<pre><code>[ req ]
prompt = no
default_md = sha256 # 在签名算法中使用 SHA-256 计算哈希值
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C = CN  #国家
ST = ZJ 
L = hz
O = linewell
OU = archives
CN = 192.168.206.181 # 泛域名，这个字段已经被 chrome/apple 弃用了。

[ alt_names ] # 备用名称，chrome/apple 目前只信任这里面的域名。
IP = 192.168.206.181 # IP地址

[ req_ext ]
subjectAltName = @alt_names

[ v3_ext ]
subjectAltName=@alt_names  # Chrome 要求必须要有 subjectAltName(SAN)
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment,digitalSignature
extendedKeyUsage=serverAuth,clientAuth</code></pre>
<p style=""><span style="font-size: 15px">此文件的详细文档: </span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.openssl.org/docs/man1.1.1/man5/"><span style="font-size: 15px">OpenSSL file formats and conventions</span></a></p>
<ol start="2">
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(22, 18, 9)">生成证书链与服务端证书</span></p></li>
</ol>
<pre><code># 1. 生成本地 CA 根证书的私钥，使用 P-384 曲线，密钥长度 384 位
openssl ecparam -genkey -name secp384r1 -out ecc-ca.key
# 2. 使用私钥签发出 CA 根证书
openssl req -x509 -new -nodes -key ecc-ca.key -subj "/CN=MyLocalRootCA" -days 398 -out ecc-ca.crt
# 3. 生成服务端证书的 EC 私钥，使用 P-384 曲线，密钥长度 384 位
openssl ecparam -genkey -name secp384r1 -out ecc-server.key
# 4. 通过第一步编写的配置文件，生成证书签名请求（公钥+申请者信息）
openssl req -new -key ecc-server.key -out ecc-server.csr -config ecc-csr.conf
# 5. 使用 CA 根证书直接签发 ECC 服务端证书，这里指定服务端证书的有效期为 398 天
openssl x509 -req -in ecc-server.csr -CA ecc-ca.crt -CAkey ecc-ca.key \
  -CAcreateserial -out ecc-server.crt -days 398 \
  -extensions v3_ext -extfile ecc-csr.conf</code></pre>
<h3 style="" id="%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E8%AF%A6%E6%83%85">查看证书详情</h3>
<pre><code># 查看KEY信息
cat ca.key
cat server.key
# 查看CSR信息
openssl req -noout -text -in server.csr
# 查看证书信息
openssl x509 -noout -text -in server.crt</code></pre>
<h3 style="" id="%E5%85%B3%E4%BA%8E%E8%AF%81%E4%B9%A6%E5%AF%BF%E5%91%BD"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">关于证书寿命</span></h3>
<p style=""><span style="font-size: 15px; color: rgb(73, 73, 73)">对于公开服务，服务端证书的有效期不要超过 825 天（27 个月）！而 2020 年 11 月起，新申请的服务端证书有效期缩短到了 398 天（13 个月）。目前 Apple/Mozilla/Chrome 都发表了相应声明，证书有效期超过上述限制的，将被浏览器/Apple设备禁止使用。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(73, 73, 73)">对于其他用途的证书，如果更换起来很麻烦，可以考虑放宽条件。<br>
  比如 kubernetes 集群的加密证书，可以考虑有效期设长一些，比如 10 年。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(73, 73, 73)">据</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://mp.weixin.qq.com/s?__biz=MzA4MTQ2MjI5OA==&amp;mid=2664079008&amp;idx=1&amp;sn=dede1114d5705880ea757f8d9ae4c92d"><span style="font-size: 15px">云原生安全破局｜如何管理周期越来越短的数字证书？</span></a><span style="font-size: 15px; color: rgb(73, 73, 73)">所述，大量知名企业如 特斯拉/微软/领英/爱立信 都曾因未及时更换 TLS 证书导致服务暂时不可用。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(73, 73, 73)">因此 TLS 证书最好是设置自动轮转！人工维护不可靠！</span></p>]]></description><guid isPermaLink="false">/archives/1702344792609</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fopenssl.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><category>Nginx技术</category><pubDate>Tue, 12 Dec 2023 01:33:00 GMT</pubDate></item><item><title><![CDATA[Flume安装]]></title><link>https://xiaoming728.com/archives/1702301297197</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Flume%E5%AE%89%E8%A3%85&amp;url=/archives/1702301297197" width="1" height="1" alt="" style="opacity:0;">
<p style="">集群规划</p>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">编号</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">ip</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">hostname</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">进程</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">00</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">192.168.206.180</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">server-00</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">Master</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">04</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">192.168.206.184</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">server-04</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">Worker</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">05</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">192.168.206.185</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">server-05</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">Worker</p></td>
  </tr>
 </tbody>
</table>
<p style=""><span fontsize="" color="white" style="color: white">软件准备</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">ssh-keygen&nbsp;-t&nbsp;rsa&nbsp;-C "</span><a target="_blank" rel="noopener noreferrer nofollow" href="mailto:sxiaochuan@linewell.com"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">sxiaochuan@linewell.com</span></a><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">"</span></p>
<p style="">&nbsp;</p>
<p style="">&nbsp;</p>
<p style=""><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">export JAVA_HOME=</span><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">/usr/local/src/hoox/jdk1.8.0_91</span></p>
<p style="">&nbsp;</p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;定义这个&nbsp;agent&nbsp;中各组件的名字</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sources = r1</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks = k1</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.channels = c1</span></p>
<p style="">&nbsp;</p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;描述和配置&nbsp;source&nbsp;组件：r1</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sources.r1.type = avro</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sources.r1.bind = 0.0.0.0</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sources.r1.port = 44444</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;描述和配置&nbsp;channel&nbsp;组件，此处使用是内存缓存的方式</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.channels.c1.type = memory</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;默认该通道中最大的可以存储的&nbsp;event&nbsp;数量</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.channels.c1.capacity = 10000</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;每次最大可以从&nbsp;source&nbsp;中拿到或者送到&nbsp;sink&nbsp;中的&nbsp;event&nbsp;数量</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.channels.c1.transactionCapacity = 1000</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;描述和配置&nbsp;sink&nbsp;组件：k1</span></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="http://a1.sinks.k1.channel"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.channel</span></a><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)"> = c1</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.type = hdfs</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.hdfs.path = hdfs://server-00:9000/syslog/%Y%m%d</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.hdfs.filePrefix = archive-sys-%H</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.</span>hdfs.fileSuffix = .log</p>
<p style="">a1.sinks.k1.hdfs.round = false</p>
<p style="">#a1.sinks.k1.hdfs.roundValue = 1</p>
<p style="">#a1.sinks.k1.hdfs.roundUnit = hour</p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;默认值：30; hdfs sink&nbsp;间隔多长将临时文件滚动成最终目标文件，单位：秒;&nbsp;如果设置成&nbsp;0，则表示不根据时间来滚动文件</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.hdfs.rollInterval = 0</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;默认值：1024;&nbsp;当临时文件达到该大小（单位：bytes）时，滚动成目标文件;&nbsp;如果设置成&nbsp;0，则表示不根据临时文件大小来滚动文件</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.hdfs.rollSize =&nbsp;</span><span fontsize="" color="rgb(85, 85, 85)" style="color: rgb(85, 85, 85)">0</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;默认值：10;&nbsp;当&nbsp;events&nbsp;数据达到该数量时候，将临时文件滚动成目标文件;&nbsp;如果设置成&nbsp;0，则表示不根据&nbsp;events&nbsp;数据来滚动文件</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.hdfs.rollCount = 0</span></p>
<p style="">#闲置文件关闭后超时（0=禁用自动关闭闲置文件）</p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.</span><span style="font-size: 16px">hdfs.idleTimeout=60</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.hdfs.batchSize = 1000</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.hdfs.writeFormat = text</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;生成的文件类型，默认是&nbsp;Sequencefile，可用&nbsp;DataStream，则为普通文本</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.hdfs.fileType = DataStream</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.hdfs.</span>minBlockReplicas&nbsp;<span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">= 1</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;操作&nbsp;hdfs&nbsp;超时时间</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.callTimeout =10000</span></p>
<p style="">a1.sinks.k1.hdfs.useLocalTimeStamp = true</p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">#&nbsp;描述和配置&nbsp;source channel sink&nbsp;之间的连接关系</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sources.r1.channels = c1</span></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="http://a1.sinks.k1.channel"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">a1.sinks.k1.channel</span></a><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)"> = c1</span></p>
<p style="">&nbsp;</p>
<p style="">nohup flume-ng agent --conf /data01/flume-1.9.0/conf --conf-file /data01/flume-1.9.0/conf/flume.conf --name a1 -Dflume.root.logger=INFO,console &amp;</p>
<p style="">&nbsp;</p>
<p style="">&nbsp;</p>]]></description><guid isPermaLink="false">/archives/1702301297197</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FFlume.png&amp;size=m" type="image/jpeg" length="0"/><category>Hadoop技术</category><pubDate>Mon, 11 Dec 2023 13:28:25 GMT</pubDate></item><item><title><![CDATA[阿里云OSS对象存储对接]]></title><link>https://xiaoming728.com/archives/1702301280220</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E9%98%BF%E9%87%8C%E4%BA%91OSS%E5%AF%B9%E8%B1%A1%E5%AD%98%E5%82%A8%E5%AF%B9%E6%8E%A5&amp;url=/archives/1702301280220" width="1" height="1" alt="" style="opacity:0;">
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.alibabacloud.com/help/zh/doc-detail/31817.htm?spm=a2c63.p38356.b99.4.7635a752xlRKu5">oss学习地址</a></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">可跳过前半部分，直接查看工具类使用方法。</span></p>
<h2 style="" id="oss%E4%B8%8E%E8%87%AA%E5%BB%BA%E5%AD%98%E5%82%A8%E5%AF%B9%E6%AF%94%E7%9A%84%E4%BC%98%E5%8A%BF">OSS与自建存储对比的优势</h2>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">对比项</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">对象存储OSS</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">自建服务器存储</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">可靠性</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">OSS作为阿里巴巴全集团数据存储的核心基础设施，多年支撑双11业务高峰，历经高可用与高可靠的严苛考验。OSS的多重冗余架构设计，为数据持久存储提供可靠保障。同时，OSS基于高可用架构设计，消除单节故障，确保数据业务的持续性。</p>
    <ul>
     <li>
      <p style="">服务设计可用性不低于99.995%。</p></li>
     <li>
      <p style="">数据设计持久性不低于99.9999999999%（12个9）。</p></li>
     <li>
      <p style="">规模自动扩展，不影响对外服务。</p></li>
     <li>
      <p style="">数据自动多重冗余备份。</p></li>
    </ul></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <ul>
     <li>
      <p style="">受限于硬件可靠性，易出问题，一旦出现磁盘坏道，容易出现不可逆转的数据丢失。</p></li>
     <li>
      <p style="">人工数据恢复困难、耗时、耗力。</p></li>
    </ul></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">安全</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <ul>
     <li>
      <p style="">提供企业级多层次安全防护，包括服务端加密、客户端加密、防盗链、IP黑白名单、细粒度权限管控、日志审计、WORM特性等。</p></li>
     <li>
      <p style="">多用户资源隔离机制，支持异地容灾机制。</p></li>
     <li>
      <p style="">获得多项合规认证，包括SEC、FINRA等，满足企业数据安全与合规要求。</p></li>
    </ul></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <ul>
     <li>
      <p style="">需要另外购买清洗和黑洞设备。</p></li>
     <li>
      <p style="">需要单独实现安全机制。</p></li>
    </ul></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">成本</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <ul>
     <li>
      <p style="">多线BGP骨干网络，无带宽限制，上行流量免费。</p></li>
     <li>
      <p style="">无需运维人员与托管费用，0成本运维。</p></li>
    </ul></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <ul>
     <li>
      <p style="">存储受硬盘容量限制，需人工扩容。</p></li>
     <li>
      <p style="">单线或双线接入速度慢，有带宽限制，峰值时期需人工扩容。</p></li>
     <li>
      <p style="">需专人运维，成本高。</p></li>
    </ul></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">智能存储</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">提供多种数据处理能力，如图片处理、视频截帧、文档预览、图片场景识别、人脸识别、SQL就地查询等，并无缝对接Hadoop生态、以及阿里云函数计算、EMR、DataLakeAnalytics、BatchCompute、MaxCompute、DBS等产品，满足企业数据分析与管理的需求。</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">需要额外采购，单独部署。</p></td>
  </tr>
 </tbody>
</table>
<p style=""></p>
<h4 style="" id="%E5%BC%BA%E4%B8%80%E8%87%B4%E6%80%A7">强一致性</h4>
<p style="">Object 操作在 OSS 上具有原子性，操作要么成功要么失败，不会存在有中间状态的 Object。OSS 保证用户一旦上传完成之后读到的 Object 是完整的，OSS 不会返回给用户一个部分上传成功的 Object。</p>
<p style="">Object 操作在 OSS 上同样具有强一致性，用户一旦收到了一个上传（PUT）成功的响应，该上传的 Object 就已经立即可读，并且 Object 的冗余数据已经写成功。不存在一种上传的中间状态，即 read-after-write 却无法读取到数据。对于删除操作也是一样的，用户删除指定的 Object 成功之后，该 Object 立即变为不存在。</p>
<h2 style="" id="oss%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5">OSS基本概念</h2>
<h4 style="" id="%E5%AD%98%E5%82%A8%E7%A9%BA%E9%97%B4%EF%BC%88bucket%EF%BC%89">存储空间（Bucket）</h4>
<p style="">存储空间是用户用于存储对象（Object）的容器，所有的对象都必须隶属于某个存储空间。存储空间具有各种配置属性，包括地域、访问权限、存储类型等。用户可以根据实际需求，创建不同类型的存储空间来存储不同的数据。</p>
<ul>
 <li>
  <p style="">同一个存储空间的内部是扁平的，<span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">没有文件系统的目录等概念</span>，所有的对象都直接隶属于其对应的存储空间。</p></li>
 <li>
  <p style="">每个用户可以拥有多个存储空间。</p></li>
 <li>
  <p style="">存储空间的名称在 OSS 范围内必须是全局唯一的，一旦创建之后无法修改名称。</p></li>
 <li>
  <p style="">存储空间内部的对象数目没有限制。</p></li>
</ul>
<p style="">存储空间的命名规范如下：</p>
<ul>
 <li>
  <p style="">只能包括小写字母、数字和短横线（-）。</p></li>
 <li>
  <p style="">必须以小写字母或者数字开头和结尾。</p></li>
 <li>
  <p style="">长度必须在 3–63 字节之间。</p></li>
</ul>
<h4 style="" id="%E5%AF%B9%E8%B1%A1%2F%E6%96%87%E4%BB%B6%EF%BC%88object%EF%BC%89">对象/文件（Object）</h4>
<p style="">对象是 OSS 存储数据的基本单元，也被称为 OSS 的文件。对象由元信息（Object Meta），用户数据（Data）和文件名（Key）组成。对象由存储空间内部唯一的 Key 来标识。对象元信息是一组键值对，表示了对象的一些属性，比如最后修改时间、大小等信息，同时用户也可以在元信息中存储一些自定义的信息。</p>
<p style="">对象的生命周期是从上传成功到被删除为止。在整个生命周期内，只有通过追加上传的 Object 可以继续通过追加上传写入数据，其他上传方式上传的 Object 内容无法编辑，您可以通过重复上传同名的对象来覆盖之前的对象。</p>
<p style="">对象的命名规范如下：</p>
<ul>
 <li>
  <p style="">使用 UTF-8 编码。</p></li>
 <li>
  <p style="">长度必须在 1–1023 字节之间。</p></li>
 <li>
  <p style="">不能以正斜线（/）或者反斜线（\）开头。</p></li>
</ul>
<h4 style="" id="endpoint%EF%BC%88%E8%AE%BF%E9%97%AE%E5%9F%9F%E5%90%8D%EF%BC%89">Endpoint（访问域名）</h4>
<p style="">Endpoint 表示 OSS 对外服务的访问域名。OSS 以 HTTP RESTful API 的形式对外提供服务，当访问不同的 Region 的时候，需要不同的域名。通过内网和外网访问同一个 Region 所需要的 Endpoint 也是不同的。例如杭州 Region 的外网 Endpoint 是 <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.alibabacloud.com/help/zh/doc-detail/31817.htm?spm=a2c63.p38356.b99.4.7635a752xlRKu5">oss-cn-hangzhou.aliyuncs.com</a>，内网 Endpoint 是 <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.alibabacloud.com/help/zh/doc-detail/31817.htm?spm=a2c63.p38356.b99.4.7635a752xlRKu5">oss-cn-hangzhou-internal.aliyuncs.com</a>。具体的内容请参见<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.alibabacloud.com/help/zh/doc-detail/31837.htm#concept-zt4-cvy-5db">各个 Region 对应的 Endpoint</a>。</p>
<h4 style="" id="accesskey%EF%BC%88%E8%AE%BF%E9%97%AE%E5%AF%86%E9%92%A5%EF%BC%89">AccessKey（访问密钥）</h4>
<p style="">AccessKey（简称 AK）指的是访问身份验证中用到的 AccessKeyId 和 AccessKeySecret。OSS 通过使用 AccessKeyId 和 AccessKeySecret 对称加密的方法来验证某个请求的发送者身份。AccessKeyId 用于标识用户；AccessKeySecret 是用户用于加密签名字符串和 OSS 用来验证签名字符串的密钥，必须保密。对于 OSS 来说，AccessKey 的来源有：</p>
<ul>
 <li>
  <p style="">Bucket 的拥有者申请的 AccessKey。</p></li>
 <li>
  <p style="">被 Bucket 的拥有者通过 RAM 授权给第三方请求者的 AccessKey。</p></li>
 <li>
  <p style="">被 Bucket 的拥有者通过 STS 授权给第三方请求者的 AccessKey。</p></li>
</ul>
<p style=""></p>
<h2 style="" id="oss%E5%B7%A5%E5%85%B7%E7%B1%BB%E4%BD%BF%E7%94%A8">OSS工具类使用</h2>
<h4 style="" id="oss%E9%85%8D%E7%BD%AE%EF%BC%9A">oss配置：</h4>
<p style=""><span fontsize="" color="rgb(128, 128, 128)" style="color: rgb(128, 128, 128)">#aliyun oss配置信息</span></p>
<p style=""><span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">aliyun.oss.endpoint</span><span fontsize="" color="rgb(128, 128, 128)" style="color: rgb(128, 128, 128)">=</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.alibabacloud.com/help/zh/doc-detail/31817.htm?spm=a2c63.p38356.b99.4.7635a752xlRKu5"><span fontsize="" color="rgb(106, 135, 89)" style="color: rgb(106, 135, 89)">http://oss-cn-hangzhou.aliyuncs.com</span></a></p>
<p style=""><span fontsize="" color="rgb(128, 128, 128)" style="color: rgb(128, 128, 128)">#AccessKey</span></p>
<p style=""><span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">aliyun.oss.keyid</span><span fontsize="" color="rgb(128, 128, 128)" style="color: rgb(128, 128, 128)">=</span><span fontsize="" color="rgb(106, 135, 89)" style="color: rgb(106, 135, 89)">LTAI4G7MfAz3Uq7Cb4E8dYkW</span></p>
<p style=""><span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">aliyun.oss.keysecret</span><span fontsize="" color="rgb(128, 128, 128)" style="color: rgb(128, 128, 128)">=</span><span fontsize="" color="rgb(106, 135, 89)" style="color: rgb(106, 135, 89)">48EAhGhBMMumK6cEPuCjxJiEYqRpUE</span></p>
<p style=""><span fontsize="" color="rgb(128, 128, 128)" style="color: rgb(128, 128, 128)">#存储空间--bucket</span></p>
<p style=""><span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">aliyun.oss.bucketname</span><span fontsize="" color="rgb(128, 128, 128)" style="color: rgb(128, 128, 128)">=</span><span fontsize="" color="rgb(106, 135, 89)" style="color: rgb(106, 135, 89)">archives-gd</span></p>
<p style=""><span fontsize="" color="rgb(128, 128, 128)" style="color: rgb(128, 128, 128)">#当前环境</span></p>
<p style=""><span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">aliyun.oss.filehost</span><span fontsize="" color="rgb(128, 128, 128)" style="color: rgb(128, 128, 128)">=</span><span fontsize="" color="rgb(106, 135, 89)" style="color: rgb(106, 135, 89)">test</span></p>
<p style=""></p>
<h4 style="" id="oss%E5%B7%A5%E5%85%B7%E7%B1%BB%E4%BD%BF%E7%94%A8%EF%BC%9A">oss工具类使用：</h4>
<p style=""><strong><span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">注入：</span></strong></p>
<p style=""><span fontsize="" color="rgb(187, 181, 41)" style="color: rgb(187, 181, 41)">@Autowired</span></p>
<p style="">OssUtil <span fontsize="" color="rgb(152, 118, 170)" style="color: rgb(152, 118, 170)">ossUtil</span><span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">;</span></p>
<p style=""></p>
<p style=""><strong><span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">代码示例：</span></strong></p>
<p style=""></p>
<p style=""></p>
<pre><code>@RestController
public class OssController {

    @Autowired
    OssUtil ossUtil;


    @GetMapping("/oss")
    public void test(){
        File file = new File("F:\\testFtp\\localFile.txt");
        //上传文件。 --也可采用流式上传，使用ossUtil中流式上传方法即可
        String path = ossUtil.uploadFile("gd.txt","/gd",file);
        System.out.println(path);
        //删除文件
        ossUtil.deleteFile("http://archives-gd.oss-cn-hangzhou.aliyuncs.com/test/gd/gd.txt");
        //列举当前bucket下所有文件
        List&lt;String&gt; list = ossUtil.listObjects();
        System.out.println(list.size());
        
        //上传文件--断点续传
        String parh = ossUtil.uploadObjectOSS("F:\\testFtp\\4.zip","s/4.zip",null);
        System.out.println(parh);

        //下载文件--断点续传
        ossUtil.downFileFromOSS("F:\\testFtp\\1\\4.zip","http://archives-gd.oss-cn-hangzhou.aliyuncs.com/s/4.zip");
        
         //上传文件夹
        //保存所有文件返回地址
        List&lt;String&gt; path = new ArrayList&lt;&gt;();
        ossUtil.uploadDir("33010","D:\\t",path);
        System.out.println(path);
    }

}
</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702301280220</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Foss.png&amp;size=m" type="image/jpeg" length="0"/><category>OSS技术</category><pubDate>Mon, 11 Dec 2023 13:28:00 GMT</pubDate></item><item><title><![CDATA[Hadoop HA 集群安装]]></title><link>https://xiaoming728.com/archives/1702301316967</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Hadoop%20HA%20%E9%9B%86%E7%BE%A4%E5%AE%89%E8%A3%85&amp;url=/archives/1702301316967" width="1" height="1" alt="" style="opacity:0;">
<p style=""><strong>集群规划</strong></p>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">编号</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">ip</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">hostname</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">进程</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">00</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">192.168.206.180</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">server-00</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">nn1（NameNode）</p>
    <p style="">zkfc（DFSZKFailoverController）</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">01</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">192.168.206.181</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">server-01</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">dn（datanode）</p>
    <p style="">jn（journalnode）</p>
    <p style="">zk（QuorumPeerMain）</p>
    <p style="">NodeManager</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">02</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">192.168.206.182</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">server-02</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">dn（datanode）</p>
    <p style="">jn（journalnode）</p>
    <p style="">zk（QuorumPeerMain）</p>
    <p style="">NodeManager</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">03</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">192.168.206.183</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">server-03</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">dn（datanode）</p>
    <p style="">jn（journalnode）</p>
    <p style="">zk（QuorumPeerMain）</p>
    <p style="">NodeManager</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">04</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">192.168.206.184</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">server-04</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">ResourceManager</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">05</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">192.168.206.185</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="text-align: center; ">server-05</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">nn2（NameNode）</p>
    <p style="">zkfc（DFSZKFailoverController）</p>
    <p style="">ResourceManager</p></td>
  </tr>
 </tbody>
</table>
<p style="">&nbsp;</p>
<p style="">hadoop各个组件介绍</p>
<p style="">&nbsp;</p>
<p style=""><strong>*修改hosts及hostname</strong></p>
<p style="">127.0.0.1 locahost</p>
<p style="">192.168.206.180 server-00</p>
<p style="">192.168.206.181 server-01</p>
<p style="">192.168.206.182 server-02</p>
<p style="">192.168.206.183 server-03</p>
<p style="">192.168.206.184 server-04</p>
<p style="">192.168.206.185 server-05</p>
<p style=""><strong>*ssh设置免密登录</strong></p>
<p style="">我们这里选择的是00机器和05机器作为NameNode，需要配置这两台机器免密登录到其他机器</p>
<p style="">生成秘钥对</p>
<p style=""><span style="font-size: 12px">ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa</span></p>
<p style="">spc将00机器公钥拷贝到其他机器</p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-00:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">00.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-01:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">00.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-02:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">00.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-03:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">00.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-04:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">00.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-05:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">00.pub</a></p>
<p style="">spc将05机器公钥拷贝到其他机器</p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-00:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">05.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-01:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">05.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-02:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">05.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-03:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">05.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-04:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">05.pub</a></p>
<p style="">scp id_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">rsa.pub</a> root@server-05:~/.ssh/id_rsa_<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">05.pub</a></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">在每台机器上可以使用cat将秘钥追加到authorized_keys文件</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">cat id_rsa_</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">00.pub</span></a><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)"> &gt;&gt; authorized_keys</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">cat id_rsa_</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">04.pub</span></a><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)"> &gt;&gt; authorized_keys</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">cat id_rsa_</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">05.pub</span></a><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)"> &gt;&gt; authorized_keys</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">此时authorized_keys文件权限需要改为644(经常因为权限问题导致ssh无密登录失败)</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">chmod 644 authorized_keys</span></p>
<p style="">*关闭防火墙</p>
<p style="">*关闭<strong>selinux</strong></p>
<p style=""><strong>安装zookeeper</strong></p>
<p style="">我们这里选择01机器、02机器和03机器</p>
<p style="">tar -xzvf apache-zookeeper-3.6.0-bin.tar.gz</p>
<p style="">mv apache-zookeeper-3.6.0-bin zookeeper-3.6.0</p>
<p style="">在/etc/profile中添加zk环境变量，并重新编译/etc/profile文件</p>
<p style="">vim /etc/profile</p>
<p style="">exprot ZK_HOME=/home/zookeeper-3.6.0</p>
<p style="">source /etc/profile</p>
<p style="">复制conf/zoo_simple.cfg&nbsp;为同目录下&nbsp;zoo.cfg，三台机器配置文件统一</p>
<p style="">cp zoo_simple.cfg zoo.cfg</p>
<p style="">vi zoo.cfg</p>
<p style="">修改datadir配置</p>
<p style="">dataDir=/home/tmp/zookeeper</p>
<p style="">增加集群配置</p>
<p style="">server.1=server-01:2888:3888</p>
<p style="">server.2=server-02:2888:3888</p>
<p style="">server.3=server-03:2888:3888</p>
<p style="">分别在三台机器的/home/hdfs/zookeeper目录下创建myid文件，内容分别为1/2/3</p>
<p style=""><strong>配置NameService</strong></p>
<p style="">tar -xzvf hadoop-2.10.0.tar.gz</p>
<p style=""><strong>[</strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong>hadoo-env.sh</strong></a><strong>]</strong></p>
<p style="">export JAVA_HOME=/usr/local/src/hoox/jdk1.8.0_91</p>
<p style=""><strong>[core-site.xml]</strong></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;configuration&gt;</span></p>
<p style="">&lt;!--&nbsp;指定hdfs的nameservice为archivescenter --&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;fs.defaultFS&lt;/name&gt;</p>
<p style="">&lt;value&gt;hdfs://archivescenter/&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;指定hadoop临时目录&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;hadoop.tmp.dir&lt;/name&gt;</p>
<p style="">&lt;value&gt;/data01/tmp/hadoop&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;指定zookeeper地址&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;ha.zookeeper.quorum&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-01:2181,server-02:2181,server-03:2181&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;/configuration&gt;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">在hadoop的配置文件core-site.xml中，需要设置&nbsp;</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">fs.default.name</span></a><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;或&nbsp;fs.defaultFS&nbsp;，具体应该使用哪一个?&nbsp;要首先判断是否开启了NameNode的HA (namenode&nbsp;的&nbsp;highavaliable)，如果开启了nn ha，那么就用fs.defaultFS，在单一namenode的情况下，就用&nbsp;</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">fs.default.name</span></a><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)"> ,&nbsp;如果在单一namenode节点的情况使用&nbsp;fs.defaultFS&nbsp;，系统将报</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">ERROR org.apache.hadoop.hdfs.server.namenode.NameNode: Failed to start namenode.</span></p>
<p style=""><strong>[hdfs-site.xml]</strong></p>
<p style=""><strong>参数说明请参考：</strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><span fontsize="" color="rgb(0, 56, 132)" style="color: rgb(0, 56, 132)">https://blog.csdn.net/w13770269691/article/details/24457241</span></a></p>
<p style="">&lt;configuration&gt;</p>
<p style="">&lt;!--指定hdfs的nameservice为archivescenter，需要和core-site.xml中的保持一致&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;dfs.nameservices&lt;/name&gt;</p>
<p style="">&lt;value&gt;archivescenter&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!-- archivescenter下面有两个NameNode，分别是center00，center05--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;dfs.ha.namenodes.archivescenter&lt;/name&gt;</p>
<p style="">&lt;value&gt;center00,center05&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!-- center00的RPC通信地址&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">dfs.namenode.rpc-address.archivescenter.center</a>00&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-00:9000&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!-- center00的http通信地址&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">dfs.namenode.http-address.archivescenter.center</a>00&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-00:50070&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!-- center05的RPC通信地址&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">dfs.namenode.rpc-address.archivescenter.center</a>05&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-05:9000&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!-- center05的http通信地址&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241">dfs.namenode.http-address.archivescenter.center</a>05&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-05:50070&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;指定NameNode的元数据在JournalNode上的存放位置&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;dfs.namenode.shared.edits.dir&lt;/name&gt;</p>
<p style="">&lt;value&gt;qjournal://server-01:8485;server-02:8485;server-03:8485/archivescenter&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;指定JournalNode在本地磁盘存放数据的位置&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;dfs.journalnode.edits.dir&lt;/name&gt;</p>
<p style="">&lt;value&gt;/data01/tmp/hadoop/journaldata&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;开启NameNode失败自动切换&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;dfs.ha.automatic-failover.enabled&lt;/name&gt;</p>
<p style="">&lt;value&gt;true&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;配置失败自动切换实现方式&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;dfs.client.failover.proxy.provider.archivescenter&lt;/name&gt;</p>
<p style="">&lt;value&gt;org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;配置隔离机制方法，多个机制用换行分割，即每个机制暂用一行--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;dfs.ha.fencing.methods&lt;/name&gt;</p>
<p style="">&lt;value&gt;</p>
<p style="">sshfence</p>
<p style="">shell(/bin/true)</p>
<p style="">&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;使用sshfence隔离机制时需要ssh免登陆&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;dfs.ha.fencing.ssh.private-key-files&lt;/name&gt;</p>
<p style="">&lt;value&gt;~/.ssh/id_rsa&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;配置sshfence隔离机制超时时间&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;dfs.ha.fencing.ssh.connect-timeout&lt;/name&gt;</p>
<p style="">&lt;value&gt;30000&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;/configuration&gt;</p>
<p style=""><strong>[mapred-site.xml]</strong></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;configuration&gt;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;!--&nbsp;指定mr框架为yarn方式&nbsp;--&gt;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;property&gt;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;name&gt;</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">mapreduce.framework.name</span></a><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;/name&gt;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;value&gt;yarn&lt;/value&gt;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;/property&gt;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&lt;/configuration&gt;</span></p>
<p style=""><strong>[yarn-site.xml]</strong></p>
<p style="">hadoop1.x版本JobTracker的作用是资源管理和任务的调度，当存在多个计算框架时，比如说spark，如果两个计算框架都有着自己的资源管理模块，就会存在资源竞争，不便于管理。此时就需要一个公共的资源管理模块，这就产生了YARN.</p>
<p style=""><strong><span fontsize="" color="rgb(57, 57, 57)" style="color: rgb(57, 57, 57)">Yarn相关知识参考：</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong><span fontsize="" color="rgb(0, 56, 132)" style="color: rgb(0, 56, 132)">https://www.jianshu.com/p/f50e85bdb9ce</span></strong></a></p>
<p style=""><strong><span fontsize="" color="rgb(57, 57, 57)" style="color: rgb(57, 57, 57)">&nbsp;&nbsp;&nbsp;&nbsp;</span></strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong><span fontsize="" color="rgb(0, 56, 132)" style="color: rgb(0, 56, 132)">https://blog.csdn.net/amandalm/article/details/81630702</span></strong></a></p>
<p style="">&lt;configuration&gt;</p>
<p style="">&lt;!--&nbsp;开启RM高可用&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;yarn.resourcemanager.ha.enabled&lt;/name&gt;</p>
<p style="">&lt;value&gt;true&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;指定RM的cluster id --&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;yarn.resourcemanager.cluster-id&lt;/name&gt;</p>
<p style="">&lt;value&gt;<span style="font-size: 13px">yarn_cluster</span>&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;指定RM的名字&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;yarn.resourcemanager.ha.rm-ids&lt;/name&gt;</p>
<p style="">&lt;value&gt;server04,server05&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;分别指定RM的地址&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;yarn.resourcemanager.hostname.server04&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-04&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;yarn.resourcemanager.webapp.address.server04&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-04:8088&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;yarn.resourcemanager.hostname.server05&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-05&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;yarn.resourcemanager.webapp.address.server05&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-05:8088&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;!--&nbsp;指定zk集群地址&nbsp;--&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;yarn.resourcemanager.zk-address&lt;/name&gt;</p>
<p style="">&lt;value&gt;server-01:2181,server-02:2181,server-03:2181&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;property&gt;</p>
<p style="">&lt;name&gt;yarn.nodemanager.aux-services&lt;/name&gt;</p>
<p style="">&lt;value&gt;mapreduce_shuffle&lt;/value&gt;</p>
<p style="">&lt;/property&gt;</p>
<p style="">&lt;/configuration&gt;</p>
<p style=""><strong>修改slaves</strong></p>
<p style="">slaves是指定子节点的位置，因为要在server00上启动HDFS、在server04启动yarn，所以server00上的slaves文件指定的是datanode的位置，server04上的slaves文件指定的是nodemanager的位置</p>
<p style="">分别修改&nbsp;server00和&nbsp;server04上的配置文件增加子节点</p>
<p style=""><strong>hadoop HA&nbsp;集群启动顺序</strong></p>
<p style="">*&nbsp;启动&nbsp;zookeeper&nbsp;在server-01，server-02，server-03执行</p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong>zkServer.sh</strong></a><strong> start</strong></p>
<p style="">*&nbsp;启动&nbsp;journalnode&nbsp;在server-01，server-02，server-03执行</p>
<p style=""><strong>sbin/</strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong>hadoop-daemon.sh</strong></a><strong> start journalnode</strong></p>
<p style="">*&nbsp;格式化HDFS&nbsp;&nbsp;在active NameNode&nbsp;（这里是server-00）上执行</p>
<p style=""><strong><span style="font-size: 16px; color: rgb(77, 77, 77)">hdfs namenode -format</span></strong></p>
<p style=""><span fontsize="" color="rgb(77, 77, 77)" style="color: rgb(77, 77, 77)">格式化后会在core-site.xml中配置的hadoop.tmp.dir目录生成配置文件，这里配置的目录是</span>/data01/tmp/hadoop</p>
<p style="">*&nbsp;拷贝配置信息到&nbsp;standby NameNode&nbsp;（这里是server-05）</p>
<p style=""><strong>scp -r /</strong>data01<strong>/tmp/hadoop root@server-05:/</strong>data01<strong>/tmp/hadoop</strong></p>
<p style=""><strong>*&nbsp;</strong>启动&nbsp;active NameNode&nbsp;（这里是server-00）</p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong>hadoop-daemon.sh</strong></a><strong> start namenode</strong></p>
<p style="">*&nbsp;在&nbsp;standby NameNode&nbsp;执行 （这里是server-05）</p>
<p style=""><strong>hdfs namenode -bootstrapStandby</strong></p>
<p style="">当收到提示：Re-format filesystem in Storage Directory /home/tmp/hadoop/dfs/name ? (Y or N)&nbsp;选择N</p>
<p style="">*&nbsp;在&nbsp;active NameNode&nbsp;执行 （这里是server-00）</p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong>hadoop-daemon.sh</strong></a><strong> stop namenode</strong></p>
<p style=""><strong>*&nbsp;</strong>在&nbsp;standby NameNode&nbsp;执行 （这里是server-05）</p>
<p style=""><strong>hdfs namenode -initializeSharedEdits</strong></p>
<p style="">当收到提示：Re-format filesystem in QJM to [192.168.206.181:8485, 192.168.206.182:8485, 192.168.206.183:8485] ? (Y or N)&nbsp;选择N</p>
<p style="">*&nbsp;关闭<span style="font-size: 16px; color: rgb(77, 77, 77)">journalnode&nbsp;</span>在三台<span style="font-size: 16px; color: rgb(77, 77, 77)">journalnode上执行</span></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">hadoop-daemon.sh</span></strong></a><strong><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)"> stop&nbsp;</span>journalnode</strong></p>
<p style="">*&nbsp;格式化ZKFC&nbsp;在server-00上执行即可，仅第一次启动执行</p>
<p style=""><strong>hdfs zkfc -formatZK</strong></p>
<p style="">*&nbsp;启动HDFS&nbsp;在server-00上执行</p>
<p style=""><strong>sbin/</strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong>start-dfs.sh</strong></a></p>
<p style="">*&nbsp;启动YARN&nbsp;在server-05上执行</p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong>start-yarn.sh</strong></a></p>
<p style="">把namenode和resourcemanager分开是因为性能问题，因为他们都要占用大量资源，所以把他们分开了，他们分开了就要分别在不同的机器上启动</p>
<p style="">*&nbsp;手动启动server-05上面的resourcemanager</p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/w13770269691/article/details/24457241"><strong>yarn-daemon.sh</strong></a><strong> start resourcemanager</strong></p>
<p style="">至此&nbsp;hadoop HA&nbsp;集群搭建成功</p>
<p style="">&nbsp;</p>]]></description><guid isPermaLink="false">/archives/1702301316967</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fhadoop-logo.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Hadoop技术</category><pubDate>Mon, 11 Dec 2023 13:28:00 GMT</pubDate></item><item><title><![CDATA[CentOS7下安装Perl编程环境]]></title><link>https://xiaoming728.com/archives/1702301245570</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=CentOS7%E4%B8%8B%E5%AE%89%E8%A3%85Perl%E7%BC%96%E7%A8%8B%E7%8E%AF%E5%A2%83&amp;url=/archives/1702301245570" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E5%AE%89%E8%A3%85perl"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">安装Perl</span></h3>
<p style=""><strong><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">安装方式一：</span></strong><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">如果环境能够联网的话，一个命令基本上就搞定了。</span></p>
<pre><code># yum install perl*</code></pre>
<p style=""><strong><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">安装方式二：</span></strong><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">如果不能联网，可以下载最新的源码包进行编译安装。</span></p>
<pre><code># wget http://www.cpan.org/src/5.0/perl-5.24.1.tar.gz
# tar -xzf perl-5.24.1.tar.gz
# cd perl-5.24.1
# ./Configure -des -Dprefix=$HOME/localperl
# make
# make test
# make install</code></pre>
<h3 style="" id="%E6%9F%A5%E7%9C%8B%E5%AE%89%E8%A3%85%E6%88%90%E5%8A%9F%E7%9A%84perl%E7%89%88%E6%9C%AC%EF%BC%9A"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">查看安装成功的Perl版本：</span></h3>
<pre><code>[root@controller ~]# perl -v

This is perl 5, version 16, subversion 3 (v5.16.3) built for x86_64-linux-thread-multi
(with 34 registered patches, see perl -V for more detail)

Copyright 1987-2012, Larry Wall

Perl may be copied only under the terms of either the Artistic License or the
GNU General Public License, which may be found in the Perl 5 source kit.

Complete documentation for Perl, including FAQ lists, should be found on
this system using "man perl" or "perldoc perl". If you have access to the
Internet, point your browser at http://www.perl.org/, the Perl Home Page.</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702301245570</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fcentos-2hda.png&amp;size=m" type="image/jpeg" length="0"/><category>Apache Druid技术</category><pubDate>Mon, 11 Dec 2023 13:27:30 GMT</pubDate></item><item><title><![CDATA[Apache Druid 数据迁移方案]]></title><link>https://xiaoming728.com/archives/1702301179854</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Apache%20Druid%20%E6%95%B0%E6%8D%AE%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88&amp;url=/archives/1702301179854" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E6%9C%AC%E6%96%87%E4%BB%8B%E7%BB%8D%E7%9A%84apache-druid-%E6%95%B0%E6%8D%AE%E8%BF%81%E7%A7%BB%E5%8F%AA%E6%98%AF%E5%9F%BA%E4%BA%8Elinux%E7%8E%AF%E5%A2%83%E9%83%A8%E7%BD%B2">本文介绍的Apache Druid 数据迁移只是基于linux环境部署</h3>
<h3 style="" id="%E7%8E%AF%E5%A2%83%E8%A6%81%E6%B1%82">环境要求</h3>
<ul>
 <li>
  <p style="">java7 或者更高版本</p></li>
 <li>
  <p style="">linux， macOS或者其他unix系统(不支持windows系统)</p></li>
 <li>
  <p style="">8G内存</p></li>
 <li>
  <p style="">2核CPU</p></li>
</ul>
<h3 style="" id="%E7%AC%AC%E4%B8%80%E7%A7%8D%E6%95%B0%E6%8D%AE%E8%BF%81%E7%A7%BB%E6%96%B9%E5%BC%8F">第一种数据迁移方式</h3>
<p style="">只是升级Druid的版本，还是使用默认的元数据管理数据库DerBy</p>
<p style=""></p>
<p style="">1、首先从apache druid官网下载合适的版本，我这里以apache-druid-0.20.0-bin.tar.gz版本为例，下载好安装包</p>
<p style=""></p>
<p style="">2、解压</p>
<p style=""><span fontsize="" color="rgb(56, 58, 66)" style="color: rgb(56, 58, 66)">tar&nbsp;-xzf&nbsp;</span>apache-druid-0.20.0-bin.tar.gz</p>
<p style=""></p>
<p style="">3、查看旧版本Druid运行时保存segments和数据库的所在目录</p>
<p style="">cd /home/apache-druid-0.20.0/conf/druid/cluster/_common</p>
<p style="">查看配置文件 <a target="_blank" rel="noopener noreferrer nofollow" href="http://common.runtime.properties">common.runtime.properties</a>&nbsp;</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F28c3360a0bafbe005968a928ab2ab328.png&amp;size=m" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F5bdd44ef1651e23859f4e58142f6c0f6.png&amp;size=m" style="display: inline-block"></p>
<p style="">4、 复制segment和元数据文件到新迁移的druid存储路径</p>
<p style=""></p>
<pre><code>如新版本Druid的存储目录是/data/druid
cp -r $DRUID_HOME/var/druid/segments /data/druid    #复制segment文件
cp -r $DRUID_HOME/var/druid/metadata.db $NEW_DRUID_HOME/var/druid/  #复制元数据文件，目录没有可以手动创建，注意权限</code></pre>
<p style=""></p>
<p style="">5、启动druid</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F5e757c7c1b5f1e6e85cba67af3199c56.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="%E7%AC%AC%E4%BA%8C%E7%A7%8D%E6%95%B0%E6%8D%AE%E8%BF%81%E7%A7%BB%E6%96%B9%E5%BC%8F">第二种数据迁移方式</h3>
<p style=""></p>
<p style="">1、使用导出元数据工具</p>
<p style=""></p>
<blockquote>
 <p style=""><span style="font-size: 14px; color: rgb(68, 68, 68)">进入到apache druid 的根目录：cd ${DRUID_ROOT}</span></p>
 <p style=""><span style="font-size: 14px; color: rgb(68, 68, 68)">创建一个临时文件夹：mkdir -p /tmp/csv</span></p>
 <p style=""><span style="font-size: 14px; color: rgb(68, 68, 68)">该命令只适用于linux部署，导出命令：</span></p>
 <p style=""><span style="font-size: 14px; color: rgb(68, 68, 68)">java -classpath </span><span style="font-size: 14px; color: rgb(136, 0, 0)">"lib/*"</span><span style="font-size: 14px; color: rgb(68, 68, 68)"> -Dlog4j.configurationFile=conf/druid/cluster/_common/log4j2.xml -</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://Ddruid.extensions.directory"><span style="font-size: 14px; color: rgb(68, 68, 68)">Ddruid.extensions.directory</span></a><span style="font-size: 14px; color: rgb(68, 68, 68)">=</span><span style="font-size: 14px; color: rgb(136, 0, 0)">"extensions"</span><span style="font-size: 14px; color: rgb(68, 68, 68)"> -Ddruid.extensions.loadList=[] org.apache.druid.cli.Main tools </span><span style="font-size: 14px; color: rgb(57, 115, 0)">export</span><span style="font-size: 14px; color: rgb(68, 68, 68)">-metadata --connectURI </span><span style="font-size: 14px; color: rgb(136, 0, 0)">"jdbc:derby://</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://localhost:1527/var/druid/metadata.db"><span style="font-size: 14px; color: rgb(136, 0, 0)">localhost:1527/var/druid/metadata.db</span></a><span style="font-size: 14px; color: rgb(136, 0, 0)">;"</span><span style="font-size: 14px; color: rgb(68, 68, 68)"> -o /tmp/csv</span></p>
</blockquote>
<p style=""></p>
<p style="">2、导入元数据</p>
<p style=""></p>
<blockquote>
 <p style=""><span style="font-size: 14px; color: rgb(0, 0, 0)">运行该工具后，输出目录将包含&lt;table-name&gt;_raw.csv和&lt;table-name&gt;.csv文件。</span></p>
 <p style=""><span style="font-size: 14px; color: rgb(0, 0, 0)">这些&lt;table-name&gt;_raw.csv文件是工具使用的中间文件，包含Derby导出的表数据，而无需进行修改。</span></p>
 <p style=""><span style="font-size: 14px; color: rgb(0, 0, 0)">这些&lt;table-name&gt;.csv文件用于导入到另一个数据库（例如MySQL和PostgreSQL）中，并且已应用任何已配置的深度存储位置重写。</span></p>
 <p style=""><span style="font-size: 14px; color: rgb(0, 0, 0)">下面显示了针对Derby，MySQL和PostgreSQL的示例导入命令。</span></p>
 <p style=""><span style="font-size: 14px; color: rgb(0, 0, 0)">这些示例导入命令期望/tmp/csv可以从服务器访问其内容。有关其他选项，例如从客户端文件系统导入，请参考数据库的文档。</span></p>
</blockquote>
<h3 style="" id="%E5%BE%B7%E6%AF%94">德比</h3>
<h3 style="" id="%E7%9A%84mysql">的MySQL</h3>
<pre><code>CALL SYSCS_UTIL.SYSCS_IMPORT_TABLE (null,'DRUID_SEGMENTS','/tmp/csv/druid_segments.csv',',','"',null,0);
CALL SYSCS_UTIL.SYSCS_IMPORT_TABLE (null,'DRUID_RULES','/tmp/csv/druid_rules.csv',',','"',null,0);
CALL SYSCS_UTIL.SYSCS_IMPORT_TABLE (null,'DRUID_CONFIG','/tmp/csv/druid_config.csv',',','"',null,0);
CALL SYSCS_UTIL.SYSCS_IMPORT_TABLE (null,'DRUID_DATASOURCE','/tmp/csv/druid_dataSource.csv',',','"',null,0);
CALL SYSCS_UTIL.SYSCS_IMPORT_TABLE (null,'DRUID_SUPERVISORS','/tmp/csv/druid_supervisors.csv',',','"',null,0);
复制</code></pre>
<pre><code>LOAD DATA INFILE '/tmp/csv/druid_segments.csv' INTO TABLE druid_segments FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' (id,dataSource,created_date,start,end,partitioned,version,used,payload); SHOW WARNINGS;
LOAD DATA INFILE '/tmp/csv/druid_rules.csv' INTO TABLE druid_rules FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' (id,dataSource,version,payload); SHOW WARNINGS;
LOAD DATA INFILE '/tmp/csv/druid_config.csv' INTO TABLE druid_config FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' (name,payload); SHOW WARNINGS;
LOAD DATA INFILE '/tmp/csv/druid_dataSource.csv' INTO TABLE druid_dataSource FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' (dataSource,created_date,commit_metadata_payload,commit_metadata_sha1); SHOW WARNINGS;
LOAD DATA INFILE '/tmp/csv/druid_supervisors.csv' INTO TABLE druid_supervisors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' (id,spec_id,created_date,payload); SHOW WARNINGS;
复制</code></pre>
<h3 style="" id="postgresql%E7%9A%84">PostgreSQL的</h3>
<pre><code>COPY druid_segments(id,dataSource,created_date,start,"end",partitioned,version,used,payload) FROM '/tmp/csv/druid_segments.csv' DELIMITER ',' CSV;
COPY druid_rules(id,dataSource,version,payload) FROM '/tmp/csv/druid_rules.csv' DELIMITER ',' CSV;
COPY druid_config(name,payload) FROM '/tmp/csv/druid_config.csv' DELIMITER ',' CSV;
COPY druid_dataSource(dataSource,created_date,commit_metadata_payload,commit_metadata_sha1) FROM '/tmp/csv/druid_dataSource.csv' DELIMITER ',' CSV;
COPY druid_supervisors(id,spec_id,created_date,payload) FROM '/tmp/csv/druid_supervisors.csv' DELIMIT</code></pre>
<p style="">针对默认的derby数据库，经研究未找到apache druid的内置数据库如何连接和使用，已经命令如何执行。</p>
<p style=""></p>
<p style="">3、将内置的derby数据库修改为mysql数据库</p>
<pre><code>vim $NEW_DRUID_HOME/conf/druid/single-server/micro-quickstart/_common/common.runtime.properties

添加MySQL为元数据存储 mysql-metadata-storage ：
druid.extensions.loadList=["druid-hdfs-storage", "druid-kafka-indexing-service", "druid-datasketches", "mysql-metadata-storage"]</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F591f384607cf027c0f9bad1af5550f30.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style="">4、 复制segments 数据</p>
<pre><code>cp -r $DRUID_HOME/var/druid/segments /data/druid    #复制segment文件</code></pre>
<p style=""></p>
<p style="">5、上传MySQL的驱动包到扩展目录</p>
<pre><code>cd $NEW_DRUID_HOME/extensions/mysql-metadata-storage/
wget http://192.168.1.85/source/mysql-connector-java-5.1.47.jar #或者本地上传至此目录下
ls
mysql-connector-java-5.1.47.jar  mysql-metadata-storage-0.17.0.jar</code></pre>
<p style=""></p>
<p style="">6、启动druid</p>
<p style=""><br><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Ff4fd1b198428dd172203e6e050511bce.png&amp;size=m" style="display: inline-block"></p>]]></description><guid isPermaLink="false">/archives/1702301179854</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Frruid.png&amp;size=m" type="image/jpeg" length="0"/><category>Apache Druid技术</category><pubDate>Mon, 11 Dec 2023 13:27:18 GMT</pubDate></item><item><title><![CDATA[Docker部署Apache Druid]]></title><link>https://xiaoming728.com/archives/1702301265435</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E9%83%A8%E7%BD%B2Apache%20Druid&amp;url=/archives/1702301265435" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">Apache Druid 官方网站：<a target="_blank" rel="noopener noreferrer nofollow" href="https://druid.apache.org/">https://druid.apache.org/</a></p>
 <p style="">Apache Druid Github：<a target="_blank" rel="noopener noreferrer nofollow" href="https://druid.apache.org/">https://github.com/apache/druid/</a></p>
 <p style="">Apache Druid 中文文档：<a target="_blank" rel="noopener noreferrer nofollow" href="https://druid.apache.org/">http://www.apache-druid.cn/</a></p>
 <p style="">电子书：<a target="_blank" rel="noopener noreferrer nofollow" class="ne-card-file" href="https://www.yuque.com/attachments/yuque/0/2021/pdf/22910907/1637638762725-19fc1d47-1530-48cf-a94c-93a4922d56bd.pdf">📎undefined.undefined</a></p>
 <p style="">Apache Druid 视频教程：<a target="_blank" rel="noopener noreferrer nofollow" href="https://druid.apache.org/">https://www.imooc.com/learn/1147</a></p>
</blockquote>
<p style=""></p>
<h4 style="" id="docker%E5%AE%B9%E5%99%A8%E5%AE%89%E8%A3%85druid">Docker容器安装Druid</h4>
<p style="">1、下载源码（要下载很长时间）</p>
<pre><code>git clone https://github.com/returncode/docker-druid.git -b druid-0.20.0</code></pre>
<p style="">2、编译打包镜像</p>
<pre><code>cd docker-druid
// 修改MySQL数据库链接
vi config/common.runtime.properties
// 打包镜像命令
docker build -t docker-druid .</code></pre>
<p style="">3、启动容器</p>
<pre><code>docker run -d --restart=always --name docker-druid \
-p 8888:8888 \
-p 8082:8082 \
-v /home/docker/druid/var:/opt/druid/var \
docker-druid</code></pre>
<p style="">4、打开链接 <a target="_blank" rel="noopener noreferrer nofollow" href="https://druid.apache.org/">http://192.168.8.201:8888/</a> 进入Druid配置管理页面。</p>
<p style=""></p>
<h4 style="" id="docker-compose%E5%AE%89%E8%A3%85druid">docker-compose安装Druid</h4>
<p style=""><span style="font-size: 13px; color: rgb(17, 17, 17)">Docker-Compose项目是Docker官方的开源项目，负责实现对Docker容器集群的快速编排。</span></p>
<p style="">1、下载druid镜像</p>
<pre><code>docker pull apache/druid:0.20.0</code></pre>
<p style="">2、安装python-pip</p>
<pre><code># 安装epel扩展源
yum -y install epel-release
# 安装pip
yum -y install python-pip
# 查看pip版本
pip --version
# 更新pip
pip install --upgrade pip</code></pre>
<p style="">3、安装<span style="font-size: 13px; color: rgb(17, 17, 17)">Docker-Compose</span></p>
<pre><code># 安装docker-compose
pip install docker-compose 
# 查看版本
docker-compose --version</code></pre>
<p style="">4、配置<span style="font-size: 14px; color: rgb(36, 41, 46)">docker-compose.yml</span></p>
<pre><code># 创建druid目录
mkdir /home/docker/druid
# 下载docker-compose.yml
https://github.com/apache/druid/blob/0.20.0/distribution/docker/docker-compose.yml
# 下载environment
https://github.com/apache/druid/blob/0.20.0/distribution/docker/environment
# 后台运行druid集群
docker-compose up -d</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702301265435</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Frruid.png&amp;size=m" type="image/jpeg" length="0"/><category>Apache Druid技术</category><pubDate>Mon, 11 Dec 2023 13:27:00 GMT</pubDate></item><item><title><![CDATA[Docker部署Canal-adapter组件]]></title><link>https://xiaoming728.com/archives/1702301164307</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E9%83%A8%E7%BD%B2Canal-adapter%E7%BB%84%E4%BB%B6&amp;url=/archives/1702301164307" width="1" height="1" alt="" style="opacity:0;">
<h4 style="" id="%E6%8B%89%E5%8F%96%E6%BA%90%E7%A0%81">拉取源码</h4>
<pre><code>git clone https://github.com/returncode/canal.git</code></pre>
<h4 style="" id="%E7%94%9F%E6%88%90%E9%95%9C%E5%83%8F">生成镜像</h4>
<p style="">注意：编译用时很长</p>
<pre><code>cd canal/client-adapter/docker
sh build.sh</code></pre>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code># cp配置文件
docker cp canal-adapter:/opt/canal-adapter/conf /home/docker/canal-adapter

# 启动容器
docker run -d -p 8081:8081 \
-v /home/docker/canal-adapter/conf:/opt/canal-adapter/conf \
-v /home/docker/canal-adapter/logs:/opt/canal-adapter/logs \
--name canal-adapter \
canal/canal-adapter:v1.1.6</code></pre>
<h4 style="" id="%E5%BA%94%E7%94%A8%E9%85%8D%E7%BD%AE">应用配置</h4>
<p style="">修改/conf/application.yml配置文件中的<code>canal tcp consumer</code>、数据源及<code>instance</code>和ES服务器地址</p>
<pre><code>server:
  port: 8081
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    default-property-inclusion: non_null

canal.conf:
  mode: tcp #tcp kafka rocketMQ rabbitMQ
  flatMessage: true
  zookeeperHosts:
  syncBatchSize: 1000
  retries: 0
  timeout:
  accessKey:
  secretKey:
  consumerProperties:
    # canal tcp consumer
    canal.tcp.server.host: 192.168.206.185:11111
    canal.tcp.zookeeper.hosts:
    canal.tcp.batch.size: 500
    canal.tcp.username:
    canal.tcp.password:
  srcDataSources:
    defaultDS:
      url: jdbc:mysql://192.168.206.180:3306/dev_archives_center?useUnicode=true
      username: canal
      password: linewell@123
  canalAdapters:
  - instance: dev # canal instance Name or mq topic name
    groups:
    - groupId: g1
      outerAdapters:
      - name: logger
      - name: es7
        hosts: 192.168.206.183:9300 # 127.0.0.1:9200 for rest mode
        properties:
          mode: transport # or rest
          # security.auth: test:123456 #  only used for rest mode
          cluster.name: docker-cluster</code></pre>
<blockquote>
 <p style="">参考文献</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.jianshu.com/p/9be1f2125c08">https://www.jianshu.com/p/9be1f2125c08</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.jianshu.com/p/9be1f2125c08">https://www.jianshu.com/u/14b3dd246ec4</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702301164307</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fcanal.png&amp;size=m" type="image/jpeg" length="0"/><category>ElasticSearch搜索引擎技术</category><pubDate>Mon, 11 Dec 2023 13:26:00 GMT</pubDate></item><item><title><![CDATA[Linux安装Elasticsearch]]></title><link>https://xiaoming728.com/archives/1702301026666</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%E5%AE%89%E8%A3%85Elasticsearch&amp;url=/archives/1702301026666" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="1%E3%80%81%E7%AE%80%E4%BB%8B">1、简介</h1>
<p style="">Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene(TM) 基础上的搜索引擎.当然 Elasticsearch 并不仅仅是 Lucene 那么简单，它不仅包括了全文搜索功能，还可以进行以下工作:</p>
<ul>
 <li>
  <p style="">分布式实时文件存储，并将每一个字段都编入索引，使其可以被搜索。</p></li>
 <li>
  <p style="">实时分析的分布式搜索引擎。</p></li>
 <li>
  <p style="">可以扩展到上百台服务器，处理PB级别的结构化或非结构化数据</p></li>
</ul>
<p style=""></p>
<blockquote>
 <p style="">像天猫、京东这样的商城，用户访问商城的首页，一般都会直接搜索来寻找自己想要购买的商品。而商品的数量非常多，而且分类繁杂。</p>
 <p style="">如果能正确的显示出用户想要的商品，并进行合理的过滤，尽快促成交易，是搜索系统要研究的核心。</p>
 <p style="">面对这样复杂的搜索业务和数据量，使用传统数据库搜索就显得力不从心，一般我们都会使用全文检索技术，比如Solr，Elasticsearch。</p>
</blockquote>
<p style=""></p>
<p style="">Elastic官网：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.elastic.co/cn/">https://www.elastic.co/cn/</a></p>
<p style="">Elastic有一条完整的产品线及解决方案：Elasticsearch、Kibana、Logstash等，前面说的三个就是大家常说的ELK技术栈。</p>
<p style=""></p>
<p style="">Elasticsearch（官网：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.elastic.co/cn/">https://www.elastic.co/cn/products/elasticsearch</a> ）是Elastic Stack 的核心技术。详细介绍参考官网</p>
<p style="">Elasticsearch具备以下特点：</p>
<ul>
 <li>
  <p style="">分布式，无需人工搭建集群（solr就需要人为配置，使用Zookeeper作为注册中心）</p></li>
 <li>
  <p style="">Restful风格，一切API都遵循Rest原则，容易上手近实时搜索，数据更新在Elasticsearch中几乎是完全同步的。</p></li>
</ul>
<h1 style="" id=""></h1>
<h1 style="" id="2%E3%80%81%E5%AE%89%E8%A3%85">2、安装</h1>
<p style="">为了模拟真实场景，我们将在linux下安装Elasticsearch。 虚拟机（需要JDK1.8以上）</p>
<h3 style="" id="2.1%EF%BC%9A%E5%85%88%E6%96%B0%E5%BB%BA%E4%B8%80%E4%B8%AA%E7%94%A8%E6%88%B7">2.1：先新建一个用户</h3>
<p style="">出于安全考虑，elasticsearch默认不允许以root账号运行</p>
<pre><code>创建用户：useradd esuser
设置密码：passwd esuser</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F3e2461a007ab943c391711333c0978fb.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="2.2%EF%BC%9A%E4%B8%8B%E8%BD%BD%E5%AE%89%E8%A3%85%E5%8C%85">2.2：下载安装包</h3>
<p style="">官网下载，选择linux版本：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.elastic.co/cn/">https://www.elastic.co/cn/products/elasticsearch</a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F3c2894211d500d676d9088f6358bbf49.png&amp;size=m" style="display: inline-block"></p>
<p style="">选择亦可下载，选择linux版本</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F4814e6aac4c6666fa403054979322b82.png&amp;size=m" style="display: inline-block"></p>
<p style="">&nbsp;</p>
<h3 style="" id="2.3%EF%BC%9A%E4%B8%8A%E4%BC%A0%E5%AE%89%E8%A3%85%E5%8C%85%E5%B9%B6%E8%A7%A3%E5%8E%8B">2.3：上传安装包并解压</h3>
<p style="">新建文件夹：mkdir es<br>
 我们将安装包上传到：/home/es目录</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F1c432ba2f6ec41ed44dfd3731e1ca3bf.png&amp;size=m" style="display: inline-block"></p>
<pre><code>解压：tar -zxvf elasticsearch-7.3.2-linux-x86_64.tar.gz 
目录重命名：mv elasticsearch-7.3.2 elasticsearch</code></pre>
<p style="">完成后，查看下目录结构：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fc753582ac1642e482666ec24f897ec47.png&amp;size=m" style="display: inline-block"></p>
<p style="">&nbsp;</p>
<h3 style="" id="2.4%EF%BC%9A%E4%BF%AE%E6%94%B9%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">&nbsp;2.4：修改配置文件</h3>
<pre><code>cd config</code></pre>
<p style="">需要修改的配置文件有两个：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F95a4b8db80403764a76f32552e9e47b1.png&amp;size=m" style="display: inline-block"></p>
<p style="">Elasticsearch基于Lucene的，而Lucene底层是java实现，因此我们需要配置jvm参数。编辑jvm.options</p>
<pre><code>vi jvm.options</code></pre>
<p style="">修改默认配置：-Xms1g&nbsp; &nbsp; -Xmx1g为<img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F554f3f061ff505eb0ac7fa506f287ac3.png&amp;size=m" style="display: inline-block"></p>
<p style="">&nbsp;编辑elasticsearch.yml修改数据和日志目录</p>
<pre><code>vi elasticsearch.yml

node.name: node-1 #配置当前es节点名称（默认是被注释的，并且默认有一个节点名）
cluster.name: my-application #默认是被注释的，并且默认有一个集群名
path.data: /home/es/data # 数据目录位置path.logs: /home/es/logs # 日志目录位置
network.host: 0.0.0.0   #绑定的ip：默认只允许本机访问，修改为0.0.0.0后则可以远程访问
cluster.initial_master_nodes: ["node-1", "node-2"] #默认是被注释的 设置master节点列表 用逗号分隔</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fa605a1f0db047b3830bdcee811d4db63.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style="">&nbsp;进入es的根目录，然后创建logs data</p>
<pre><code>mkdir data
mkdir logs</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F5d5b9064aec1e851aec96e0ff07cfd62.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style="">elasticsearch.yml的其它可配置信息：</p>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FAFAFA">
    <p style="">属性名</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FAFAFA">
    <p style="">说明</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.elastic.co/cn/">cluster.name</a></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">配置elasticsearch的集群名称，默认是elasticsearch。建议修改成一个有意义的名称。</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.elastic.co/cn/">node.name</a></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">节点名，es会默认随机指定一个名字，建议指定一个有意义的名称，方便管理</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">path.conf</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">设置配置文件的存储路径，tar或zip包安装默认在es根目录下的config文件夹，rpm安装默认在/etc/ elasticsearch</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.elastic.co/cn/">path.data</a></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">设置索引数据的存储路径，默认是es根目录下的data文件夹，可以设置多个存储路径，用逗号隔开</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">path.logs</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">设置日志文件的存储路径，默认是es根目录下的logs文件夹</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">path.plugins</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">设置插件的存放路径，默认是es根目录下的plugins文件夹</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">bootstrap.memory_lock</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">设置为true可以锁住ES使用的内存，避免内存进行swap</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.elastic.co/cn/">network.host</a></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">设置bind_host和publish_host，设置为0.0.0.0允许外网访问</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">http.port</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">设置对外服务的http端口，默认为9200。</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">transport.tcp.port</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">集群结点之间通信端口</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.elastic.co/cn/">discovery.zen.ping</a>.timeout</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">设置ES自动发现节点连接超时的时间，默认为3秒，如果网络延迟高可设置大些</p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">discovery.zen.minimum_master_nodes</p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style="">主结点数量的最少值 ,此值的公式为：(master_eligible_nodes / 2) + 1 ，比如：有3个符合要求的主结点，那么这里要设置为2</p></td>
  </tr>
 </tbody>
</table>
<h3 style="" id="2.5%EF%BC%9A%E4%BF%AE%E6%94%B9%2Fetc%2Fsecurity%2Flimits.conf%E6%96%87%E4%BB%B6-%E5%A2%9E%E5%8A%A0%E9%85%8D%E7%BD%AE">2.5：修改/etc/security/limits.conf文件 增加配置</h3>
<pre><code>vi /etc/security/limits.conf</code></pre>
<p style="">在文件最后，增加如下配置：</p>
<pre><code>* soft nofile 65536
* hard nofile 65536</code></pre>
<p style="">在/etc/sysctl.conf文件最后添加一行 vm.max_map_count=655360 添加完毕之后，执行命令： sysctl -p</p>
<pre><code>vi /etc/sysctl.conf
sysctl -p</code></pre>
<h3 style="" id="2.6%EF%BC%9A%E5%90%AF%E5%8A%A8">2.6：启动</h3>
<p style="">先将es文件夹下的所有目录的所有权限迭代给esuser用户</p>
<pre><code>chgrp -R esuser ./es
chown -R esuser ./es
chmod 777 es</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F1bdcacda46577734d09a40a6818915f4.png&amp;size=m" style="display: inline-block"></p>
<p style="">&nbsp;</p>
<p style="">切换到esuser用户启动</p>
<pre><code>su esuser

./bin/elasticsearch</code></pre>
<p style=""><strong>可以看到绑定了两个端口:</strong></p>
<ul>
 <li>
  <p style="">9300：集群节点间通讯接口</p></li>
 <li>
  <p style="">9200：客户端访问接口</p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F389bd55f4bae8c24d5d0e8d32ce96ade.png&amp;size=m" style="display: inline-block"></p>
<p style="">&nbsp;</p>
<h1 style="" id="3%E3%80%81%E6%B5%8B%E8%AF%95%E9%AA%8C%E8%AF%81">3、测试验证</h1>
<p style="">&nbsp;我们在浏览器中访问</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F0aa610db0a568ccaabafc6c4c0776a34.png&amp;size=m" style="display: inline-block"></p>
<blockquote>
 <p style="">来源：博客园-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://home.cnblogs.com/u/weibanggang/">韦邦杠</a></p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.elastic.co/cn/">https://www.cnblogs.com/weibanggang/p/11589464.html</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702301026666</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FElasticSearch.png&amp;size=m" type="image/jpeg" length="0"/><category>ElasticSearch搜索引擎技术</category><pubDate>Mon, 11 Dec 2023 13:25:20 GMT</pubDate></item><item><title><![CDATA[请求elasticsearch报403forbidden排查过程]]></title><link>https://xiaoming728.com/archives/1702301148990</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E8%AF%B7%E6%B1%82elasticsearch%E6%8A%A5403forbidden%E6%8E%92%E6%9F%A5%E8%BF%87%E7%A8%8B&amp;url=/archives/1702301148990" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1%E3%80%81es%E6%8A%A5%E9%94%99%EF%BC%9A">1、es报错：</h3>
<pre><code>{"type": "server", "timestamp": "2021-04-14T02:18:39,905Z", "level": "WARN", "component": "o.e.c.r.a.DiskThresholdMonitor", "cluster.name": "docker-cluster", "node.name": "c9721b7cf6e9", "message": "flood stage disk watermark [95%] exceeded on [L5lpjyRxQY6lIFn8F0g0xA][c9721b7cf6e9][/usr/share/elasticsearch/data/nodes/0] free: 12.9gb[4.3%], all indices on this node will be marked read-only", "cluster.uuid": "Me3hE70hSYSBLY9mdi3PHw", "node.id": "L5lpjyRxQY6lIFn8F0g0xA"  }</code></pre>
<p style="">显示磁盘空间占用95以上，es开启了只读保护。</p>
<p style="">原因参考： <a target="_blank" rel="noopener noreferrer nofollow" href="https://zhuanlan.zhihu.com/p/181671838">https://zhuanlan.zhihu.com/p/181671838</a></p>
<p style="">es修改：</p>
<pre><code>#es属性查看
GET _settings?pretty
#手动关闭只读保护
PUT _settings
{
  "index": {
    "blocks": {
      "read_only_allow_delete": "false"
    }
  }
}</code></pre>
<h3 style="" id="2%E3%80%81%E5%8E%BB%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%9F%A5%E7%9C%8B%E7%A3%81%E7%9B%98%E5%8D%A0%E7%94%A8%E6%83%85%E5%86%B5">2、去服务器查看磁盘占用情况</h3>
<pre><code>使用df -h查看磁盘空间
linux查询文件占用硬盘大小：
du -hs ./*</code></pre>
<h3 style="" id="3%E3%80%81%E5%8F%91%E7%8E%B0%E6%98%AFlogstash%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%89%93%E7%9A%84%E6%97%A5%E5%BF%97%E5%A4%AA%E5%A4%9A%E4%BA%86">3、发现是logstash容器日志打的日志太多了</h3>
<p style="">解决方法：config配置里删除output中的stdout{}</p>
<h3 style="" id="4%E3%80%81%E6%B8%85%E7%90%86docker%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97">4、清理Docker容器日志</h3>
<p style="">Docker容器日志查看与配置</p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://zhuanlan.zhihu.com/p/181671838">https://www.yuque.com/ygkdsh/newbie/hwo8vi</a></p>]]></description><guid isPermaLink="false">/archives/1702301148990</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FElasticSearch.png&amp;size=m" type="image/jpeg" length="0"/><category>ElasticSearch搜索引擎技术</category><pubDate>Mon, 11 Dec 2023 13:25:00 GMT</pubDate></item><item><title><![CDATA[Docker部署ElasticSearch、ELK、canal组件]]></title><link>https://xiaoming728.com/archives/1702300810506</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E9%83%A8%E7%BD%B2ElasticSearch%E3%80%81ELK%E3%80%81canal%E7%BB%84%E4%BB%B6&amp;url=/archives/1702300810506" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="%E4%B8%80%E3%80%81elasticsearch">一、ElasticSearch</h2>
<h3 style="" id="1%E3%80%81elasticsearch%E7%AE%80%E4%BB%8B">1、ElasticSearch简介</h3>
<p style=""><span style="font-size: 15px">Elaticsearch，简称为es， es是一个开源的</span><strong><span style="font-size: 15px">高扩展的分布式全文检索引擎</span></strong><span style="font-size: 15px">，它可以近乎实时的</span><strong><span style="font-size: 15px">存储</span></strong><span style="font-size: 15px">、</span><strong><span style="font-size: 15px">检索</span></strong><span style="font-size: 15px">数据；本身扩展性很好，可以扩展到上百台服务器，处理PB级别（大数据时代）的数据。es也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能，但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性，从而让全文搜索变得简单。</span></p>
<p style=""><span style="font-size: 15px">据国际权威的数据库产品评测机构DB Engines的统计，在2016年1月，ElasticSearch已超过Solr等，成为排名第一的搜索引擎类应用。</span></p>
<h3 style="" id="2%E3%80%81docker%E5%AE%89%E8%A3%85elasticsearch">2、Docker安装ElasticSearch</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull elasticsearch:7.6.2</code></pre>
<h4 style="" id=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F2cc547ba0e745b5917217644432f0a5c.png&amp;size=m" style="display: inline-block"></h4>
<h4 style="" id="%E6%9F%A5%E7%9C%8B%E9%95%9C%E5%83%8F">查看镜像</h4>
<pre><code>docker images</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F76636a6c9230448d116831315f2cca87.png&amp;size=m" style="display: inline-block"></p>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code>docker run -d -p 9200:9200 -p 9300:9300 \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
--name elasticsearch  \
elasticsearch:7.6.2</code></pre>
<p style="">把配置文件挂载到主机上</p>
<pre><code>docker run -d -p 9200:9200 -p 9300:9300 \
-v /home/docker/elasticsearch/config:/usr/share/elasticsearch/config \
-v /home/docker/elasticsearch/data:/usr/share/elasticsearch/data \
-v /home/docker/elasticsearch/logs:/usr/share/elasticsearch/logs \
-e "ES_JAVA_OPTS=-Xms2g -Xmx2g" \
-e "discovery.type=single-node" \
--name elasticsearch \
elasticsearch:7.6.2</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fd767d53712931a8c3fb82ee577a57121.png&amp;size=m" style="display: inline-block"></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">-e "discovery.type=single-node" 设置为单节点</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" 在启动时要设置ES的初始内存和最大内存，初始设置为2G，可能因为过大导致启动不了ES。</span></p></li>
</ul>
<h4 style="" id="%E8%AE%BF%E9%97%AEes%EF%BC%8C%E6%9F%A5%E7%9C%8Bes%E7%9B%B8%E5%85%B3%E4%BF%A1%E6%81%AF%EF%BC%88ip%3A9200%EF%BC%89">访问ES，查看ES相关信息（ip:9200）</h4>
<p style=""><span style="font-size: 15px">容器已经启动完成。现在可以在浏览器输入 ip + 端口号访问。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F0bb7d187d95c57a4b4f3a451700d673b.png&amp;size=m" style="display: inline-block"></p>
<h4 style="" id="%E5%AE%89%E8%A3%85elasticsearch%E7%9A%84%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D%E5%99%A8ik">安装ElasticSearch的中文分词器IK</h4>
<p style="">使用了Elasticsearch中默认的标准分词器，这个分词器在处理中文的时候会把中文单词切分成一个一个的汉字，因此引入中文的分词器就能解决这个问题。</p>
<ul>
 <li>
  <p style="">下载Ik分词器地址</p></li>
</ul>
<p style="">下载Ik分词器地址：<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2</a></p>
<p style="">注意：IK分词器版本号应和ES版本号一致</p>
<ul>
 <li>
  <p style="">进入容器</p></li>
</ul>
<pre><code>docker exec -it elasticsearch /bin/bash</code></pre>
<ul>
 <li>
  <p style="">在/usr/share/elasticsearch/plugins目录下创建ik文件夹</p></li>
</ul>
<pre><code>mkdir /usr/share/elasticsearch/plugins/ik</code></pre>
<ul>
 <li>
  <p style="">退出容器</p></li>
 <li>
  <p style="">将/<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">home文件夹中的elasticsearch-analysis-ik-7.6.2.zip</a>拷贝到容器ik文件夹中：</p></li>
</ul>
<pre><code>docker cp  /home/elasticsearch-analysis-ik-7.6.2.zip  elasticsearch:/usr/share/elasticsearch/plugins/ik/</code></pre>
<ul>
 <li>
  <p style="">重新进入容器的ik文件夹，<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">并解压和删除elasticsearch-analysis-ik-7.10.1.zip</a>：</p></li>
</ul>
<pre><code>docker exec -it elasticsearch  /bin/bash
 cd /usr/share/elasticsearch/plugins/ik
 unzip elasticsearch-analysis-ik-7.6.2.zip 
 rm -rf  elasticsearch-analysis-ik-7.6.2.zip</code></pre>
<ul>
 <li>
  <p style="">退出容器并重启容器</p></li>
</ul>
<pre><code>exit
docker restart  elasticsearch</code></pre>
<ul>
 <li>
  <p style="">测试效果</p></li>
</ul>
<pre><code>未使用ik，使用默认
POST archives_info/_analyze
{
  "text": "我是中国人"
}</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F658478a5ff13eadd71156fc5c0ed77ad-bukq.png&amp;size=m" style="display: inline-block"></p>
<pre><code>使用ik分词器
POST archives_info/_analyze
{
  "analyzer": "ik_max_word",
  "text": "我是中国人"
}</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F812d6ed863a1f979dfd5b8dd92b2a794.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E4%BA%8C%E3%80%81elasticsearch-head%E7%AE%A1%E7%90%86%E7%95%8C%E9%9D%A2">二、ElasticSearch Head管理界面</h2>
<h3 style="" id="1%E3%80%81elasticsearch-head%E7%AE%80%E4%BB%8B">1、ElasticSearch Head简介</h3>
<p style=""><span style="font-size: 15px">elasticsearch-head将是一款专门针对于elasticsearch的客户端工具。</span>提供集群管理、数据可视化、增删改查工具，<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.sojson.com/tag_elasticsearch.html"> </a><span style="font-size: 15px">elasticsearch</span>语句可视化等功能。</p>
<h3 style="" id="2%E3%80%81docker%E5%AE%89%E8%A3%85elasticsearch-head">2、Docker安装ElasticSearch Head</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull mobz/elasticsearch-head:5</code></pre>
<h4 style="" id=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F38c85ed7a48aeed8b0607aa3b70ed35b.png&amp;size=m" style="display: inline-block"></h4>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<p style="">因和后面Canal配置端口冲突此处映射端口改为<code>9001</code></p>
<pre><code>docker run -d -p 9001:9100 --name elasticsearch-head mobz/elasticsearch-head:5</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fc6e4465ddf59d035470f2c9358e2d5c1.png&amp;size=m" style="display: inline-block"></p>
<h4 style="" id="%E8%AE%BF%E9%97%AE%E5%B9%B6%E8%BF%9E%E6%8E%A5elasticsearch%EF%BC%88ip%3A9001%EF%BC%89">访问并连接elasticsearch（ip:9001）</h4>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F6e98adad063d17600a2d53b0d4b16965.png&amp;size=m" style="display: inline-block"></p>
<h4 style="" id="%E5%8F%AF%E8%83%BD%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98">可能遇到的问题</h4>
<ul>
 <li>
  <p style=""><strong><span style="font-size: 15px">可能会出现跨域拒绝访问问题，无法连接ES服务</span></strong></p></li>
</ul>
<p style=""><span style="font-size: 16px; color: rgb(51, 51, 51)">解决方法：</span></p>
<p style=""><span style="font-size: 16px; color: rgb(51, 51, 51)">进入elasticsearch容器内部，修改配置文件elasticsearch.yml</span></p>
<pre><code>cd ./config
vim elasticsearch.yml </code></pre>
<p style=""><span style="font-size: 16px; color: rgb(51, 51, 51)">在elasticsearch.yml中添加：</span></p>
<pre><code>http.cors.enabled: true
http.cors.allow-origin: "*"</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F8e11215fe7a81191d7aecba017503ee3.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(51, 51, 51)">重启 elasticsearch容器</span></p>
<p style=""></p>
<ul>
 <li>
  <p style=""><strong><span style="font-size: 15px">Docker容器ElasticSearch-Head创建索引无响应406</span></strong></p></li>
</ul>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F086430f2a1b873987a9be5b6f2bc6747.png&amp;size=m" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F8f74d48f06f8733f3023a0dec63cf81b.png&amp;size=m" style="display: inline-block"></p>
<p style="">解决方法：修改head的 Content-Type 设置，需要安装<code>vi</code></p>
<pre><code>1、进入容器
docker exec -it 容器id /bin/bash
2、修改vendor.js
vim /usr/src/app/_site/vendor.js
3、6886行 /contentType: "application/x-www-form-urlencoded 
   改成 contentType: "application/json;charset=UTF-8" 
  7574行 var inspectData = s.contentType === "application/x-www-form-urlencoded" &amp;&amp;` 
    改成 var inspectData = s.contentType === "application/json;charset=UTF-8" &amp;&amp;
4、退出容器，重启容器</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F96a8552d3f1e056a57e5762a9f5a88e7.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h2 style="" id="%E4%B8%89%E3%80%81elasticsearch%E7%9B%91%E6%8E%A7%E5%B7%A5%E5%85%B7---cerebro">三、ElasticSearch监控工具 - cerebro</h2>
<h3 style="" id="1%E3%80%81cerebro%E7%AE%80%E4%BB%8B">1、cerebro简介</h3>
<ul>
 <li>
  <p style="">监控es集群状态</p></li>
 <li>
  <p style="">管理es节点信息</p></li>
 <li>
  <p style="">通过REST结构对索引进行操作</p></li>
</ul>
<h3 style="" id="2%E3%80%81docker%E5%AE%89%E8%A3%85cerebro">2、docker安装cerebro</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull lmenezes/cerebro</code></pre>
<h4 style="" id=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fba5ca30518b86804f96ff558d6942ed8.png&amp;size=m" style="display: inline-block"></h4>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code>docker run -d --name cerebro -p 9001:9000 lmenezes/cerebro</code></pre>
<p style="">注意：cerebro的9000端口会与Portainer端口重复，所以这里映射成9001端口。</p>
<p style=""></p>
<h4 style="" id="%E8%AE%BF%E9%97%AEcerebro%EF%BC%8C%E8%BF%9E%E6%8E%A5es%E6%9C%8D%E5%8A%A1%EF%BC%88ip%3A9001%EF%BC%89">访问cerebro，连接es服务（ip:9001）</h4>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F22033fab5e97e938b01b36045c6ae372.png&amp;size=m" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F41d97c0454da5c709ee9fb20dd1fc577.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E5%9B%9B%E3%80%81elk-stack">四、ELK Stack</h2>
<p style="">“ELK”是三个开源项目的首字母缩写，这三个项目分别是：Elasticsearch、Logstash 和 Kibana。Elasticsearch 是一个搜索和分析引擎。Logstash是服务器端数据处理管道，能够同时从多个来源采集数据，转换数据，然后将数据发送到诸如 Elasticsearch 等“存储库”中。Kibana 则可以让用户在 Elasticsearch 中使用图形和图表对数据进行可视化。Elastic Stack 是 ELK Stack 的更新换代产品。新增了一个Beats，它是一系列轻量型的单一功能数据采集器，如Filebeat它是一个轻量级的日志收集处理工具(Agent)，Filebeat占用资源少，适合于在各个服务器上搜集日志后传输给Logstash，官方也推荐此工具。</p>
<h3 style="" id="1%E3%80%81docker%E5%AE%89%E8%A3%85kibana">1、Docker安装Kibana</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull kibana:7.6.2</code></pre>
<p style="">注意：版本必须与es一致</p>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code>docker run -d -p 5601:5601 --link 37d1e505576f:elasticsearch --name kibana f70986bc5191</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fd28271c8219ee54b3fbc3da00240d4e0.png&amp;size=m" style="display: inline-block"></p>
<p style="">37d1e505576f：es的容器id</p>
<h4 style="" id="%E8%AE%BF%E9%97%AE%E5%9F%9F%E5%90%8D%3A5601">访问域名:5601</h4>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fdee6e79fb975b00dc9fee7aabe7a1e9c.png&amp;size=m" style="display: inline-block"></p>
<h4 style="" id="%E6%B1%89%E5%8C%96kibana">汉化kibana</h4>
<p style="">安装后，默认是英文的，可以修改一下kibana的配置文件，显示成中文</p>
<p style="">登录容器，然后修改配置文件<code>kibana.yml</code>，<code>加上一行配置i18n.locale: zh-CN</code>。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fb861492aa21c0839bfa0d9fc936bd0bc.png&amp;size=m" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F1e9ca90a1b668188b64e0cba52d0c934.png&amp;size=m" style="display: inline-block"></p>
<p style="">重启kibana</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F60a8e73958d866fa009474b06f069d30.png&amp;size=m" style="display: inline-block"></p>
<h4 style="" id="%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7%EF%BC%9A%E7%94%A8%E6%9D%A5%E9%80%9A%E8%BF%87rest%E8%AF%B7%E6%B1%82%E6%9F%A5%E8%AF%A2es%E4%B8%AD%E6%95%B0%E6%8D%AE">常用工具：用来通过rest请求查询es中数据</h4>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F00eb5d9e254c741199f554119461b920.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="2%E3%80%81docker%E5%AE%89%E8%A3%85logstash">2、Docker安装Logstash</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull logstash:7.6.2</code></pre>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code>docker run --name logstash -d -p 5044:5044 fa5b3b1e9757</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fd6e97fff71c4093b8496d4e9155cd869.png&amp;size=m" style="display: inline-block"></p>
<h4 style="" id="%E6%A0%B9%E6%8D%AE%E5%AE%9E%E9%99%85%E9%9C%80%E8%A6%81%E9%85%8D%E7%BD%AElogstash">根据实际需要配置logstash</h4>
<h3 style="" id="3%E3%80%81docker%E5%AE%89%E8%A3%85filebeat">3、Docker安装Filebeat</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull elastic/filebeat:7.6.2</code></pre>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code>// 临时启动容器
docker run -d --name=filebeat elastic/filebeat:7.6.2
// 拷贝数据文件
docker cp filebeat:/usr/share/filebeat ./</code></pre>
<p style=""></p>
<h4 style="" id=""></h4>
<h2 style="" id="%E4%BA%94%E3%80%81canal">五、canal</h2>
<h3 style="" id="1%E3%80%81canal%E7%AE%80%E4%BB%8B">1、canal简介</h3>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcamo.githubusercontent.com%2F63881e271f889d4a424c55cea2f9c2065f63494fecac58432eac415f6e47e959%2F68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f32303139313130343130313733353934372e706e67&amp;size=m" width="1361" style="display: inline-block"></p>
<p style=""><strong>canal [kə'næl]</strong>，译意为水道/管道/沟渠，主要用途是基于 MySQL 数据库增量日志解析，提供增量数据订阅和消费。</p>
<p style="">简单来说canal的原理是根据mysql的主从复制原理实现的，canal伪装成slave库从而向master库读取binlog日志获取增量日志。</p>
<p style="">canal-admin(非必须但推荐使用)：为canal提供整体配置管理、节点运维等面向运维的功能，提供相对友好的WebUI操作界面，方便更多用户快速和安全的操作。</p>
<p style="">canal-server：服务端，从mysql读取binlog日志获取增量日志，可以通过tcp、kafka、RocketMQ等方式与客户端通信；通过zookeeper搭建集群。</p>
<p style="">canal-adapter：客户端，根据canal-server获取的增量日志执行适配到其他诸如elasticsearch、redis、mysql等端，实现数据同步。</p>
<h3 style="" id="2%E3%80%81%E5%87%86%E5%A4%87">2、准备</h3>
<p style="">1)使用canal-server需要先准备mysql，对于自建 MySQL , 需要先开启 Binlog 写入功能，配置 binlog-format 为 ROW 模式，<code>/mysql.conf.d/mysqld.cnf</code>中配置如下：</p>
<pre><code>[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义，不要和 canal 的 slaveId 重复</code></pre>
<p style="">配置完成后重启mysql，并查询是否配置生效：</p>
<pre><code>show variables like 'log_bin%';
show variables like 'binlog_format%';</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Ffbb2047a1a490c9179ff6f9d0feff544-uuvq.png&amp;size=m" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F4826cbbb447d4ef4acfd4304d0f6ca1b-ccxw.png&amp;size=m" style="display: inline-block"></p>
<p style="">2)授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant。</p>
<pre><code>CREATE USER canal IDENTIFIED BY '123456';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;</code></pre>
<h3 style="" id="3%E3%80%81docker%E5%AE%89%E8%A3%85canal-admin">3、docker安装canal-admin</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull canal/canal-admin:latest</code></pre>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code>docker run -d -p 8089:8089 -e server.port=8089 --name canal-admin canal/canal-admin</code></pre>
<p style="">用户名：admin</p>
<p style="">密码：123456</p>
<h3 style="" id="4%E3%80%81docker%E5%AE%89%E8%A3%85canal-server">4、docker安装canal-server</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull canal/canal-server:latest</code></pre>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code>docker run -d -p 9100:9100 -p 11110:11110 -p 11111:11111 -p 11112:11112 \
-v /home/docker/canal-server/logs:/home/admin/canal-server/logs \
-e canal.admin.manager=192.168.8.128:8089 \
-e canal.admin.port=11110 \
-e canal.admin.user=admin \
-e canal.admin.passwd=4ACFE3202A5FF5CF467898FC58AAB1D615029441 \
--name=canal-server \
--restart=always \
canal/canal-server</code></pre>
<h4 style="" id="%E5%BA%94%E7%94%A8%E9%85%8D%E7%BD%AE">应用配置</h4>
<p style=""><code>canal-server</code>启动完成后访问<code>canal-admin</code>链接（IP:8089）因为docke启动的时候配置<code>admin</code>地址，<code>canal-admin</code>服务管理里会自动识别到<code>canal-server</code>的服务</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fb2c58994f59d5fe5a747393f0bc10ac3-zigh.png&amp;size=m" style="display: inline-block"></p>
<p style="">第一步：现在 <code>Instance 管理</code>里添加一个实例，先输入实例名称和选择服务，点击<code>载入模板</code>修改数据库配置</p>
<pre><code>...
# position info
canal.instance.master.address=192.168.8.128:3306
...
# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=linewell@123
# table regex
canal.instance.filter.regex=archives_center\\..*
# mq config
canal.mq.topic=example
canal.mq.partition=0</code></pre>
<p style="">第二部：在<code>Server 管理</code>中选择操作下拉框配置，在配置文件99行添加刚配置的实例名称，多个用<code>,</code>（逗号）隔开</p>
<pre><code>#################################################
######### 		destinations		#############
#################################################
canal.destinations = archives</code></pre>
<h3 style="" id="5%E3%80%81docker%E5%AE%89%E8%A3%85canal-adapter">5、docker安装canal-adapter</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull liazhan/canal-adapter:v1.1.5</code></pre>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code>docker cp canal-adapter:/opt/canal/adapter/conf /home/docker/canal-adapter
docker run -d -p 8081:8081 \
-v /home/docker/canal-adapter/conf:/opt/canal/adapter/conf \
-v /home/docker/canal-adapter/logs:/opt/canal/adapter/logs \
--name canal-adapter \
liazhan/canal-adapter:v1.1.5 </code></pre>
<h4 style="" id="%E5%BA%94%E7%94%A8%E9%85%8D%E7%BD%AE">应用配置</h4>
<p style="">修改/conf/application.yml配置文件中的<code>canalServerHost</code>、数据源及<code>instance</code>和ES服务器地址</p>
<pre><code>server:
  port: 8081
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    default-property-inclusion: non_null
# canal conf
canal.conf:
  mode: tcp
  canalServerHost: 192.168.8.128:11111 # canal server host
  batchSize: 500
  syncBatchSize: 1000
  retries: 0
  timeout:
  accessKey:
  secretKey:
  username:
  password:
  vhost:
  srcDataSources:
    defaultDS:
      url: jdbc:mysql://192.168.8.128:3306/archives_center?useUnicode=true
      username: canal
      password: linewell@123
  canalAdapters:
  - instance: archives # canal instance Name or mq topic name
    groups:
    - groupId: g1
      outerAdapters:
      - name: logger
      - name: es7
        hosts: 192.168.8.128:9300 # 127.0.0.1:9200 for rest mode
        properties:
          mode: transport # or rest
          # security.auth: test:123456 #  only used for rest mode
          cluster.name: docker-cluster</code></pre>
<blockquote>
 <p style="">参考文献</p>
 <p style="">1、Docker安装ElasticSearch</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://blog.csdn.net/qq_32101993/article/details/100021002</a></p>
 <p style="">2、docker安装elasticsearch及head插件</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://www.cnblogs.com/afeige/p/10771140.html</a></p>
 <p style="">3、Docker容器ElasticSearch-Head创建索引无响应406</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://blog.csdn.net/gang_luo/article/details/104210051/</a></p>
 <p style="">4、docker搭建cerebro（elasticsearch监控工具）</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://blog.csdn.net/wyfsxs/article/details/89305935</a></p>
 <p style="">5、使用Docker安装IK分词器</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://blog.csdn.net/qq_49470767/article/details/112463810</a></p>
 <p style="">6、安装Kibana:7.6.2完整教程</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://blog.csdn.net/WoAiShuiGeGe/article/details/106647832</a></p>
 <p style="">7、docker部署elk时汉化Kibana服务</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://www.cnblogs.com/abclife/p/12697866.html</a></p>
 <p style="">8、docker logstash安装</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://blog.csdn.net/qq_33547169/article/details/86629261</a></p>
 <p style="">9、docker安装canal同步mysql8与elasticsearch7数据</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://blog.csdn.net/daziyuanazhen/article/details/106098887</a></p>
 <p style="">10、github——canal</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2">https://github.com/alibaba/canal</a></p>
 <p style=""></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702300810506</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FElasticSearch.png&amp;size=m" type="image/jpeg" length="0"/><category>ElasticSearch搜索引擎技术</category><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 13:23:00 GMT</pubDate></item><item><title><![CDATA[Docker部署Elasticsearch集群]]></title><link>https://xiaoming728.com/archives/1702300767946</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E9%83%A8%E7%BD%B2Elasticsearch%E9%9B%86%E7%BE%A4&amp;url=/archives/1702300767946" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1%E3%80%81%E4%B8%8B%E8%BD%BD%E9%95%9C%E5%83%8F">1、下载镜像</h3>
<pre><code>docker pull elasticsearch:7.6.2</code></pre>
<h3 style="" id="2%E3%80%81%E9%85%8D%E7%BD%AE%E5%8F%82%E6%95%B0">2、配置参数</h3>
<p style="">创建3个节点目录</p>
<pre><code>mkdir -p /home/docker/elasticsearch/node0/config
mkdir -p /home/docker/elasticsearch/node1/config
mkdir -p /home/docker/elasticsearch/node2/config</code></pre>
<p style="">添加节点1配置文件</p>
<pre><code>vi /home/docker/elasticsearch/node0/config/elasticsearch.yml</code></pre>
<p style="">将下面内容复制到配置文件中保存</p>
<pre><code>cluster.name: docker-cluster

node.name: es-node0
node.master: true
node.data: true

path.data: /usr/share/elasticsearch/data
path.logs: /usr/share/elasticsearch/logs

network.bind_host: 0.0.0.0
network.publish_host: 192.168.8.129

http.port: 9200
transport.tcp.port: 9300

http.cors.enabled: true
http.cors.allow-origin: "*"

discovery.zen.ping.unicast.hosts: ["192.168.8.129:9300","192.168.8.129:9301","192.168.8.129:9302"]
discovery.zen.minimum_master_nodes: 2</code></pre>
<p style="">添加节点2配置文件</p>
<pre><code>vi /home/docker/elasticsearch/node1/config/elasticsearch.yml</code></pre>
<p style="">将下面内容复制到配置文件中保存</p>
<pre><code>cluster.name: docker-cluster

node.name: es-node1
node.master: true
node.data: true

path.data: /usr/share/elasticsearch/data
path.logs: /usr/share/elasticsearch/logs

network.bind_host: 0.0.0.0
network.publish_host: 192.168.8.129

http.port: 9201
transport.tcp.port: 9301

http.cors.enabled: true
http.cors.allow-origin: "*"

discovery.zen.ping.unicast.hosts: ["192.168.8.129:9300","192.168.8.129:9301","192.168.8.129:9302"]
discovery.zen.minimum_master_nodes: 2</code></pre>
<p style="">添加节点3配置文件</p>
<pre><code>vi /home/docker/elasticsearch/node2/config/elasticsearch.yml</code></pre>
<p style="">将下面内容复制到配置文件中保存</p>
<pre><code>cluster.name: docker-cluster

node.name: es-node2
node.master: true
node.data: true

path.data: /usr/share/elasticsearch/data
path.logs: /usr/share/elasticsearch/logs

network.bind_host: 0.0.0.0
network.publish_host: 192.168.8.129

http.port: 9202
transport.tcp.port: 9302

http.cors.enabled: true
http.cors.allow-origin: "*"

discovery.zen.ping.unicast.hosts: ["192.168.8.129:9300","192.168.8.129:9301","192.168.8.129:9302"]
discovery.zen.minimum_master_nodes: 2</code></pre>
<h3 style="" id="3%E3%80%81%E5%AE%89%E8%A3%85%E9%95%9C%E5%83%8F">3、安装镜像</h3>
<pre><code>docker run -d -p 9200:9200 -p 9300:9300 \
-v /home/docker/elasticsearch/node0/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /home/docker/elasticsearch/node0/data:/usr/share/elasticsearch/data \
-v /home/docker/elasticsearch/node0/logs:/usr/share/elasticsearch/logs \
-e ES_JAVA_OPTS="-Xms2g -Xmx2g" \
--name elasticsearch-node0 \
elasticsearch:7.6.2</code></pre>
<pre><code>docker run -d -p 9201:9201 -p 9301:9301 \
-v /home/docker/elasticsearch/node1/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /home/docker/elasticsearch/node1/data:/usr/share/elasticsearch/data \
-v /home/docker/elasticsearch/node1/logs:/usr/share/elasticsearch/logs \
-e ES_JAVA_OPTS="-Xms2g -Xmx2g" \
--name elasticsearch-node1 \
elasticsearch:7.6.2</code></pre>
<pre><code>docker run -d -p 9202:9202 -p 9302:9302 \
-v /home/docker/elasticsearch/node2/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /home/docker/elasticsearch/node2/data:/usr/share/elasticsearch/data \
-v /home/docker/elasticsearch/node2/logs:/usr/share/elasticsearch/logs \
-e ES_JAVA_OPTS="-Xms2g -Xmx2g" \
--name elasticsearch-node2 \
elasticsearch:7.6.2</code></pre>
<h3 style="" id="4%E3%80%81%E9%AA%8C%E8%AF%81%E5%AE%89%E8%A3%85">4、验证安装</h3>
<p style="">镜像安装完成后访问：<a target="_blank" rel="noopener noreferrer nofollow" href="http://192.168.8.129:9200/_cat/nodes?pretty">http://192.168.8.129:9200/_cat/nodes?pretty</a> 查看集群情况</p>]]></description><guid isPermaLink="false">/archives/1702300767946</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FElasticSearch.png&amp;size=m" type="image/jpeg" length="0"/><category>ElasticSearch搜索引擎技术</category><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 13:19:00 GMT</pubDate></item><item><title><![CDATA[Filebeat安装部署]]></title><link>https://xiaoming728.com/archives/1702300785662</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Filebeat%E5%AE%89%E8%A3%85%E9%83%A8%E7%BD%B2&amp;url=/archives/1702300785662" width="1" height="1" alt="" style="opacity:0;">
<p style="">　　最近在了解ELK做日志采集相关的内容，这篇文章主要讲解通过filebeat来实现日志的收集。日志采集的工具有很多种，如fluentd, flume, logstash,betas等等。首先要知道为什么要使用filebeat呢？因为logstash是jvm跑的，资源消耗比较大，启动一个logstash就需要消耗500M左右的内存，而filebeat只需要10来M内存资源。常用的ELK日志采集方案中，大部分的做法就是将所有节点的日志内容通过filebeat送到kafka消息队列，然后使用logstash集群读取消息队列内容，根据配置文件进行过滤。然后将过滤之后的文件输送到elasticsearch中，通过kibana去展示。</p>
<h3 style="" id="filebeat%E4%BB%8B%E7%BB%8D">filebeat介绍</h3>
<p style="">　　Filebeat由两个主要组成部分组成：prospector和 harvesters。这些组件一起工作来读取文件并将事件数据发送到您指定的output。</p>
<p style="">什么是harvesters？</p>
<p style="">　　harvesters负责读取单个文件的内容。harvesters逐行读取每个文件，并将内容发送到output中。每个文件都将启动一个harvesters。harvesters负责文件的打开和关闭，这意味着harvesters运行时，文件会保持打开状态。如果在收集过程中，即使删除了这个文件或者是对文件进行重命名，Filebeat依然会继续对这个文件进行读取，这时候将会一直占用着文件所对应的磁盘空间，直到Harvester关闭。默认情况下，Filebeat会一直保持文件的开启状态，直到超过配置的close_inactive参数，Filebeat才会把Harvester关闭。</p>
<p style="">关闭Harvesters会带来的影响：</p>
<p style="">　　file Handler将会被关闭，如果在Harvester关闭之前，读取的文件已经被删除或者重命名，这时候会释放之前被占用的磁盘资源。</p>
<p style="">　　当时间到达配置的scan_frequency参数，将会重新启动为文件内容的收集。</p>
<p style="">　　如果在Havester关闭以后，移动或者删除了文件，Havester再次启动时，将会无法收集文件数据。</p>
<p style="">　　当需要关闭Harvester的时候，可以通过close_*配置项来控制。</p>
<p style="">什么是Prospector？</p>
<p style="">　　Prospector负责管理Harvsters，并且找到所有需要进行读取的数据源。如果input type配置的是log类型，Prospector将会去配置度路径下查找所有能匹配上的文件，然后为每一个文件创建一个Harvster。每个Prospector都运行在自己的Go routine里。</p>
<p style="">　　Filebeat目前支持两种Prospector类型：log和stdin。每个Prospector类型可以在配置文件定义多个。log Prospector将会检查每一个文件是否需要启动Harvster，启动的Harvster是否还在运行，或者是该文件是否被忽略（可以通过配置 ignore_order，进行文件忽略）。如果是在Filebeat运行过程中新创建的文件，只要在Harvster关闭后，文件大小发生了变化，新文件才会被Prospector选择到。</p>
<h3 style="" id="filebeat%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86">filebeat工作原理</h3>
<p style="">　　Filebeat可以保持每个文件的状态，并且频繁地把文件状态从注册表里更新到磁盘。这里所说的文件状态是用来记录上一次Harvster读取文件时读取到的位置，以保证能把全部的日志数据都读取出来，然后发送给output。如果在某一时刻，作为output的ElasticSearch或者Logstash变成了不可用，Filebeat将会把最后的文件读取位置保存下来，直到output重新可用的时候，快速地恢复文件数据的读取。在Filebaet运行过程中，每个Prospector的状态信息都会保存在内存里。如果Filebeat出行了重启，完成重启之后，会从注册表文件里恢复重启之前的状态信息，让FIlebeat继续从之前已知的位置开始进行数据读取。</p>
<p style="">Prospector会为每一个找到的文件保持状态信息。因为文件可以进行重命名或者是更改路径，所以文件名和路径不足以用来识别文件。对于Filebeat来说，都是通过实现存储的唯一标识符来判断文件是否之前已经被采集过。</p>
<p style="">　　如果在你的使用场景中，每天会产生大量的新文件，你将会发现Filebeat的注册表文件会变得非常大。这个时候，你可以参考（<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.elastic.co/guide/en/beats/filebeat/current/faq.html#reduce-registry-size">the section called “Registry file is too large?</a><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://github.com/elastic/beats/edit/5.1/filebeat/docs/faq.asciidoc">edit</a>），来解决这个问题。</p>
<h3 style="" id="%E5%AE%89%E8%A3%85filebeat%E6%9C%8D%E5%8A%A1">安装filebeat服务</h3>
<p style="">下载和安装key文件</p>
<pre><code>rpm --import https://packages.elastic.co/GPG-KEY-elasticsearch</code></pre>
<h4 style="" id="1%E3%80%81%E4%BD%BF%E7%94%A8yum%E5%AE%89%E8%A3%85%EF%BC%88%E6%8E%A8%E8%8D%90%EF%BC%89">1、使用yum安装（推荐）</h4>
<p style="">创建yum源文件</p>
<pre><code>[root@localhost ~]# vi /etc/yum.repos.d/elastic.repo
[elastic-7.x]
name=Elastic repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/oss-7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md</code></pre>
<p style=""><span fontsize="" color="">开始安装</span></p>
<pre><code>yum install filebeat</code></pre>
<h4 style="" id="2%E3%80%81%E4%BD%BF%E7%94%A8rpm%E5%AE%89%E8%A3%85">2、使用rpm安装</h4>
<p style="">下载rmp包</p>
<pre><code>curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.6.2-x86_64.rpm</code></pre>
<p style="">开发安装</p>
<pre><code>rpm -vi filebeat-7.6.2-x86_64.rpm</code></pre>
<p style="">启动服务</p>
<pre><code># 启动服务
systemctl start filebeat
# 查看状态
systemctl status filebeat</code></pre>
<p style="">重启服务</p>
<pre><code>systemctl restart filebeat</code></pre>
<p style="">提示：如果你启动的是一个filebeat容器，需要将/var/lib/docker/containers目录挂载到该容器中；</p>
<p style=""></p>
<h3 style="" id="%E6%94%B6%E9%9B%86%E6%97%A5%E5%BF%97">收集日志</h3>
<p style="">这里我们先以收集docker日志为例，简单来介绍一下filebeat的配置文件该如何编写。具体内容如下：</p>
<pre><code>[root@localhost ~]# grep "^\s*[^# \t].*$" /etc/filebeat/filebeat.yml 
filebeat.prospectors:
- input_type: log
  paths:
    - /var/lib/docker/containers/*/*.log
output.elasticsearch:
  hosts: ["192.168.58.128:9200"]</code></pre>
<p style="">和我们看的一样，其实并没有太多的内容。我们采集/var/lib/docker/containers/*/*.log，即filebeat所在节点的所有容器的日志。输出的位置是我们ElasticSearch的服务地址，这里我们直接将log输送给ES，而不通过Logstash中转。</p>
<p style="">再启动之前，我们还需要向ES提交一个filebeat index template，以便让elasticsearch知道filebeat输出的日志数据都包含哪些属性和字段。filebeat.template.json这个文件安装完之后就有，无需自己编写，找不到的同学可以通过find查找。加载模板到elasticsearch中：</p>
<pre><code>[root@localhost ~]# curl -XPUT 'http://192.168.58.128:9200/_template/filebeat?pretty' -d@/etc/filebeat/filebeat.template.json
{
  "acknowledged" : true
}</code></pre>
<p style=""></p>
<h3 style="" id="kibana%E9%85%8D%E7%BD%AE">Kibana配置</h3>
<p style="">如果上面配置都没有问题，就可以访问Kibana，不过这里需要添加一个新的index pattern。按照manual中的要求，对于filebeat输送的日志，我们的index name or pattern应该填写为："filebeat-*"。</p>]]></description><guid isPermaLink="false">/archives/1702300785662</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FFlume.png&amp;size=m" type="image/jpeg" length="0"/><category>ElasticSearch搜索引擎技术</category><pubDate>Mon, 11 Dec 2023 13:19:00 GMT</pubDate></item><item><title><![CDATA[canal-adapter实时增量同步多数据源数据（Mysql-Es）]]></title><link>https://xiaoming728.com/archives/1702300709866</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=canal-adapter%E5%AE%9E%E6%97%B6%E5%A2%9E%E9%87%8F%E5%90%8C%E6%AD%A5%E5%A4%9A%E6%95%B0%E6%8D%AE%E6%BA%90%E6%95%B0%E6%8D%AE%EF%BC%88Mysql-Es%EF%BC%89&amp;url=/archives/1702300709866" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1%E3%80%81%E5%87%86%E5%A4%87">1、准备</h3>
<p style="">1)使用canal-server需要先准备mysql，对于自建 MySQL , 需要先开启 Binlog 写入功能，配置 binlog-format 为 ROW 模式，<code>/mysql.conf.d/mysqld.cnf</code>中配置如下：</p>
<pre><code>[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义，不要和 canal 的 slaveId 重复</code></pre>
<p style=""><strong>需要注意binlog-do-db是否存在指定开启数据库如果配置了只会打印此数据库binlog日志</strong></p>
<p style="">配置完成后重启mysql，并查询是否配置生效：</p>
<pre><code>show variables like 'log_bin%';
show variables like 'binlog_format%';</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Ffbb2047a1a490c9179ff6f9d0feff544.png&amp;size=m" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F4826cbbb447d4ef4acfd4304d0f6ca1b-wyho.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="3%E3%80%81%E5%88%9B%E5%BB%BA%E5%AE%9E%E4%BE%8B">3、创建实例</h3>
<p style=""><code>canal-server</code>启动完成后访问<code>canal-admin</code>链接（IP:8089）因为docke启动的时候配置<code>admin</code>地址，<code>canal-admin</code>服务管理里会自动识别到<code>canal-server</code>的服务</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fb2c58994f59d5fe5a747393f0bc10ac3.png&amp;size=m" style="display: inline-block"></p>
<p style="">第一步：现在 <code>Instance 管理</code>里添加多个实例，先输入实例名称和选择服务，点击<code>载入模板</code>修改数据库配置</p>
<pre><code>...
# position info
canal.instance.master.address=192.168.8.128:3306
...
# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=linewell@123
# table regex
canal.instance.filter.regex=archives_center\\..*
# mq config
canal.mq.topic=example
canal.mq.partition=0</code></pre>
<p style="">第二部：在<code>Server 管理</code>中选择操作下拉框配置，在配置文件99行添加刚配置的实例名称，多个用<code>,</code>（逗号）隔开</p>
<pre><code>#################################################
######### 		destinations		#############
#################################################
canal.destinations = official,center</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fad26c0d7e47bf6634eb9d2c331e24fcc.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="5%E3%80%81docker%E5%AE%89%E8%A3%85canal-adapter">5、docker安装canal-adapter</h3>
<h4 style="" id="%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F">拉取镜像</h4>
<pre><code>docker pull liazhan/canal-adapter:v1.1.5</code></pre>
<ul>
 <li>
  <p style="">注意: 1.1.5版本有时间同步bug, 会将名称相似的时间字段同时更新, 1.1.6版本已修复但未发版本.</p></li>
 <li>
  <p style="">1.1.6版本暂时未发布, 解决办法为下载源码手动导出镜像, 现在安装adapter是需要上传镜像包手动导入镜像:</p></li>
</ul>
<p style=""><code>docker load &lt; canal-adapter_v1.1.6.tar canal/canal-adapter:1.1.6</code></p>
<h4 style="" id="%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8">运行容器</h4>
<pre><code>docker cp canal-adapter:/opt/canal/adapter/conf /home/docker/canal-adapter
docker run -d -p 8081:8081 \
-v /home/docker/canal-adapter/conf:/opt/canal/adapter/conf \
-v /home/docker/canal-adapter/logs:/opt/canal/adapter/logs \
--name canal-adapter \
canal/canal-adapter:v1.1.6</code></pre>
<h3 style="" id="6%E3%80%81%E5%90%8C%E6%AD%A5%E8%84%9A%E6%9C%AC%E9%85%8D%E7%BD%AE">6、同步脚本配置</h3>
<p style="">修改/conf/application.yml配置文件中的<code>canalServerHost</code>、数据源及<code>instance</code>和ES服务器地址</p>
<pre><code>server:
  port: 8081
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    default-property-inclusion: non_null

canal.conf:
  mode: tcp #tcp kafka rocketMQ rabbitMQ
  flatMessage: true
  zookeeperHosts:
  syncBatchSize: 1000
  retries: 0
  timeout:
  accessKey:
  secretKey:
  consumerProperties:
    # canal tcp consumer
    canal.tcp.server.host: 172.17.0.5:11111
    canal.tcp.zookeeper.hosts:
    canal.tcp.batch.size: 500
    canal.tcp.username:
    canal.tcp.password:
  srcDataSources:
    #可以配置多个监控数据源名称自定义不可重复
    sourceDB1: 
      url: jdbc:mysql://172.17.0.4:3306/pre_archives_center?serverTimezone=GMT%2B8&amp;useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;allowMultiQueries=true
      username: canal
      password: 123456
    sourceDB2:
      url: jdbc:mysql://172.17.0.4:3306/pre_archives_center?serverTimezone=GMT%2B8&amp;useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;allowMultiQueries=true
      username: canal
      password: 123456
  canalAdapters:
  - instance: official # 对应实例名称 多个实例创建多个
    groups:
    - groupId: g1
      outerAdapters:
      - name: logger
      - name: es7
        hosts: 172.17.0.8:9300
        properties:
          mode: transport # or rest
#          # security.auth: test:123456 #  only used for rest mode
          cluster.name: docker-cluster
  - instance: center # 对应实例名称 多个实例创建多个
    groups:
    - groupId: g1
      outerAdapters:
      - name: logger
      - name: es7
        hosts: 172.17.0.8:9300
        properties:
          mode: transport # or rest
#          # security.auth: test:123456 #  only used for rest mode
          cluster.name: docker-cluster
#      - name: rdb
#        key: mysql1
#        properties:
#          jdbc.driverClassName: com.mysql.jdbc.Driver
#          jdbc.url: jdbc:mysql://127.0.0.1:3306/mytest2?useUnicode=true
#          jdbc.username: root
#          jdbc.password: 121212
#      - name: rdb
#        key: oracle1
#        properties:
#          jdbc.driverClassName: oracle.jdbc.OracleDriver
#          jdbc.url: jdbc:oracle:thin:@localhost:49161:XE
#          jdbc.username: mytest
#          jdbc.password: m121212
#      - name: rdb
#        key: postgres1
#        properties:
#          jdbc.driverClassName: org.postgresql.Driver
#          jdbc.url: jdbc:postgresql://localhost:5432/postgres
#          jdbc.username: postgres
#          jdbc.password: 121212
#          threads: 1
#          commitSize: 3000
#      - name: hbase
#        properties:
#          hbase.zookeeper.quorum: 127.0.0.1
#          hbase.zookeeper.property.clientPort: 2181
#          zookeeper.znode.parent: /hbase
</code></pre>
<p style=""><strong><span style="font-size: 19px; color: rgb(38, 38, 38)">6、同步脚本编写</span></strong></p>
<pre><code>dataSourceKey: officialDS #监控数据源
destination: official #实例
groupId: g1 #分组
esMapping:
  _index: official_archives_info
  _type: _doc
  _id: _id
  sql: "SELECT
  t.unid as _id,
        t.unid,
        t.system_id as systemId,
        t.config_id as configId,
        t.province_code as provinceCode,
        t.city_code as cityCode,
        t.area_code as areaCode,
        t.dept_code as deptCode,
        t.document_code as documentCode,
        t.document_title as documentTitle,
        t.completed_time as completedTime,
        t.archive_code as archiveCode,
        t.accept_type as acceptType,
        t.version,
        t.archive_path as archivePath,
        t.archive_state as archiveState,
        t.accept_flag as acceptFlag,
        t.withdraw_node as withdrawNode,
        t.accept_time as acceptTime,
        t.pack_flag as packFlag,
        t.pack_time as packTime,
        t.encry_flag as encryFlag,
        t.encry_type as encryType,
        t.encry_password as encryPassword,
        t.encry_time as encryTime,
        t.check_flag as checkFlag,
        t.check_time as checkTime,
        t.audit_flag as auditFlag,
        t.audit_opinion as auditOpinion,
        t.audit_time as auditTime,
        t.transfer_flag as transferFlag,
        t.transfer_time as transferTime,
        t.failure_cause as failureCause,
        t.overdue_time as overdueTime,
        t.expired_time as expiredTime,
        t.batch_id as batchId,
        t.create_time as createTime,
        t.update_time as updateTime
FROM
        official_archives_info AS t
"
  #etlCondition: "where t.update_time&gt;={}"
  commitBatch: 3000

</code></pre>
<blockquote>
 <p style="">参考文献</p>
 <p style="">1、Docker安装ElasticSearch</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://blog.csdn.net/qq_32101993/article/details/100021002</a></p>
 <p style="">2、docker安装elasticsearch及head插件</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://www.cnblogs.com/afeige/p/10771140.html</a></p>
 <p style="">3、Docker容器ElasticSearch-Head创建索引无响应406</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://blog.csdn.net/gang_luo/article/details/104210051/</a></p>
 <p style="">4、docker搭建cerebro（elasticsearch监控工具）</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://blog.csdn.net/wyfsxs/article/details/89305935</a></p>
 <p style="">5、使用Docker安装IK分词器</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://blog.csdn.net/qq_49470767/article/details/112463810</a></p>
 <p style="">6、安装Kibana:7.6.2完整教程</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://blog.csdn.net/WoAiShuiGeGe/article/details/106647832</a></p>
 <p style="">7、docker部署elk时汉化Kibana服务</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://www.cnblogs.com/abclife/p/12697866.html</a></p>
 <p style="">8、docker logstash安装</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://blog.csdn.net/qq_33547169/article/details/86629261</a></p>
 <p style="">9、docker安装canal同步mysql8与elasticsearch7数据</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://blog.csdn.net/daziyuanazhen/article/details/106098887</a></p>
 <p style="">10、github——canal</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_32101993/article/details/100021002">https://github.com/alibaba/canal</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702300709866</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fcanal.png&amp;size=m" type="image/jpeg" length="0"/><category>ElasticSearch搜索引擎技术</category><category>Mysql技术</category><pubDate>Mon, 11 Dec 2023 13:19:00 GMT</pubDate></item><item><title><![CDATA[Spring Data Elasticsearch4.0(整合SpringBoot)]]></title><link>https://xiaoming728.com/archives/1702300487493</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Spring%20Data%20Elasticsearch4.0%28%E6%95%B4%E5%90%88SpringBoot%29&amp;url=/archives/1702300487493" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="%E7%AE%80%E4%BB%8B"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">简介</span></h1>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">Spring Data Elasticsearch是Spring Data项目下的一个子模块。<br>
  查看 Spring Data的官网：</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://projects.spring.io/spring-data/">链接</a><span style="font-size: 16px; color: rgb(77, 77, 77)"><br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fd920aa084a9eab29c181dcb4310854dc-xagj.png&amp;size=m" style="display: inline-block"></p>
<pre><code>Spring Data的使命是为数据访问提供熟悉且一致的基于Spring的编程模型，同时仍保留底层数据存储的特殊特性。
它使得使用数据访问技术，关系数据库和非关系数据库，map-reduce框架和基于云的数据服务变得容易。
这是一个总括项目，其中包含许多特定于给定数据库的子项目。
这些令人兴奋的技术项目背后，是由许多公司和开发人员合作开发的。</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">Spring Data 的使命是给各种数据访问提供统一的编程接口，不管是关系型数据库（如MySQL），还是非关系数据库（如Redis），或者类似Elasticsearch这样的索引数据库。从而简化开发人员的代码，提高开发效率。</span></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">包含很多不同数据操作的模块：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fdc06c48e136ad1d4483ab5e6936a8b6e.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  Spring Data Elasticsearch的页面：</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://projects.spring.io/spring-data-elasticsearch//">链接</a><span style="font-size: 16px; color: rgb(77, 77, 77)"><br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fb2d323a0cd42cac2a64b753b640c5eaa.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  特征：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">支持Spring的基于</span><span style="font-size: 14px; color: rgb(199, 37, 78)">@Configuration</span><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">的java配置方式，或者XML配置方式</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">提供了用于操作ES的便捷工具类**</span><span style="font-size: 14px; color: rgb(199, 37, 78)">ElasticsearchTemplate</span><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">**。包括实现文档到POJO之间的自动智能映射。</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">利用Spring的数据转换服务实现的功能丰富的对象映射</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">基于注解的元数据映射方式，而且可扩展以支持更多不同的数据格式</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">根据持久层接口自动生成对应实现方法，无需人工编写基本操作代码（类似mybatis，根据接口自动得到实现）。当然，也支持人工定制查询</span></p></li>
</ul>
<h1 style="" id="%E5%88%9B%E5%BB%BAdemo%E5%B7%A5%E7%A8%8B"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">创建Demo工程</span></h1>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">使用Spring Data Elasticsearch 版本必须对应elasticsearch服务实例,否则会出现一些报错信息<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F5827e1f470642079ed28406f76d78ad5.png&amp;size=m" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fc539568f8062aa10f57a48001b63f15c.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">新建一个demo，学习Elasticsearch<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Ff04dc0ff9a6d21044edcc7915cbeef54.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  pom核心依赖：</span></p>
<pre><code>&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-data-elasticsearch&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
  &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
  &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
  &lt;artifactId&gt;lombok&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">application.yml文件配置(没有配置用户名和密码无需输入)：</span></p>
<pre><code>spring:
   elasticsearch:
	 rest:
	    uris: localhost:9200
	    username: 
  	  password:  </code></pre>
<h1 style="" id="spring-data-elasticsearch%E6%B3%A8%E8%A7%A3"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">Spring Data Elasticsearch注解</span></h1>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">首先准备好实体类<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F211f856963ad7de25fdc103df0d909aa.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">Spring Data通过注解来声明字段的映射属性，有下面的三个注解：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(199, 37, 78)">@Document</span><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)"> 在类级别应用，以指示该类是映射到数据库的候选对象。最重要的属性是：</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">indexName：对应索引库名称</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">type：</span><s><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">映射类型。如果未设置，则使用小写的类的简单名称</span></s><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">。（从版本4.0开始不推荐使用）</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">shards：索引的分片数</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">replicas：索引的副本数</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">refreshIntervall :索引的刷新间隔。用于索引创建。默认值为“ 1s”。</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">indexStoreType: 索引的索引存储类型。用于索引创建。默认值为“ fs”。</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">createIndex: 配置是否在存储库引导中创建索引。默认值为true。</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">versionType: 版本管理的配置。默认值为EXTERNAL。</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(199, 37, 78)">@Id</span><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)"> 作用在成员变量，标记一个字段作为id主键</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(199, 37, 78)">@Transient</span><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)"> :默认情况下，存储或检索文档时，所有字段都映射到文档，此注释不包括该字段。</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(199, 37, 78)">@PersistenceConstructor</span><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">: 标记从数据库实例化对象时要使用的给定构造函数，甚至是受保护的程序包。构造函数参数按名称映射到检索到的Document中的键值。</span></p></li>
 <li>
  <p style=""><span style="font-size: 14px; color: rgb(199, 37, 78)">@Field</span><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)"> 作用在成员变量，标记为文档的字段，并指定字段映射属性：</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">type：字段类型，取值是枚举：FieldType</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">index：是否索引，布尔类型，默认是true</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">store：是否存储，布尔类型，默认是false</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">analyzer：插入是使用分词器名称</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">searchAnalyzer :查询是使用分词器</span></p></li>
</ul>
<ol>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">analyzer和search_analyzer的区别<br>
    分析器主要有两种情况会被使用：</span></p></li>
</ol>
<pre><code>第一种是插入文档时，将text类型的字段做分词然后插入倒排索引，
第二种就是在查询时，先对要查询的text类型的输入做分词，再去倒排索引搜索
如果想要让 索引 和 查询 时使用不同的分词器，ElasticSearch也是能支持的，只需要在字段上加上search_analyzer参数
在索引时，只会去看字段有没有定义analyzer，有定义的话就用定义的，没定义就用ES预设的
在查询时，会先去看字段有没有定义search_analyzer，如果没有定义，就去看有没有analyzer，再没有定义，才会去使用ES预设的</code></pre>
<h1 style="" id="elasticsearchtemplate%E7%B4%A2%E5%BC%95%E6%93%8D%E4%BD%9C"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">elasticsearchTemplate索引操作</span></h1>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">ElasticsearchTemplate中提供了创建索引并映射的API运行如下图：<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fe853c26f64b1db5ddebb67133180dc2f.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  使用kibana控制台查询映射结果:</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F144c063fcbbc3108662e7f9159ba1c0d.png&amp;size=m" style="display: inline-block"></p>
<h1 style="" id="%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">删除索引</span></h1>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">可以根据类名或索引名删除。<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F88c67a35fd836d953e5f2441107cced7.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">使用kibana控制台查询结果:</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F57e4a6798eb9c6f06b31de94cfd1fe31.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  index_not_found_exception(索引删除成功)</span></p>
<h1 style="" id="%E7%AE%80%E5%8D%95%E7%9A%84crud"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">简单的CRUD</span></h1>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">新增<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fec704848023c96a41fff40e9c164a5c3.png&amp;size=m" style="display: inline-block"></p>
<pre><code>public void saveCreate() {
    Product product = new Product(1L, "苹果手机",
                                  "手机", "苹果", 8600.00);
    //保存 第一种方式
    elasticsearchRestTemplate.save(product);
    //保存 第二种方式
    IndexQuery indexQuery = new IndexQueryBuilder()
        .withId(product.getId().toString())
        .withObject(product)
        .build();
    elasticsearchRestTemplate.index(indexQuery, IndexCoordinates.of("product"));
}</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">使用kibana控制台查询结果<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fd67b3cb8b23c1d8b2d768ceba261cc84.png&amp;size=m" style="display: inline-block"></p>
<pre><code>GET /product/_search
{
	"query": {
		"match_all": {}
	}
}</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">批量新增<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F56914003ab3f968338fe76962d203906.png&amp;size=m" style="display: inline-block"></p>
<pre><code>public void saveAllCreate() {
    List&lt;Product&gt; list = new ArrayList&lt;&gt;();
    Product product1 = new Product(1L, "小米手机",
                                   "手机", "小米", 3600.00);
    list.add(product1);
    Product product2 = new Product(2L, "苹果手机",
                                   "手机", "苹果", 8600.00);
    list.add(product2);
    Product product3 = new Product(3L, "华为手机",
                                   "手机", "华为", 8600.00);
    list.add(product3);
    Product product4 = new Product(4L, "小米电视",
                                   "电视", "小米", 18600.00);
    list.add(product4);
    Product product5 = new Product(5L, "华为电视",
                                   "电视", "华为", 28600.00);
    list.add(product5);
    elasticsearchRestTemplate.save(list);
}</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">使用kibana控制台查询结果:<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F35eaf44a022a816fd4503ec7e2cfb8c8.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  修改和新增是同一个接口，区分的依据就是id，这一点跟我们在页面发起PUT请求是类似的。<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fc82090cedb37b21ac38c350ca7f9f9e9.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  把id等于1的名称修改为</span><span style="font-size: 16px; color: rgb(0, 0, 0)">小米至尊版</span><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  运行:<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F756cfa2c353350e4536b80d9766a4f1d.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  查看结果:<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F2eb9881565e7a7a957d7a89a2e9dab64.png&amp;size=m" style="display: inline-block"></p>
<h1 style="" id="%E9%AB%98%E7%BA%A7%E6%9F%A5%E8%AF%A2"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">高级查询</span></h1>
<h2 style="" id="%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9F%A5%E8%AF%A2"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">自定义查询</span></h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">先来看最基本的match query：<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F826190f4a222d9bfef572bb1be49dafe.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  NativeSearchQueryBuilder：Spring提供的一个查询条件构建器，帮助构建json格式的请求体</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">SearchHit包含以下信息：</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">Id</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">Score 得分</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">Sort Values 排序值</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">Highlight fields 突出显示字段</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">The retrieved entity of type 检索到的类型为的实体</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">SearchHits 包含以下信息：</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">Number of total hits 总条数</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">Total hits relation 总匹配关系</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">Maximum score 最高分数</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">A list o fSearchHitobjects SearchHit对象列表</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">TReturned aggregations 返回的汇总</span></p></li>
</ul>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">官方文档:<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fcd38736880af57ef2db04d07989c1c19.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E5%88%86%E9%A1%B5%E6%9F%A5%E8%AF%A2"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">分页查询</span></h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">利用</span><span style="font-size: 14px; color: rgb(199, 37, 78)">NativeSearchQueryBuilder</span><span style="font-size: 16px; color: rgb(77, 77, 77)">可以方便的实现分页：<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F6cdbb5df321cdb9e1c9c139d5c54f7f5.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E5%AD%97%E6%AE%B5%E6%8E%92%E5%BA%8F"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">字段排序</span></h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">排序也通过</span><span style="font-size: 14px; color: rgb(199, 37, 78)">NativeSearchQueryBuilder</span><span style="font-size: 16px; color: rgb(77, 77, 77)">完成：<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fde14cb260eac234b34c54522d771648c.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E8%81%9A%E5%90%88"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">聚合</span></h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">桶就是分组，比如这里我们按照品牌brand进行分组:<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fd3ef4472f14e6b709b1dee9781897af1.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  显示的结果：<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Ff6f82c57b9d65f885eb47e8515823576.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  关键API</span><span style="font-size: 14px; color: rgb(199, 37, 78)">AggregationBuilders</span><span style="font-size: 16px; color: rgb(77, 77, 77)">：聚合的构建工厂类。所有聚合都由这个类来构建，看看他的静态方法：<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F35a2bcc8bc1f04a05fe303fd42c40f4e.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E5%B5%8C%E5%A5%97%E8%81%9A%E5%90%88%EF%BC%8C%E6%B1%82%E5%B9%B3%E5%9D%87%E5%80%BC"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">嵌套聚合，求平均值</span></h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">代码：<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fea28b4cbae818818ffb675baeb622c25.png&amp;size=m" style="display: inline-block"><span style="font-size: 16px; color: rgb(77, 77, 77)"><br>
  结果：<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fa5cd8177f0362147e988bbe4cd2ec26b.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E5%85%B3%E9%94%AE%E8%AF%8D%E9%AB%98%E4%BA%AE"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">关键词高亮</span></h2>
<pre><code>@Test
    public void testHighlight() {
        //分词字段/高亮字段
        String fieId = "name";
        // 构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // match类型查询，会把查询条件进行分词，然后进行查询,多个词条之间是or的关系
        MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("手机电视至尊", fieId);
        multiMatchQueryBuilder.type(MultiMatchQueryBuilder.Type.BEST_FIELDS);
        multiMatchQueryBuilder.tieBreaker(0.3F);
        //应该匹配的分词的最少数量
        multiMatchQueryBuilder.minimumShouldMatch("30%");
        queryBuilder.withQuery(multiMatchQueryBuilder);
        // 排序
        queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
        //生成高亮查询器
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        //高亮查询字段
        highlightBuilder.field(fieId);
        //如果要多个字段高亮,这项要为false
        highlightBuilder.requireFieldMatch(false);
        //高亮标签
        highlightBuilder.preTags("&lt;font color=\"#4F4FEC\"&gt;");
        highlightBuilder.postTags("&lt;/font&gt;");
        queryBuilder.withHighlightBuilder(highlightBuilder);
        // 执行搜索，获取结果
        SearchHits&lt;Product&gt; search = elasticsearchRestTemplate.search(queryBuilder.build(), Product.class);
        for (SearchHit&lt;Product&gt; hit : search.getSearchHits()) {
            Product content = hit.getContent();
            List&lt;String&gt; name = hit.getHighlightField(fieId);
            System.out.println(name);
            StringBuilder stringBuilder = new StringBuilder();
            for (String text : name) {
                stringBuilder.append(text);
            }
            content.setName(stringBuilder.toString());
        }
        //当前页数据
        search.getSearchHits().forEach(System.out::println);
    }</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">结果:<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fb34ca76014eab29a299a29cec5045e8b.png&amp;size=m" style="display: inline-block"></p>]]></description><guid isPermaLink="false">/archives/1702300487493</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FElasticSearch.png&amp;size=m" type="image/jpeg" length="0"/><category>ElasticSearch搜索引擎技术</category><pubDate>Mon, 11 Dec 2023 13:18:00 GMT</pubDate></item><item><title><![CDATA[Spring Data Elasticsearch 5.2]]></title><link>https://xiaoming728.com/archives/1702300418857</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Spring%20Data%20Elasticsearch%205.2&amp;url=/archives/1702300418857" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E4%B8%80%E3%80%81%E9%85%8D%E7%BD%AE%E4%BE%9D%E8%B5%96"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">一、配置依赖</span></h3>
<pre><code>&lt;dependency&gt;
	&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
	&lt;artifactId&gt;spring-boot-starter-data-elasticsearch&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<h3 style="" id="%E4%BA%8C%E3%80%81elasticsearch%E9%85%8D%E7%BD%AE"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">二、Elasticsearch配置</span></h3>
<p style="">springboot对于elasticsearch活着的其它一些工具比如kafka，redis的配置都可以在resorces/<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/wtl1992/article/details/124072854">application.properties</a>中配置，新版的配置与旧版的不太一样，<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/wtl1992/article/details/124072854">从ElasticsearchProperties.java</a>中可以看到，前缀是spring.elasticsearch，主机配置项已经变成了uris的List，<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/wtl1992/article/details/124072854">以前好像是spring.data</a>.elasticsearch.cluster-nodes，因此我们在resorces/<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/wtl1992/article/details/124072854">application.properties</a>这样子配置</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F8722c227272a682c8c5de934cd7c61b5.png&amp;size=m" style="display: inline-block"></p>
<pre><code>server:
  port: 8090
 
 
spring:
  redis:
    database: 0
    port: 6379
    host: 192.168.69.201
    password: wtl1992
 
  elasticsearch:
    password: 'XXXXXXXXXXXX'
    username: elastic
    # uris：是个以,分隔的数组
    uris: http://ljxwtl.cn:9200</code></pre>
<h3 style="" id="%E4%B8%89%E3%80%81%E9%85%8D%E7%BD%AE%E5%AE%9E%E4%BD%93%E7%B1%BB%E5%92%8Crepository"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">三、配置实体类和Repository</span></h3>
<ol>
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">在类前加上注解@Document(indexName = “discusspost”)，indexName为索引名称，新版本好像移除了分片和副本的配置。</span></p></li>
</ol>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F9e4958b00e86e8251e1606a9d4910afb-dtll.png&amp;size=m" style="display: inline-block"></p>
<ol start="2">
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">配置类的属性，和之前一样</span></p></li>
</ol>
<pre><code>@Document(indexName = "discusspost")
public class DiscussPost {
    @Id
    private int id;
    @Field(type = FieldType.Integer)
    private int userId;
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String title;
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String content;
    @Field(type = FieldType.Integer)
    private int type;
    @Field(type = FieldType.Integer)
    private int status;
    @Field(type = FieldType.Date)
    private Date createTime;
    @Field(type = FieldType.Integer)
    private int commentCount;
    @Field(type = FieldType.Double)
    private double score;</code></pre>
<ol start="3">
 <li>
  <p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">声明Repository接口，程序中注入这个接口，可以完成一些简单的操作，比如插入更新，全亮查询等</span></p></li>
</ol>
<pre><code>@Repository
public interface DiscussPostRepository extends ElasticsearchRepository&lt;DiscussPost, Integer&gt; {

}</code></pre>
<h3 style="" id="%E5%9B%9B%E3%80%81%E5%8C%B9%E9%85%8D%E6%9F%A5%E8%AF%A2"><span fontsize="" color="rgb(79, 79, 79)" style="color: rgb(79, 79, 79)">四、匹配查询</span></h3>
<p style=""><span style="font-size: 16px; color: rgb(180, 180, 180)">ElasticsearchTemplate已经过时</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F07bb57cc234245f0ad03d3d22ccdb139-irae.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">匹配查询的主要作用就是提供一个关键字和字段，将关键字分词后将ES中对应字段包含相应关键字的结果返回，主要运用在搜索功能中。</span></p>
<pre><code>	//注入elasticsearchTemplate，用它进行匹配查询，Repository功能不支持
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;
        @Test void testSearch(){

		//查询标准构建，匹配字段“content”和“title”中包含“你是谁啊”关键字的数据
		//在这里使用matches，而不是contains，contains必须包含完整的关键字，而matches不需要，如果要匹配更多字段使用or或者and。
        Criteria criteria = new Criteria("title").matches("你是谁啊").or(new Criteria("content").matches("你是谁啊"));
        //添加排序条件，下面写了两种排序条件的方式，其中第二种方式如果有多个排序条件，测试下来并不满足需求。
        //因此采用第一种并在最后构建匹配查询的时候依次添加条件。
        //yes
//        Sort sort = Sort.by(Sort.Direction.DESC,"type");
//        Sort sort2 = Sort.by(Sort.Direction.DESC,"createTime");
//        Sort sort3 = Sort.by(Sort.Direction.DESC,"createTime");
        //no
//        sort.and(Sort.by(Sort.Direction.ASC,"createTime"));
//        sort.and(Sort.sort(DiscussPost.class).by(DiscussPost::getCreateTime).ascending());
        //高亮查询
        List&lt;HighlightField&gt; highlightFieldList = new ArrayList&lt;&gt;();
        HighlightField highlightField = new HighlightField("title", HighlightFieldParameters.builder().withPreTags("&lt;em&gt;").withPostTags("&lt;/em&gt;").build());
        highlightFieldList.add(highlightField);
        highlightField = new HighlightField("content", HighlightFieldParameters.builder().withPreTags("&lt;em&gt;").withPostTags("&lt;/em&gt;").build());
        highlightFieldList.add(highlightField);

        Highlight highlight = new Highlight(highlightFieldList);
        HighlightQuery highlightQuery = new HighlightQuery(highlight,DiscussPost.class);
        //构建查询
        CriteriaQueryBuilder builder = new CriteriaQueryBuilder(criteria)
                .withSort(Sort.by(Sort.Direction.DESC,"type"))
                .withSort(Sort.by(Sort.Direction.DESC,"score"))
                .withSort(Sort.by(Sort.Direction.DESC,"createTime"))
                .withHighlightQuery(highlightQuery)
                .withPageable(PageRequest.of(0,10));
        CriteriaQuery query = new CriteriaQuery(builder);
        //通过elasticsearchTemplate查询
        SearchHits&lt;DiscussPost&gt; result = elasticsearchTemplate.search(query, DiscussPost.class);
        //处理结果
        List&lt;SearchHit&lt;DiscussPost&gt;&gt; searchHitList = result.getSearchHits();
        List&lt;DiscussPost&gt; discussPostList = new ArrayList&lt;&gt;();
        for(SearchHit&lt;DiscussPost&gt; hit:searchHitList){
            DiscussPost post = hit.getContent();
            //将高亮结果添加到返回的结果类中显示
            var titleHighlight = hit.getHighlightField("title");
            if(titleHighlight.size()!=0){
                post.setTitle(titleHighlight.get(0));
            }
            var contentHighlight = hit.getHighlightField("content");
            if(contentHighlight.size()!=0){
                post.setContent(contentHighlight.get(0));
            }
            discussPostList.add(post);

        }
        //构建Page对象
        Page&lt;DiscussPost&gt; page = new PageImpl&lt;&gt;(discussPostList,PageRequest.of(9,10), result.getTotalHits());
        for (DiscussPost post:page){
            System.out.println(post);
        }
    }</code></pre>
<h3 style="" id="%E4%BA%94%E3%80%81%E6%B5%8B%E8%AF%95%E7%A4%BA%E4%BE%8B">五、测试示例</h3>
<pre><code>package ljxwtl;
 
import ljxwtl.dao.PersonRepository;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.*;
import org.springframework.data.elasticsearch.core.clients.elasticsearch7.ElasticsearchAggregations;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * @author: wtl
 * @License: (C) Copyright 2022, wtl Corporation Limited.
 * @Contact: 1050100468@qq.com
 * @Date: 2022/4/10 7:43
 * @Version: 1.0
 * @Description:
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpringBootApplicationMain.class)
public class ApplicationMainTest {
 
    @Resource
    private PersonRepository personRepository;
 
    @Resource
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
 
    @Test
    public void test(){
        System.out.println(personRepository);
        System.out.println(elasticsearchRestTemplate);
    }
 
    @Test
    public void createIndex(){
        IndexCoordinates indexCoordinates = IndexCoordinates.of("person");
        IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(indexCoordinates);
        boolean created = indexOperations.create();
        System.out.println(created);
    }
 
    @Test
    public void setIndexMappings(){
        IndexCoordinates indexCoordinates = IndexCoordinates.of("person");
        IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(indexCoordinates);
        boolean created = indexOperations.putMapping(Person.class);
        System.out.println(created);
    }
 
    @Test
    public void createIndexWithSetSettings(){
        IndexCoordinates indexCoordinates = IndexCoordinates.of("person");
        IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(indexCoordinates);
        Map&lt;String,Object&gt; settings = new HashMap&lt;&gt;();
 
        //----------------------------------------静态设置开始----------------------------------------------
        // 静态设置：只能在索引创建时或者在状态为 closed index（闭合的索引）上设置
 
        //主分片数，默认为5.只能在创建索引时设置，不能修改
        settings.put("index.number_of_shards",2);
 
        //是否应在索引打开前检查分片是否损坏，当检查到分片损坏将禁止分片被打开
        settings.put("index.shard.check_on_startup","false");//默认值
//        settings.put("index.shard.check_on_startup","checksum");//检查物理损坏
//        settings.put("index.shard.check_on_startup","true");//检查物理和逻辑损坏，这将消耗大量内存和CPU
//        settings.put("index.shard.check_on_startup","fix");//检查物理和逻辑损坏。有损坏的分片将被集群自动删除，这可能导致数据丢失
 
        //自定义路由值可以转发的目的分片数。默认为 1，只能在索引创建时设置。此值必须小于index.number_of_shards
        settings.put("index.routing_partition_size",1);
 
        //默认使用LZ4压缩方式存储数据，也可以设置为 best_compression，它使用 DEFLATE 方式以牺牲字段存储性能为代价来获得更高的压缩比例。
        settings.put("index.codec","best_compression");
        //----------------------------------------静态设置结束----------------------------------------------
 
 
        //----------------------------------------动态设置开始----------------------------------------------
        //每个主分片的副本数。默认为 1。
        settings.put("index.number_of_replicas",0);
 
        //基于可用节点的数量自动分配副本数量,默认为 false（即禁用此功能）
        settings.put("index.auto_expand_replicas",false);
 
        //执行刷新操作的频率，这使得索引的最近更改可以被搜索。默认为 1s。可以设置为 -1 以禁用刷新。
        settings.put("index.refresh_interval","1s");
 
        //用于索引搜索的 from+size 的最大值。默认为 10000
        settings.put("index.max_result_window",10000);
 
        // 在搜索此索引中 rescore 的 window_size 的最大值
        settings.put("index.max_rescore_window",10000);
 
        //设置为 true 使索引和索引元数据为只读，false 为允许写入和元数据更改。
        settings.put("index.blocks.read_only",false);
 
        // 设置为 true 可禁用对索引的读取操作
        settings.put("index.blocks.read",false);
 
        //设置为 true 可禁用对索引的写入操作。
        settings.put("index.blocks.write",false);
 
        // 设置为 true 可禁用索引元数据的读取和写入。
        settings.put("index.blocks.metadata",false);
 
        //索引的每个分片上可用的最大刷新侦听器数
        settings.put("index.max_refresh_listeners",1000);
        //----------------------------------------动态设置结束----------------------------------------------
 
        boolean created = indexOperations.create(settings);
        System.out.println(created);
    }
 
    @Test
    public void deleteIndex(){
        IndexCoordinates indexCoordinates = IndexCoordinates.of("person");
        IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(indexCoordinates);
        boolean isDeleted = indexOperations.delete();
        System.out.println(isDeleted);
    }
 
    @Test
    public void saveOne(){
        Person person = new Person();
        person.setId("1");
        person.setName("王天龙");
        person.setAge(30);
        person.setSex("man");
        person.setTel("1111111");
        person.setCreateTime(new Date());
 
        Person savePerson = personRepository.save(person);
 
        System.out.println(savePerson);
    }
 
    @Test
    public void updateOne(){
        Document document = Document.create();
        document.setId("1");
        document.put("name","天龙战神");
 
        UpdateQuery.Builder builder = UpdateQuery.builder("1").withDocument(document).withScriptedUpsert(true);
 
        UpdateResponse updateResponse = elasticsearchRestTemplate.update(builder.build(), IndexCoordinates.of("person"));
 
        System.out.println(updateResponse.getResult());
    }
 
    @Test
    public void saveAll(){
        List&lt;Person&gt; personList = new ArrayList&lt;&gt;(3);
        Person person2 = new Person();
        person2.setId("2");
        person2.setName("王天祥");
        person2.setAge(26);
        person2.setSex("男");
        person2.setTel("222222222222");
        person2.setCreateTime(new Date());
        personList.add(person2);
 
        Person person3 = new Person();
        person3.setId("3");
        person3.setName("王杰");
        person3.setAge(31);
        person3.setSex("女");
        person3.setTel("3333333333");
        person3.setCreateTime(new Date());
        personList.add(person3);
        personRepository.saveAll(personList);
    }
 
    @Test
    public void searchMatchAll() throws IOException {
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder().must(new MatchAllQueryBuilder());
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder);
 
        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        SearchHits&lt;Person&gt; searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Person.class);
 
        searchHits.getSearchHits().forEach(personSearchHit -&gt; {
            Person content = personSearchHit.getContent();
            System.out.println(content);
        });
    }
 
 
    @Test
    public void searchBoolMustWhere(){
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.must(QueryBuilders.matchQuery("name","王天龙"));
        boolQueryBuilder.must(QueryBuilders.matchQuery("age",30));
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder);
        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        SearchHits&lt;Person&gt; searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Person.class);
 
        searchHits.forEach(personSearchHit -&gt; {
            System.out.println(personSearchHit.getContent());
        });
    }
 
    @Test
    public void searchShouldWhere(){
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.should(QueryBuilders.matchQuery("name","天龙"));
        boolQueryBuilder.should(QueryBuilders.matchQuery("age",26));
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder);
        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        SearchHits&lt;Person&gt; searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Person.class);
        searchHits.forEach(personSearchHit -&gt; {
            System.out.println(personSearchHit.getContent());
        });
    }
 
    @Test
    public void searchRange(){
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("age").gte(26).lt(31);
        boolQueryBuilder.filter(rangeQueryBuilder);
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder);
        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        SearchHits&lt;Person&gt; searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Person.class);
        searchHits.forEach(personSearchHit -&gt; {
            System.out.println(personSearchHit.getContent());
        });
    }
 
 
    /**
     * 聚合搜索
     * 聚合搜索，aggs，类似于group by，对age字段进行聚合，
     */
    @Test
    public void aggregations() {
        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                .withAggregations(AggregationBuilders.terms("count").field("sex"))
                .build();
 
        SearchHits&lt;Person&gt; searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Person.class);
        //取出聚合结果
        ElasticsearchAggregations elasticsearchAggregations = (ElasticsearchAggregations) searchHits.getAggregations();
        Aggregations aggregations = elasticsearchAggregations.aggregations();
        Terms terms = (Terms) aggregations.asMap().get("count");
 
        for (Terms.Bucket bucket : terms.getBuckets()) {
            String keyAsString = bucket.getKeyAsString();   // 聚合字段列的值
            long docCount = bucket.getDocCount();           // 聚合字段对应的数量
            System.out.println(keyAsString + " " + docCount);
        }
    }
 
    /**
     * 分页实现
     */
    @Test
    public void searchWithPageable(){
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder().must(new MatchAllQueryBuilder());
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder)
                //分页实现
                .withPageable(PageRequest.of(0,2));
 
        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        SearchHits&lt;Person&gt; searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Person.class);
 
        searchHits.getSearchHits().forEach(personSearchHit -&gt; {
            Person content = personSearchHit.getContent();
            System.out.println(content);
        });
    }
 
    /**
     * 排序实现
     */
    @Test
    public void searchWithSort(){
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder().must(new MatchAllQueryBuilder());
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder)
                //分页实现
                .withPageable(PageRequest.of(0,10))
                //排序
                .withSorts(SortBuilders.fieldSort("createTime").order(SortOrder.DESC),SortBuilders.fieldSort("age").order(SortOrder.DESC));
 
        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        SearchHits&lt;Person&gt; searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Person.class);
 
        searchHits.getSearchHits().forEach(personSearchHit -&gt; {
            Person content = personSearchHit.getContent();
            System.out.println(content);
        });
    }
 
    //高并发场景下模拟ES分布式悲观锁的实现
    @Test
    public void highConcurrencyWithPessimisticLock() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
 
        AtomicInteger failedCount = new AtomicInteger(0);
 
        /**
         * 在testindex未创建的前提下，并发情况下，只能有一个成功，19个失败
         */
        for (int i = 0; i &lt; 20; i++) {
            new Thread(new Task(countDownLatch,failedCount,elasticsearchRestTemplate)).start();
        }
 
        countDownLatch.countDown();
 
        TimeUnit.SECONDS.sleep(10);
 
        System.out.println(failedCount.get());
    }
 
    private class Task implements Runnable {
 
        private CountDownLatch countDownLatch;
        private AtomicInteger failedCount;
        private ElasticsearchRestTemplate elasticsearchRestTemplate;
 
        public Task(CountDownLatch countDownLatch,AtomicInteger failedCount,ElasticsearchRestTemplate elasticsearchRestTemplate){
            this.countDownLatch = countDownLatch;
            this.failedCount = failedCount;
            this.elasticsearchRestTemplate = elasticsearchRestTemplate;
        }
 
        @Override
        public void run() {
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                IndexCoordinates indexCoordinates = IndexCoordinates.of("testindex");
                IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(indexCoordinates);
                boolean created = indexOperations.create();
                System.out.println(created);
            } catch (Exception e) {
                failedCount.getAndIncrement();
            }
        }
    }
 
}</code></pre>
<blockquote>
 <h3 style="" id="%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</h3>
 <p style="">Elasticsearch8.2 版本的Spring Data Elasticsearch的API使用：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/wtl1992/article/details/124072854">https://blog.csdn.net/wtl1992/article/details/124072854</a></p>
 <p style=""><span fontsize="" color="rgb(34, 34, 38)" style="color: rgb(34, 34, 38)">Springboot3.1+Elasticsearch8.x匹配查询</span>: <a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/wtl1992/article/details/124072854">https://blog.csdn.net/weixin_43189060/article/details/131247199</a></p>
</blockquote>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702300418857</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FElasticSearch.png&amp;size=m" type="image/jpeg" length="0"/><category>ElasticSearch搜索引擎技术</category><pubDate>Mon, 11 Dec 2023 13:14:00 GMT</pubDate></item><item><title><![CDATA[Redis 核心篇：唯快不破的秘密]]></title><link>https://xiaoming728.com/archives/1702300068487</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Redis%20%E6%A0%B8%E5%BF%83%E7%AF%87%EF%BC%9A%E5%94%AF%E5%BF%AB%E4%B8%8D%E7%A0%B4%E7%9A%84%E7%A7%98%E5%AF%86&amp;url=/archives/1702300068487" width="1" height="1" alt="" style="opacity:0;">
<p style="">系统观其实是至关重要的，从某种程度上说，在解决问题时，拥有了系统观，就意味着你能有依据、有章法地定位和解决问题。</p>
<h2 style="" id="redis-%E5%85%A8%E6%99%AF%E5%9B%BE"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">Redis 全景图</span></h2>
<p style="">全景图可以围绕两个维度展开，分别是：</p>
<p style=""><strong>应用维度：缓存使用、集群运用、数据结构的巧妙使用</strong></p>
<p style=""><strong>系统维度：可以归类为三高</strong></p>
<ol>
 <li>
  <p style="">高性能：线程模型、网络 IO 模型、数据结构、持久化机制；</p></li>
 <li>
  <p style="">高可用：主从复制、哨兵集群、Cluster 分片集群；</p></li>
 <li>
  <p style="">高拓展：负载均衡</p></li>
</ol>
<p style=""><strong>Redis 系列篇章</strong>围绕如下思维导图展开，这次从 <strong>《Redis 唯快不破的秘密》一起探索 Redis 的核心知识点。</strong></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F5ae69488b1c901ac36f9266e9fd26499.png&amp;size=m" style="display: inline-block">吃透Redis</p>
<h2 style="" id="%E5%94%AF%E5%BF%AB%E4%B8%8D%E7%A0%B4%E7%9A%84%E7%A7%98%E5%AF%86"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">唯快不破的秘密</span></h2>
<p style="">65 哥前段时间去面试 996 大厂，被问到「Redis 为什么快？」</p>
<blockquote>
 <p style=""><strong>“</strong></p>
 <p style="">65 哥：额，因为它是基于内存实现和单线程模型</p>
 <p style=""><strong>”</strong></p>
</blockquote>
<p style="">面试官：还有呢？</p>
<blockquote>
 <p style=""><strong>“</strong></p>
 <p style="">65 哥：没了呀。</p>
 <p style=""><strong>”</strong></p>
</blockquote>
<p style="">很多人仅仅只是知道基于内存实现，其他核心的原因模凌两可。今日跟着「码哥字节」一起探索真正快的原因，做一个唯快不破的真男人！</p>
<p style="">Redis 为了高性能，从各方各面都进行了优化，下次小伙伴们面试的时候，面试官问 Redis 性能为什么如此高，可不能傻傻的只说单线程和内存存储了。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fe4dc1dabf7bcf68fb7615a455c0e7d42.png&amp;size=m" style="display: inline-block">唯快不破的秘密</p>
<p style="">根据官方数据，Redis 的 QPS 可以达到约 100000（每秒请求数），有兴趣的可以参考官方的基准程序测试《How fast is Redis？》，地址：<a target="_blank" rel="noopener noreferrer nofollow" href="https://redis.io/topics/benchmarks">https://redis.io/topics/benchmarks</a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F9bb5e0256b537b357a8acbe665f3629c.png&amp;size=m" style="display: inline-block">基准测试</p>
<p style="">横轴是连接数，纵轴是 QPS。此时，这张图反映了一个数量级，希望大家在面试的时候可以正确的描述出来，不要问你的时候，你回答的数量级相差甚远！</p>
<h2 style="" id="%E5%AE%8C%E5%85%A8%E5%9F%BA%E4%BA%8E%E5%86%85%E5%AD%98%E5%AE%9E%E7%8E%B0"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">完全基于内存实现</span></h2>
<blockquote>
 <p style=""><strong>“</strong></p>
 <p style="">65 哥：这个我知道，Redis 是基于内存的数据库，跟磁盘数据库相比，完全吊打磁盘的速度，就像段誉的凌波微步。对于磁盘数据库来说，首先要将数据通过 IO 操作读取到内存里。</p>
 <p style=""><strong>”</strong></p>
</blockquote>
<p style="">没错，不论读写操作都是在内存上完成的，我们分别对比下内存操作与磁盘操作的差异。</p>
<p style=""><strong>磁盘调用栈图</strong></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fe4dc1dabf7bcf68fb7615a455c0e7d42-rkft.png&amp;size=m" style="display: inline-block"></p>
<p style=""><strong>内存操作</strong></p>
<p style="">内存直接由 CPU 控制，也就是 CPU 内部集成的内存控制器，所以说内存是直接与 CPU 对接，享受与 CPU 通信的最优带宽。</p>
<p style=""><strong>Redis 将数据存储在内存中，读写操作不会因为磁盘的 IO 速度限制，所以速度飞一般的感觉！</strong></p>
<p style="">最后以一张图量化系统的各种延时时间（部分数据引用 Brendan Gregg）</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F9bb5e0256b537b357a8acbe665f3629c-kzmn.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E9%AB%98%E6%95%88%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">高效的数据结构</span></h2>
<blockquote>
 <p style=""><strong>“</strong></p>
 <p style="">65 哥：学习 MySQL 的时候我知道为了提高检索速度使用了 B+ Tree 数据结构，所以 Redis 速度快应该也跟数据结构有关。</p>
 <p style=""><strong>”</strong></p>
</blockquote>
<p style="">回答正确，这里所说的数据结构并不是 Redis 提供给我们使用的 5 种数据类型：String、List、Hash、Set、SortedSet。</p>
<p style="">在 Redis 中，常用的 5 种数据类型和应用场景如下：</p>
<ul>
 <li>
  <p style=""><strong>String：</strong> 缓存、计数器、分布式锁等。</p></li>
 <li>
  <p style=""><strong>List：</strong> 链表、队列、微博关注人时间轴列表等。</p></li>
 <li>
  <p style=""><strong>Hash：</strong> 用户信息、Hash 表等。</p></li>
 <li>
  <p style=""><strong>Set：</strong> 去重、赞、踩、共同好友等。</p></li>
 <li>
  <p style=""><strong>Zset：</strong> 访问量排行榜、点击量排行榜等。</p></li>
</ul>
<p style="">上面的应该叫做 Redis 支持的数据类型，也就是数据的保存形式。「码哥字节」要说的是针对这 5 种数据类型，底层都运用了哪些高效的数据结构来支持。</p>
<blockquote>
 <p style=""><strong>“</strong></p>
 <p style="">65 哥：为啥搞这么多数据结构呢？</p>
 <p style=""><strong>”</strong></p>
</blockquote>
<p style="">当然是为了追求速度，不同数据类型使用不同的数据结构速度才得以提升。每种数据类型都有一种或者多种数据结构来支撑，底层数据结构有 6 种。</p>
<p style=""></p>
<h3 style="" id="redis-hash-%E5%AD%97%E5%85%B8"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">Redis hash 字典</span></h3>
<p style="">Redis 整体就是一个 哈希表来保存所有的键值对，无论数据类型是 5 种的任意一种。哈希表，本质就是一个数组，每个元素被叫做哈希桶，不管什么数据类型，每个桶里面的 entry 保存着实际具体值的指针。</p>
<p style="">Redis 全局哈希表</p>
<p style="">整个数据库就是一个<strong>全局哈希表</strong>，而哈希表的时间复杂度是 O(1)，只需要计算每个键的哈希值，便知道对应的哈希桶位置，定位桶里面的 entry 找到对应数据，这个也是 Redis 快的原因之一。</p>
<p style=""><strong>那 Hash 冲突怎么办？</strong></p>
<p style="">当写入 Redis 的数据越来越多的时候，哈希冲突不可避免，会出现不同的 key 计算出一样的哈希值。</p>
<p style="">Redis 通过<strong>链式哈希</strong>解决冲突：<strong>也就是同一个 桶里面的元素使用链表保存</strong>。但是当链表过长就会导致查找性能变差可能，所以 Redis 为了追求快，使用了两个全局哈希表。用于 rehash 操作，增加现有的哈希桶数量，减少哈希冲突。</p>
<p style="">开始默认使用 hash 表 1 保存键值对数据，哈希表 2 此刻没有分配空间。当数据越来多触发 rehash 操作，则执行以下操作：</p>
<ol>
 <li>
  <p style="">给 hash 表 2 分配更大的空间；</p></li>
 <li>
  <p style="">将 hash 表 1 的数据重新映射拷贝到 hash 表 2 中；</p></li>
 <li>
  <p style="">释放 hash 表 1 的空间。</p></li>
</ol>
<p style=""><strong>值得注意的是，将 hash 表 1 的数据重新映射到 hash 表 2 的过程中并不是一次性的，这样会造成 Redis 阻塞，无法提供服务。</strong></p>
<p style="">而是采用了<strong>渐进式 rehash</strong>，每次处理客户端请求的时候，先从 hash 表 1 中第一个索引开始，将这个位置的 所有数据拷贝到 hash 表 2 中，就这样将 rehash 分散到多次请求过程中，避免耗时阻塞。</p>
<h3 style="" id="sds-%E7%AE%80%E5%8D%95%E5%8A%A8%E6%80%81%E5%AD%97%E7%AC%A6"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">SDS 简单动态字符</span></h3>
<blockquote>
 <p style=""><strong>“</strong></p>
 <p style="">65 哥：Redis 是用 C 语言实现的，为啥还重新搞一个 SDS 动态字符串呢？</p>
 <p style=""><strong>”</strong></p>
</blockquote>
<p style="">字符串结构使用最广泛，通常我们用于缓存登陆后的用户信息，key = userId，value = 用户信息 JSON 序列化成字符串。</p>
<p style="">C 语言中字符串的获取 「MageByte」的长度，要从头开始遍历，直到 「\0」为止，Redis 作为唯快不破的男人是不能忍受的。</p>
<p style="">C 语言字符串结构与 SDS 字符串结构对比图如下所示：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Ffdb3daf71d912b732af4c7158bfbaa98.png&amp;size=m" style="display: inline-block">C 语言字符串与 SDS</p>
<h4 style="" id="sds-%E4%B8%8E-c-%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%BA%E5%88%AB">SDS 与 C 字符串区别</h4>
<p style=""><strong>O(1) 时间复杂度获取字符串长度</strong></p>
<p style="">C 语言字符串布吉路长度信息，需要遍历整个字符串时间复杂度为 O(n)，C 字符串遍历时遇到 '\0' 时结束。</p>
<p style="">SDS 中 len 保存这字符串的长度，O(1) 时间复杂度。</p>
<p style=""><strong>空间预分配</strong></p>
<p style="">SDS 被修改后，程序不仅会为 SDS 分配所需要的必须空间，还会分配额外的未使用空间。</p>
<p style="">分配规则如下：如果对 SDS 修改后，len 的长度小于 1M，那么程序将分配和 len 相同长度的未使用空间。举个例子，如果 len=10，重新分配后，buf 的实际长度会变为 10(已使用空间)+10(额外空间)+1(空字符)=21。如果对 SDS 修改后 len 长度大于 1M，那么程序将分配 1M 的未使用空间。</p>
<p style=""><strong>惰性空间释放</strong></p>
<p style="">当对 SDS 进行缩短操作时，程序并不会回收多余的内存空间，而是使用 free 字段将这些字节数量记录下来不释放，后面如果需要 append 操作，则直接使用 free 中未使用的空间，减少了内存的分配。</p>
<p style=""><strong>二进制安全</strong></p>
<p style="">在 Redis 中不仅可以存储 String 类型的数据，也可能存储一些二进制数据。</p>
<p style="">二进制数据并不是规则的字符串格式，其中会包含一些特殊的字符如 '\0'，在 C 中遇到 '\0' 则表示字符串的结束，但在 SDS 中，标志字符串结束的是 len 属性。</p>
<h3 style="" id="ziplist-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">zipList 压缩列表</span></h3>
<p style="">压缩列表是 List 、hash、 sorted Set 三种数据类型底层实现之一。</p>
<p style="">当一个列表只有少量数据的时候，并且每个列表项要么就是小整数值，要么就是长度比较短的字符串，那么 Redis 就会使用压缩列表来做列表键的底层实现。</p>
<p style="">ziplist 是由一系列特殊编码的连续内存块组成的顺序型的数据结构，ziplist 中可以包含多个 entry 节点，每个节点可以存放整数或者字符串。</p>
<p style="">ziplist 在表头有三个字段 zlbytes、zltail 和 zllen，分别表示列表占用字节数、列表尾的偏移量和列表中的 entry 个数；压缩列表在表尾还有一个 zlend，表示列表结束。</p>
<pre><code>struct ziplist&lt;T&gt; {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量，用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表，挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束，值恒为 0xFF
}</code></pre>
<p style="">ziplist</p>
<p style="">如果我们要查找定位第一个元素和最后一个元素，可以通过表头三个字段的长度直接定位，复杂度是 O(1)。而查找其他元素时，就没有这么高效了，只能逐个查找，此时的复杂度就是 O(N)</p>
<h3 style="" id="%E5%8F%8C%E7%AB%AF%E5%88%97%E8%A1%A8"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">双端列表</span></h3>
<p style="">Redis List 数据类型通常被用于队列、微博关注人时间轴列表等场景。不管是先进先出的队列，还是先进后出的栈，双端列表都很好的支持这些特性。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F00e370fed1ac8e770bb0c538e7d8d522.png&amp;size=m" style="display: inline-block"></p>
<p style="">Redis 的链表实现的特性可以总结如下：</p>
<ul>
 <li>
  <p style="">双端：链表节点带有 prev 和 next 指针，获取某个节点的前置节点和后置节点的复杂度都是 O（1）。</p></li>
 <li>
  <p style="">无环：表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL，对链表的访问以 NULL 为终点。</p></li>
 <li>
  <p style="">带表头指针和表尾指针：通过 list 结构的 head 指针和 tail 指针，程序获取链表的表头节点和表尾节点的复杂度为 O（1）。</p></li>
 <li>
  <p style="">带链表长度计数器：程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数，程序获取链表中节点数量的复杂度为 O（1）。</p></li>
 <li>
  <p style="">多态：链表节点使用 void* 指针来保存节点值，并且可以通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数，所以链表可以用于保存各种不同类型的值。</p></li>
</ul>
<p style="">后续版本对列表数据结构进行了改造，使用 quicklist 代替了 ziplist 和 linkedlist。</p>
<p style=""><strong>quicklist 是 ziplist 和 linkedlist 的混合体，它将 linkedlist 按段切分，每一段使用 ziplist 来紧凑存储，多个 ziplist 之间使用双向指针串接起来。</strong></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F0d52c0924f4e3c409bff59aa91ae8eb9.png&amp;size=m" style="display: inline-block"></p>
<p style="">这也是为何 Redis 快的原因，不放过任何一个可以提升性能的细节。</p>
<h3 style="" id="skiplist-%E8%B7%B3%E8%B7%83%E8%A1%A8"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">skipList 跳跃表</span></h3>
<p style="">sorted set 类型的排序功能便是通过「跳跃列表」数据结构来实现。</p>
<p style="">跳跃表（skiplist）是一种有序数据结构，它通过在每个节点中维持多个指向其他节点的指针，从而达到快速访问节点的目的。</p>
<p style="">跳跃表支持平均 O（logN）、最坏 O（N）复杂度的节点查找，还可以通过顺序性操作来批量处理节点。</p>
<p style="">跳表在链表的基础上，增加了多层级索引，通过索引位置的几个跳转，实现数据的快速定位，如下图所示：</p>
<p style="">跳跃表</p>
<p style="">当需要查找 40 这个元素需要经历 三次查找。</p>
<h3 style="" id="%E6%95%B4%E6%95%B0%E6%95%B0%E7%BB%84%EF%BC%88intset%EF%BC%89"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">整数数组（intset）</span></h3>
<p style="">当一个集合只包含整数值元素，并且这个集合的元素数量不多时，Redis 就会使用整数集合作为集合键的底层实现。结构如下：</p>
<pre><code>typedef struct intset{
     //编码方式
     uint32_t encoding;
     //集合包含的元素数量
     uint32_t length;
     //保存元素的数组
     int8_t contents[];
}intset;</code></pre>
<p style="">contents 数组是整数集合的底层实现：整数集合的每个元素都是 contents 数组的一个数组项（item），各个项在数组中按值的大小从小到大有序地排列，并且数组中不包含任何重复项。length 属性记录了整数集合包含的元素数量，也即是 contents 数组的长度。</p>
<h3 style="" id="%E5%90%88%E7%90%86%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BC%96%E7%A0%81"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">合理的数据编码</span></h3>
<p style="">Redis 使用对象（redisObject）来表示数据库中的键值，当我们在 Redis 中创建一个键值对时，至少创建两个对象，一个对象是用做键值对的键对象，另一个是键值对的值对象。</p>
<p style="">例如我们执行 SET MSG XXX 时，键值对的键是一个包含了字符串“MSG“的对象，键值对的值对象是包含字符串"XXX"的对象。</p>
<p style=""><strong>redisObject</strong></p>
<pre><code>typedef struct redisObject{
    //类型
   unsigned type:4;
   //编码
   unsigned encoding:4;
   //指向底层数据结构的指针
   void *ptr;
    //...
 }robj;</code></pre>
<p style="">其中 type 字段记录了对象的类型，包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。</p>
<p style="">对于每一种数据类型来说，底层的支持可能是多种数据结构，什么时候使用哪种数据结构，这就涉及到了编码转化的问题。</p>
<p style="">那我们就来看看，不同的数据类型是如何进行编码转化的：</p>
<p style=""><strong>String</strong>：存储数字的话，采用 int 类型的编码，如果是非数字的话，采用 raw 编码；</p>
<p style=""><strong>List</strong>：List 对象的编码可以是 ziplist 或 linkedlist，字符串长度 &lt; 64 字节且元素个数 &lt; 512 使用 ziplist 编码，否则转化为 linkedlist 编码；</p>
<p style="">注意：这两个条件是可以修改的，在 redis.conf 中：</p>
<pre><code>list-max-ziplist-entries 512
list-max-ziplist-value 64</code></pre>
<p style=""><strong>Hash</strong>：Hash 对象的编码可以是 ziplist 或 hashtable。</p>
<p style="">当 Hash 对象同时满足以下两个条件时，Hash 对象采用 ziplist 编码：</p>
<ul>
 <li>
  <p style=""><strong>Hash 对象保存的所有键值对的键和值的字符串长度均小于 64 字节。</strong></p></li>
 <li>
  <p style=""><strong>Hash 对象保存的键值对数量小于 512 个。</strong></p></li>
</ul>
<p style="">否则就是 hashtable 编码。</p>
<p style=""><strong>Set</strong>：Set 对象的编码可以是 intset 或 hashtable，intset 编码的对象使用整数集合作为底层实现，把所有元素都保存在一个整数集合里面。</p>
<p style="">保存元素为整数且元素个数小于一定范围使用 intset 编码，任意条件不满足，则使用 hashtable 编码；</p>
<p style=""><strong>Zset</strong>：Zset 对象的编码可以是 ziplist 或 zkiplist，当采用 ziplist 编码存储时，每个集合元素使用两个紧挨在一起的压缩列表来存储。</p>
<p style="">Ziplist 压缩列表第一个节点存储元素的成员，第二个节点存储元素的分值，并且按分值大小从小到大有序排列。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2F2a4c46879ba6b37f09b390efb88e9f7d.png&amp;size=m" style="display: inline-block"></p>
<p style="">当 Zset 对象同时满足一下两个条件时，采用 ziplist 编码：</p>
<ul>
 <li>
  <p style=""><strong>Zset 保存的元素个数小于 128。</strong></p></li>
 <li>
  <p style=""><strong>Zset 元素的成员长度都小于 64 字节。</strong></p></li>
</ul>
<p style="">如果不满足以上条件的任意一个，ziplist 就会转化为 zkiplist 编码。注意：这两个条件是可以修改的，在 redis.conf 中：</p>
<pre><code>zset-max-ziplist-entries 128
zset-max-ziplist-value 64</code></pre>
<h2 style="" id="%E5%8D%95%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">单线程模型</span></h2>
<blockquote>
 <p style=""><strong>“</strong></p>
 <p style="">65 哥：为什么 Redis 是单线程的而不用多线程并行执行充分利用 CPU 呢？</p>
 <p style=""><strong>”</strong></p>
</blockquote>
<p style=""><strong>我们要明确的是：Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。</strong> 对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。</p>
<p style="">至于为啥用单线程，我们先了解多线程有什么缺点。</p>
<h3 style="" id="%E5%A4%9A%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%BC%8A%E7%AB%AF"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">多线程的弊端</span></h3>
<p style="">使用多线程，通常可以增加系统吞吐量，充分利用 CPU 资源。</p>
<p style="">但是，使用多线程后，没有良好的系统设计，可能会出现如下图所示的场景，增加了线程数量，前期吞吐量会增加，再进一步新增线程的时候，系统吞吐量几乎不再新增，甚至会下降！</p>
<p style="">线程数与吞吐量</p>
<p style="">在运行每个任务之前，CPU 需要知道任务在何处加载并开始运行。也就是说，系统需要帮助它预先设置 CPU 寄存器和程序计数器，这称为 CPU 上下文。</p>
<p style="">这些保存的上下文存储在系统内核中，并在重新计划任务时再次加载。这样，任务的原始状态将不会受到影响，并且该任务将看起来正在连续运行。</p>
<p style=""><strong>切换上下文时，我们需要完成一系列工作，这是非常消耗资源的操作。</strong></p>
<p style="">另外，当多线程并行修改共享数据的时候，为了保证数据正确，需要加锁机制就会带来额外的性能开销，面临的共享资源的并发访问控制问题。</p>
<p style="">引入多线程开发，就需要使用同步原语来保护共享资源的并发读写，增加代码复杂度和调试难度。</p>
<h3 style="" id="%E5%8D%95%E7%BA%BF%E7%A8%8B%E5%8F%88%E4%BB%80%E4%B9%88%E5%A5%BD%E5%A4%84%EF%BC%9F"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">单线程又什么好处？</span></h3>
<ol>
 <li>
  <p style="">不会因为线程创建导致的性能消耗；</p></li>
 <li>
  <p style="">避免上下文切换引起的 CPU 消耗，没有多线程切换的开销；</p></li>
 <li>
  <p style="">避免了线程之间的竞争问题，比如添加锁、释放锁、死锁等，不需要考虑各种锁问题。</p></li>
 <li>
  <p style="">代码更清晰，处理逻辑简单。</p></li>
</ol>
<p style=""><strong>单线程是否没有充分利用 CPU 资源呢？</strong></p>
<p style="">官方答案：因为 Redis 是基于内存的操作，CPU 不是 Redis 的瓶颈，Redis 的瓶颈最<strong>有可能是机器内存的大小或者网络带宽</strong>。既然单线程容易实现，而且 CPU 不会成为瓶颈，那就顺理成章地采用单线程的方案了。原文地址：<a target="_blank" rel="noopener noreferrer nofollow" href="https://redis.io/topics/benchmarks">https://redis.io/topics/faq。</a></p>
<h2 style="" id="i%2Fo-%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E6%A8%A1%E5%9E%8B"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">I/O 多路复用模型</span></h2>
<p style="">Redis 采用 I/O 多路复用技术，并发处理连接。采用了 epoll + 自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件，然后利用 epoll 的多路复用特性，绝不在 IO 上浪费一点时间。</p>
<blockquote>
 <p style=""><strong>“</strong></p>
 <p style="">65 哥：那什么是 I/O 多路复用呢？</p>
 <p style=""><strong>”</strong></p>
</blockquote>
<p style="">在解释 IO 多虑复用之前我们先了解下基本 IO 操作会经历什么。</p>
<p style=""><strong>基本 IO 模型</strong></p>
<p style="">一个基本的网络 IO 模型，当处理 get 请求，会经历以下过程：</p>
<ol>
 <li>
  <p style="">和客户端建立建立 <code>accept</code>;</p></li>
 <li>
  <p style="">从 socket 种读取请求 <code>recv</code>;</p></li>
 <li>
  <p style="">解析客户端发送的请求 <code>parse</code>;</p></li>
 <li>
  <p style="">执行 <code>get</code> 指令；</p></li>
 <li>
  <p style="">响应客户端数据，也就是 向 socket 写回数据。</p></li>
</ol>
<p style="">其中，bind/listen、accept、recv、parse 和 send 属于网络 IO 处理，而 get 属于键值数据操作。既然 Redis 是单线程，那么，最基本的一种实现是在一个线程中依次执行上面说的这些操作。</p>
<p style="">关键点就是 <strong>accept 和 recv 会出现阻塞</strong>，当 Redis 监听到一个客户端有连接请求，但一直未能成功建立起连接时，会阻塞在 accept() 函数这里，导致其他客户端无法和 Redis 建立连接。</p>
<p style="">类似的，当 Redis 通过 recv() 从一个客户端读取数据时，如果数据一直没有到达，Redis 也会一直阻塞在 recv()。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fd5ace986de00052f340cd13ccb664ef2.png&amp;size=m" style="display: inline-block"></p>
<p style="">阻塞的原因由于使用传统阻塞 IO ，也就是在执行 read、accept 、recv 等网络操作会一直阻塞等待。如下图所示：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fb5128ec15b6153d1daccb52c97305e55.png&amp;size=m" style="display: inline-block">阻塞IO</p>
<p style=""><strong>IO 多路复用</strong></p>
<p style=""><strong>多路</strong>指的是多个 socket 连接，<strong>复用</strong>指的是复用一个线程。多路复用主要有三种技术：select，poll，epoll。epoll 是最新的也是目前最好的多路复用技术。</p>
<p style=""><strong>它的基本原理是，内核不是监视应用程序本身的连接，而是监视应用程序的文件描述符。</strong></p>
<p style="">当客户端运行时，它将生成具有不同事件类型的套接字。在服务器端，I / O 多路复用程序（I / O 多路复用模块）会将消息放入队列（也就是 下图的 I/O 多路复用程序的 socket 队列），然后通过文件事件分派器将其转发到不同的事件处理器。</p>
<p style="">简单来说：Redis 单线程情况下，内核会一直监听 socket 上的连接请求或者数据请求，一旦有请求到达就交给 Redis 线程处理，这就实现了一个 Redis 线程处理多个 IO 流的效果。</p>
<p style="">select/epoll 提供了基于事件的回调机制，即针对不同事件的发生，调用相应的事件处理器。所以 Redis 一直在处理事件，提升 Redis 的响应性能。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fbfccfe72edde0f51f38c1f8cfe23bcee.png&amp;size=m" style="display: inline-block">高性能 IO 多路复用</p>
<p style="">Redis 线程不会阻塞在某一个特定的监听或已连接套接字上，也就是说，不会阻塞在某一个特定的客户端请求处理上。正因为此，Redis 可以同时和多个客户端连接并处理请求，从而提升并发性。</p>
<h2 style="" id="%E5%94%AF%E5%BF%AB%E4%B8%8D%E7%A0%B4%E7%9A%84%E5%8E%9F%E7%90%86%E6%80%BB%E7%BB%93"><span fontsize="" color="rgb(34, 34, 34)" style="color: rgb(34, 34, 34)">唯快不破的原理总结</span></h2>
<blockquote>
 <p style=""><strong>“</strong></p>
 <p style="">65 哥：学完之后我终于知道 Redis 为何快的本质原因了，「码哥」你别说话，我来总结！一会我再点赞和分享这篇文章，让更多人知道 Redis 快的核心原理。</p>
 <p style=""><strong>”</strong></p>
</blockquote>
<ol>
 <li>
  <p style="">纯内存操作，一般都是简单的存取操作，线程占用的时间很多，时间的花费主要集中在 IO 上，所以读取速度快。</p></li>
 <li>
  <p style="">整个 Redis 就是一个全局 哈希表，他的时间复杂度是 O(1)，而且为了防止哈希冲突导致链表过长，Redis 会执行 rehash 操作，扩充 哈希桶数量，减少哈希冲突。并且防止一次性 重新映射数据过大导致线程阻塞，采用 渐进式 rehash。巧妙的将一次性拷贝分摊到多次请求过程后总，避免阻塞。</p></li>
 <li>
  <p style="">Redis 使用的是非阻塞 IO：IO 多路复用，使用了单线程来轮询描述符，将数据库的开、关、读、写都转换成了事件，Redis 采用自己实现的事件分离器，效率比较高。</p></li>
 <li>
  <p style="">采用单线程模型，保证了每个操作的原子性，也减少了线程的上下文切换和竞争。</p></li>
 <li>
  <p style="">Redis 全程使用 hash 结构，读取速度快，还有一些特殊的数据结构，对数据存储进行了优化，如压缩表，对短数据进行压缩存储，再如，跳表，使用有序的数据结构加快读取的速度。</p></li>
 <li>
  <p style="">根据实际存储的数据类型选择不同编码</p></li>
</ol>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702300068487</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fredis.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Redis技术</category><pubDate>Mon, 11 Dec 2023 13:13:00 GMT</pubDate></item><item><title><![CDATA[编译redis6报错/usr/bin/ld: cannot find -latomic解决方法]]></title><link>https://xiaoming728.com/archives/1702300021262</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E7%BC%96%E8%AF%91redis6%E6%8A%A5%E9%94%99%2Fusr%2Fbin%2Fld%3A%20cannot%20find%20-latomic%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95&amp;url=/archives/1702300021262" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">来源：博客园-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.cnblogs.com/mafy/">天妖姥爷</a></p>
 <p style="">日期：2020-07-26 14:25</p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">https://www.cnblogs.com/mafy/p/13380332.html</a></p>
</blockquote>
<p style=""></p>
<p style="">latomic代表的是 <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">libatomic.so</a>，也即是在编译的过程中，<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">需要libatomic.so</a>库，而系统又找不到这个库；</p>
<p style="">解决方法是手动找到这个库，可能存在如下几种情况：</p>
<p style="">1、<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">系统中没有安装依赖库libatomic.so</a>，通过执行yum -y install&nbsp;<em>atomic</em>安装相关依赖，安装后执行rpm -qa | grep atomic查看是否安装了相关依赖</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-bido.png&amp;size=m" style="display: inline-block"></p>
<p style="">2、<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">系统可能已经存在libatomic.so</a>，但是不在默认查找路径(/usr/lib之类的路径)，而是在其他路径中。</p>
<p style="">3、<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">系统中没有libatomic.so</a>这个库，但是找到类似 <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">libatomic.so</a>.1.0 这种带版本号的库，通过执行find / -name "<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">libatomic.so</a>*"来确认</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-rjff.png&amp;size=m" style="display: inline-block"></p>
<p style="">【解决方法】</p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">使用软链接的方法将依赖库libatomic.so</a>链接到正确路径，执行如下命令：</p>
<pre><code>sudo ln -s /usr/lib64/libatomic.so.1.2.0 /usr/lib/libatomic.so</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-pbeo.png&amp;size=m" style="display: inline-block"></p>
<p style="">然后再执行make编译即可正常编译通过；</p>
<p style=""></p>
<blockquote>
 <p style="">相关资料：</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/mafy/">https://bbs.huaweicloud.com/forum/thread-59424-1-1.html</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702300021262</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fredis.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Redis技术</category><pubDate>Mon, 11 Dec 2023 13:07:00 GMT</pubDate></item><item><title><![CDATA[RocketMQ消息幂等的通用解决方案]]></title><link>https://xiaoming728.com/archives/1702299962742</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=RocketMQ%E6%B6%88%E6%81%AF%E5%B9%82%E7%AD%89%E7%9A%84%E9%80%9A%E7%94%A8%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88&amp;url=/archives/1702299962742" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">来源：微信公众账号-架构师</p>
 <p style="">日期：2月14日</p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzAwNjQwNzU2NQ==&amp;amp;mid=2650347372&amp;amp;idx=1&amp;amp;sn=97a0803defeadc67931e65dc4d754a6a&amp;amp;chksm=8300638eb477ea983563bea2f1bb226b65b77581b44eea35f12775df411ee815cd4f3b703138&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0420f8llIzJfeWUhseaV0MTc&amp;amp;sharer_sharetime=1618909377947&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd">https://mp.weixin.qq.com/s?__biz=MzAwNjQwNzU2NQ==&amp;mid=2650347372&amp;idx=1&amp;sn=97a0803defeadc67931e65dc4d754a6a&amp;chksm=8300638eb477ea983563bea2f1bb226b65b77581b44eea35f12775df411ee815cd4f3b703138&amp;mpshare=1&amp;scene=23&amp;srcid=0420f8llIzJfeWUhseaV0MTc&amp;sharer_sharetime=1618909377947&amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd</a></p>
</blockquote>
<p style=""></p>
<h3 style="" id="%E4%B8%80%E3%80%81%E6%B6%88%E6%81%AF%E5%B9%82%E7%AD%89%E6%80%A7">一、消息幂等性</h3>
<p style="">消息中间件是分布式系统常用的组件，无论是异步化、解耦、削峰等都有广泛的应用价值。我们通常会认为，消息中间件是一个可靠的组件——这里所谓的可靠是指，只要我把消息成功投递到了消息中间件，消息就不会丢失，即消息肯定会至少保证消息能被消费者成功消费一次，这是消息中间件最基本的特性之一，也就是我们常说的“AT LEAST ONCE”，即消息至少会被“成功消费一遍”。</p>
<p style=""></p>
<p style="">举个例子，一个消息M发送到了消息中间件，消息投递到了消费程序A，A接受到了消息，然后进行消费，但在消费到一半的时候程序重启了，这时候这个消息并没有标记为消费成功，这个消息还会继续投递给这个消费者，直到其消费成功了，消息中间件才会停止投递。</p>
<p style=""></p>
<p style="">然而这种可靠的特性导致，消息可能被多次地投递。举个例子，还是刚刚这个例子，程序A接受到这个消息M并完成消费逻辑之后，正想通知消息中间件“我已经消费成功了”的时候，程序就重启了，那么对于消息中间件来说，这个消息并没有成功消费过，所以他还会继续投递。这时候对于应用程序A来说，看起来就是这个消息明明消费成功了，但是消息中间件还在重复投递。</p>
<p style=""></p>
<p style="">这在RockectMQ的场景来看，就是同一个messageId的消息重复投递下来了。</p>
<p style=""></p>
<p style="">基于消息的投递可靠（消息不丢）是优先级更高的，所以消息不重的任务就会转移到应用程序自我实现，这也是为什么RocketMQ的文档里强调的，消费逻辑需要自我实现幂等。背后的逻辑其实就是：不丢和不重是矛盾的（在分布式场景下），但消息重复是有解决方案的，但是消息丢失是很麻烦的。</p>
<h3 style="" id=""></h3>
<h3 style="" id="%E4%BA%8C%E3%80%81%E7%AE%80%E5%8D%95%E7%9A%84%E6%B6%88%E6%81%AF%E5%B9%82%E7%AD%89%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88">二、简单的消息幂等解决方案</h3>
<p style="">例如：假设我们业务的消息消费逻辑是：插入某张订单表的数据，然后更新库存</p>
<pre><code>insert into t_order values .....
update t_inv set count = count-1 where good_id = 'good123';</code></pre>
<p style="">要实现消息的幂等，我们可能会采取这样的方案：</p>
<pre><code>select * from t_order where order_no = 'order123'

if(order  != null) {
    return ;//消息重复，直接返回
}</code></pre>
<p style="">这对于很多情况下，的确能起到不错的效果，但是在并发场景下，还是会有问题。</p>
<h4 style="" id="%E5%B9%B6%E5%8F%91%E9%87%8D%E5%A4%8D%E6%B6%88%E6%81%AF">并发重复消息</h4>
<p style="">假设这个消费的所有代码加起来需要1秒，有重复的消息在这1秒内（假设100毫秒）内到达（例如生产者快速重发，Broker重启等），那么很可能，上面去重代码里面会发现，数据依然是空的（因为上一条消息还没消费完，还没成功更新订单状态），那么就会穿透掉检查的挡板，最后导致重复的消息消费逻辑进入到非幂等安全的业务代码中，从而引发重复消费的问题（如主键冲突抛出异常、库存被重复扣减而没释放等）</p>
<h4 style="" id="%E5%B9%B6%E5%8F%91%E5%8E%BB%E9%87%8D%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%E4%B9%8B%E4%B8%80">并发去重的解决方案之一</h4>
<p style="">要解决上面并发场景下的消息幂等问题，一个可取的方案是开启事务把select 改成 select for update语句，把记录进行锁定。</p>
<pre><code>select * from t_order where order_no = 'THIS_ORDER_NO' for update  //开启事务
if(order.status != null) {
    return ;//消息重复，直接返回
}</code></pre>
<p style="">但这样消费的逻辑会因为引入了事务包裹而导致整个消息消费时间可能变长，并发度下降。</p>
<p style=""></p>
<p style="">当然还有其他更高级的解决方案，例如更新订单状态采取乐观锁，更新失败则消息重新消费之类的。但这需要针对具体业务场景做更复杂和细致的代码开发、库表设计，不在本文讨论的范围。</p>
<p style=""></p>
<p style="">但无论是select for update， 还是乐观锁这种解决方案，实际上都是基于业务表本身做去重，这无疑增加了业务开发的复杂度， 一个业务系统里面很大部分的请求处理都是依赖MQ的，如果每个消费逻辑本身都需要基于业务本身而做去重/幂等的开发的话，这是繁琐的工作量。本文希望探索出一个通用的消息幂等处理的方法，从而抽象出一定的工具类用以适用各个业务场景。</p>
<h4 style="" id="exactly-once">Exactly Once</h4>
<p style="">在消息中间件里，有一个投递语义的概念，而这个语义里有一个叫”Exactly Once”，即消息肯定会被成功消费，并且只会被消费一次。以下是Exactly Once的解释：</p>
<p style=""></p>
<p style="">Exactly-Once 是指发送到消息系统的消息只能被消费端处理且仅处理一次，即使生产端重试消息发送导致某消息重复投递，该消息在消费端也只被消费一次。</p>
<p style=""></p>
<p style="">在我们业务消息幂等处理的领域内，可以认为业务消息的代码肯定会被执行，并且只被执行一次，那么我们可以认为是Exactly Once。</p>
<p style=""></p>
<p style="">但这在分布式的场景下想找一个通用的方案几乎是不可能的。不过如果是针对基于数据库事务的消费逻辑，实际上是可行的。</p>
<h4 style="" id="%E5%9F%BA%E4%BA%8E%E5%85%B3%E7%B3%BB%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%8B%E5%8A%A1">基于关系数据库事务</h4>
<p style="">假设我们业务的消息消费逻辑是：更新MySQL数据库的某张订单表的状态：</p>
<pre><code>update t_order set status = 'SUCCESS' where order_no= 'order123';</code></pre>
<p style=""></p>
<p style="">要实现Exaclty Once即这个消息只被消费一次（并且肯定要保证能消费一次），我们可以这样做：在这个数据库中增加一个消息消费记录表，把消息插入到这个表，并且把原来的订单更新和这个插入的动作放到同一个事务中一起提交，就能保证消息只会被消费一遍了。</p>
<p style=""></p>
<ol>
 <li>
  <p style="">开启事务</p></li>
 <li>
  <p style="">插入消息表（处理好主键冲突的问题）</p></li>
 <li>
  <p style="">更新订单表（原消费逻辑）</p></li>
 <li>
  <p style="">提交事务</p></li>
</ol>
<p style=""></p>
<p style="">说明：</p>
<ol>
 <li>
  <p style="">这时候如果消息消费成功并且事务提交了，那么消息表就插入成功了，这时候就算RocketMQ还没有收到消费位点的更新再次投递，也会插入消息失败而视为已经消费过，后续就直接更新消费位点了。这保证我们消费代码只会执行一次</p></li>
 <li>
  <p style="">如果事务提交之前服务挂了（例如重启），对于本地事务并没有执行所以订单没有更新，消息表也没插入成功；而对于RocketMQ服务端来说，消费位点也没更新，所以消息还会继续投递下来，投递下来发现这个消息插入消息表也是成功的，所以可以继续消费。这保证了消息不丢失。</p></li>
</ol>
<p style=""></p>
<p style="">基于这种方式，的确这是有能力拓展到不同的应用场景，因为他的实现方案与具体业务本身无关——而是依赖一个消息表。</p>
<p style=""></p>
<p style="">但是这里有它的局限性:</p>
<ol>
 <li>
  <p style="">消息的消费必须是仅依赖于关系型数据库，如果消费的消费过程中还涉及其他数据的修改，例如Redis这种不支持事务特性的数据源，则这些数据是不可回滚的。</p></li>
 <li>
  <p style="">数据库必须是在一个库，跨库无法解决</p></li>
</ol>
<p style=""></p>
<p style="">注：消息表的设计不应该以消息ID作为标识，而应该以业务的业务主键作为标识更为合理，以应对生产者的重发。阿里云上的消息去重只是RocketMQ的messageId，在生产者因为某些原因手动重发（例如上游针对一个交易重复请求了）的场景下起不到去重/幂等的效果（消息id不同）。</p>
<p style=""></p>
<h3 style="" id="%E4%B8%89%E3%80%81%E6%9B%B4%E5%A4%8D%E6%9D%82%E7%9A%84%E4%B8%9A%E5%8A%A1%E5%9C%BA%E6%99%AF">三、更复杂的业务场景</h3>
<p style="">如上所述，要做到Exactly Once语义，实际上有很多局限性，这种局限性使得这个方案基本不具备广泛应用的价值，并且由于基于事务，可能导致锁表时间过长等性能问题。</p>
<p style=""></p>
<p style="">例如：以一个比较常见的一个订单申请的消息来举例，可能有以下几步（以下统称为步骤X）：</p>
<ol>
 <li>
  <p style="">检查库存（RPC）</p></li>
 <li>
  <p style="">锁库存（RPC）</p></li>
 <li>
  <p style="">开启事务，插入订单表（MySQL）</p></li>
 <li>
  <p style="">调用某些其他下游服务（RPC）</p></li>
 <li>
  <p style="">更新订单状态</p></li>
 <li>
  <p style="">commit 事务（MySQL）</p></li>
</ol>
<p style=""></p>
<p style="">这种情况下，我们实际上依旧可以采取消息表+本地事务的实现方式，但实际上消息消费过程中很多子过程是不支持回滚的，也就是说就算我们加了事务，实际上这背后的操作并不是原子性的。怎么说呢，就是说有可能第一条消息经历了第二步锁库存的时候，服务重启了，这时候实际上库存是已经在另外的服务里被锁定了，这并不能被回滚。当然消息还会再次投递下来，要保证消息能至少消费一遍，换句话说，锁库存的这个RPC接口本身依旧要支持“幂等”。</p>
<p style=""></p>
<p style="">再者，如果在这个比较耗时的长链条场景下加入事务的包裹，将大大的降低系统的并发。所以通常情况下，我们处理这种场景的消息去重的方法还是会使用刚刚的业务去重的方式，如前面加select for update，或者使用乐观锁。</p>
<p style=""></p>
<p style="">那我们有没有方法抽取出一个公共的解决方案，能兼顾去重、通用、高性能呢？</p>
<h4 style="" id="%E6%8B%86%E8%A7%A3%E6%B6%88%E6%81%AF%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B">拆解消息执行过程</h4>
<p style="">其中一个思路是把上面的几步，拆解成几个不同的子消息，例如：</p>
<ol>
 <li>
  <p style="">库存系统消费A：检查库存并做锁库存，发送消息B给订单服务</p></li>
 <li>
  <p style="">订单系统消费消息B：插入订单表（MySQL），发送消息C给自己（下游系统）消费</p></li>
 <li>
  <p style="">下游系统消费消息C：处理部分逻辑，发送消息D给订单系统</p></li>
 <li>
  <p style="">订单系统消费消息D：更新订单状态</p></li>
</ol>
<p style="">注：上述步骤需要保证本地事务和消息是一个事务的（至少是最终一致性的），这其中涉及到分布式事务消息相关的话题，不在本文论述。</p>
<p style=""></p>
<p style="">可以看到这样的处理方法会使得每一步的操作都比较原子，而原子则意味着是小事务，小事务则意味着使用消息表+事务的方案显得可行。</p>
<p style=""></p>
<p style="">然而，这太复杂了！这把一个本来连续的代码逻辑割裂成多个系统多次消息交互！那还不如业务代码层面上加锁实现呢。</p>
<p style=""></p>
<h3 style="" id="%E5%9B%9B%E3%80%81%E6%9B%B4%E9%80%9A%E7%94%A8%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88">四、更通用的解决方案</h3>
<p style="">上面消息表+本地事务的方案之所以有其局限性和并发的短板，究其根本是因为它依赖于关系型数据库的事务，且必须要把事务包裹于整个消息消费的环节。</p>
<p style=""></p>
<p style="">如果我们能不依赖事务而实现消息的去重，那么方案就能推广到更复杂的场景例如：RPC、跨库等。</p>
<p style=""></p>
<p style="">例如，我们依旧使用消息表，但是不依赖事务，而是针对消息表增加消费状态，是否可以解决问题呢？</p>
<h4 style="" id="%E5%9F%BA%E4%BA%8E%E6%B6%88%E6%81%AF%E5%B9%82%E7%AD%89%E8%A1%A8%E7%9A%84%E9%9D%9E%E4%BA%8B%E5%8A%A1%E6%96%B9%E6%A1%88">基于消息幂等表的非事务方案</h4>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-hfgh.png&amp;size=m" style="display: inline-block"></p>
<p style="">以上是去事务化后的消息幂等方案的流程，可以看到，此方案是无事务的，而是针对消息表本身做了状态的区分：消费中、消费完成。只有消费完成的消息才会被幂等处理掉。而对于消息中的消息会触发延迟消费（在RocketMQ的场景下即发送到RETRY TOPIC），之所以触发延迟消费是为了控制并发场景下，第二条消息在第一条消息没完成的过程中，去控制消息不丢（如果直接幂等，那么会丢失消息（同一个消息id的话），因为上一条消息如果没有消费完成的时候，第二条消息你已经告诉broker成功了，那么第一条消息这时候失败broker也不会重新投递了）</p>
<p style=""></p>
<p style="">上面的流程不再细说，后面会贴上源码，读者可以参考源码的实现，这里我们回头看看我们一开始想解决的问题是否解决了：</p>
<ol>
 <li>
  <p style="">消息已经消费成功了，第二条消息将被直接幂等处理掉（消费成功）。</p></li>
 <li>
  <p style="">并发场景下的消息，依旧能满足不会出现消息重复，即穿透幂等挡板的问题。</p></li>
 <li>
  <p style="">支持上游业务生产者重发的业务重复的消息幂等问题。</p></li>
</ol>
<p style=""></p>
<p style="">关于第一个问题已经很明显已经解决了，在此就不讨论了。</p>
<p style=""></p>
<p style="">关于第二个问题是如何解决的？主要是依靠插入消息表的这个动作做控制的，假设我们用MySQL作为消息表的存储媒介（设置消息的唯一ID为主键），那么插入的动作只有一条消息会成功，后面的消息插入会由于主键冲突而失败，走向延迟消费的分支，然后后面延迟消费的时候就会变成上面第一个场景的问题。</p>
<p style=""></p>
<p style="">关于第三个问题，只要我们设计去重的消息键让其支持业务的主键（例如订单号、请求流水号等），而不仅仅是messageId即可。所以也不是问题。</p>
<h4 style="" id="%E6%AD%A4%E6%96%B9%E6%A1%88%E6%98%AF%E5%90%A6%E6%9C%89%E6%B6%88%E6%81%AF%E4%B8%A2%E5%A4%B1%E7%9A%84%E9%A3%8E%E9%99%A9">此方案是否有消息丢失的风险</h4>
<p style="">如果细心的读者可能会发现这里实际上是有逻辑漏洞的，问题出在上面聊到的3个问题中的第2个问题的并发场景，在并发场景下我们依赖于消息状态是做并发控制使得第二条消息会不断延迟消费（重试），但如果这时候第一条消息也由于一些异常原因（例如机器重启了、外部异常导致消费失败）没有成功消费成功呢？也就是说这时候延迟消费实际上每次下来看到的都是消费中的状态，这时候最后消费就会被视为消费失败而被投递到死信Topic中（RocketMQ默认可以重复消费16次）。</p>
<p style=""></p>
<p style="">有这种顾虑是正确的！对于此，我们解决的方法是，插入的消息表必须要带一个最长消费过期时间，例如10分钟，意思是如果一个消息处于消费中超过10分钟，就需要从消息表中删除（需要程序自行实现）。所以最后这个消息的流程会是这样的：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-lojz.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="%E4%BA%94%E3%80%81%E6%9B%B4%E7%81%B5%E6%B4%BB%E7%9A%84%E8%A1%A8%E5%AD%98%E5%82%A8%E5%AA%92%E4%BB%8B">五、更灵活的表存储媒介</h3>
<p style="">我们这个方案实际上没有事务的，只需要一个存储的中心媒介，那么自然我们可以选择更灵活的存储媒介，例如Redis。使用Redis有两个好处：</p>
<ol>
 <li>
  <p style="">性能上损耗更低</p></li>
 <li>
  <p style="">上面我们讲到的超时时间可以直接利用Redis本身的ttl实现</p></li>
</ol>
<p style=""></p>
<p style="">当然Redis存储的数据可靠性、一致性等方面是不如MySQL的，需要用户自己取舍。</p>
<p style=""></p>
<p style="">以上方案针对RocketMQ的Java实现已经开源放到Github中，具体的使用文档可以参考 <a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzAwNjQwNzU2NQ==&amp;amp;mid=2650347372&amp;amp;idx=1&amp;amp;sn=97a0803defeadc67931e65dc4d754a6a&amp;amp;chksm=8300638eb477ea983563bea2f1bb226b65b77581b44eea35f12775df411ee815cd4f3b703138&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=0420f8llIzJfeWUhseaV0MTc&amp;amp;sharer_sharetime=1618909377947&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d%23rd">https://github.com/Jaskey/RocketMQDedupListener</a></p>
<p style=""></p>
<p style="">以下仅贴一个Redis的使用样例来示意使用此代码加入消息去重幂等的是多么简单：</p>
<pre><code>//利用Redis做幂等表
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TEST-APP1");
consumer.subscribe("TEST-TOPIC", "*");

String appName = consumer.getConsumerGroup();// 大部分情况下可直接使用consumer group名
StringRedisTemplate stringRedisTemplate = null;// 这里省略获取StringRedisTemplate的过程
DedupConfig dedupConfig = DedupConfig.enableDedupConsumeConfig(appName, stringRedisTemplate);
DedupConcurrentListener messageListener = new SampleListener(dedupConfig);

consumer.registerMessageListener(messageListener);
consumer.start();</code></pre>
<p style="">以上代码大部分是原始RocketMQ的必须代码，唯一需要修改的仅仅是创建一个DedupConcurrentListener示例，在这个示例中指明你的消费逻辑和去重的业务键（默认是messageId）。</p>
<p style="">更多使用详情请参考Github上的说明。</p>
<p style=""></p>
<h3 style="" id="%E5%85%AD%E3%80%81%E7%BB%93%E6%9D%9F%E8%AF%AD">六、结束语</h3>
<h4 style="" id="%E8%BF%99%E7%A7%8D%E5%AE%9E%E7%8E%B0%E6%98%AF%E5%90%A6%E4%B8%80%E5%8A%B3%E6%B0%B8%E9%80%B8%EF%BC%9F">这种实现是否一劳永逸？</h4>
<p style="">实现到这里，似乎方案挺完美的，所有的消息都能快速的接入去重，且与具体业务实现也完全解耦。那么这样是否就完美的完成去重的所有任务呢？</p>
<p style=""></p>
<p style="">很可惜，其实不是的。原因很简单：因为要保证消息至少被成功消费一遍，那么消息就有机会消费到一半的时候失败触发消息重试的可能。还是以上面的例子，假设消息消费的流程包含：</p>
<ol>
 <li>
  <p style="">检查库存（RPC）</p></li>
 <li>
  <p style="">锁库存（RPC）</p></li>
 <li>
  <p style="">开启事务，插入订单表（MySQL）</p></li>
 <li>
  <p style="">调用某些其他下游服务（RPC）</p></li>
 <li>
  <p style="">更新订单状态</p></li>
 <li>
  <p style="">commit 事务（MySQL）</p></li>
</ol>
<p style=""></p>
<p style="">当消息消费到第三步的时候假设MySQL异常导致失败了，触发消息重试。在重试前我们会删除幂等表的记录，所以消息重试的时候就会重新进入消费代码，那么步骤1和步骤2就会重新再执行一遍。如果步骤2本身不是幂等的，那么这个业务消息消费依旧没有做好完整的幂等处理。</p>
<p style=""></p>
<h4 style="" id="%E6%9C%AC%E6%96%B9%E6%A1%88%E7%9A%84%E5%AE%9E%E7%8E%B0%E4%BB%B7%E5%80%BC%EF%BC%9F">本方案的实现价值？</h4>
<p style="">那么既然这个并不能完整的完成消息幂等，还有什么价值呢？价值可就大了！虽然这不是解决消息幂等的银弹（事实上，软件工程领域里基本没有银弹），但是他能以便捷的手段解决：</p>
<p style="">1.各种由于Broker、负载均衡等原因导致的消息重投递的重复问题</p>
<p style="">2.各种上游生产者导致的业务级别消息重复问题</p>
<p style="">3.重复消息并发消费的控制窗口问题，就算重复，重复也不可能同一时间进入消费逻辑</p>
<p style=""></p>
<h4 style="" id="%E4%B8%80%E4%BA%9B%E5%85%B6%E4%BB%96%E7%9A%84%E6%B6%88%E6%81%AF%E5%8E%BB%E9%87%8D%E7%9A%84%E5%BB%BA%E8%AE%AE">一些其他的消息去重的建议</h4>
<p style="">也就是说，使用这个方法能保证正常的消费逻辑场景下（无异常，无异常退出），消息的幂等工作全部都能解决，无论是业务重复，还是rocketmq特性带来的重复。</p>
<p style=""></p>
<p style="">事实上，这已经能解决99%的消息重复问题了，毕竟异常的场景肯定是少数的。那么如果希望异常场景下也能处理好幂等的问题，可以做以下工作降低问题率：</p>
<ol>
 <li>
  <p style="">消息消费失败做好回滚处理。如果消息消费失败本身是带回滚机制的，那么消息重试自然就没有副作用了。</p></li>
 <li>
  <p style="">消费者做好优雅退出处理。这是为了尽可能避免消息消费到一半程序退出导致的消息重试。</p></li>
 <li>
  <p style="">一些无法做到幂等的操作，至少要做到终止消费并告警。例如锁库存的操作，如果统一的业务流水锁成功了一次库存，再触发锁库存，如果做不到幂等的处理，至少要做到消息消费触发异常（例如主键冲突导致消费异常等）</p></li>
 <li>
  <p style="">在#3做好的前提下，做好消息的消费监控，发现消息重试不断失败的时候，手动做好#1的回滚，使得下次重试消费成功。</p></li>
</ol>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299962742</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>RocketMq技术</category><pubDate>Mon, 11 Dec 2023 13:06:33 GMT</pubDate></item><item><title><![CDATA[RocketMQ-延时消息实现原理分析]]></title><link>https://xiaoming728.com/archives/1702300005376</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=RocketMQ-%E5%BB%B6%E6%97%B6%E6%B6%88%E6%81%AF%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90&amp;url=/archives/1702300005376" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><span style="font-size: 15px">来源：CSDN-</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/hosaos"><span style="font-size: 15px">hosaos</span></a></p>
 <p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/hosaos"><span style="font-size: 15px">https://blog.csdn.net/hosaos/article/details/90577732</span></a></p>
 <p style=""><span style="font-size: 15px">日期： 2019-05-26 19:34:18</span></p>
</blockquote>
<p style=""><span style="font-size: 15px">在开发中经常会遇到延时任务的需求，例如在12306购买车票，若生成订单30分钟未支付则自动取消；还有在线商城完成订单后48小时不评价 ，自动5星好评。像这类在某事件触发后一段时间内执行的需求任务我们称之为 延时任务。</span></p>
<h3 style="" id="%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%BB%B6%E8%BF%9F%E4%BB%BB%E5%8A%A1">如何实现延迟任务</h3>
<p style=""><span style="font-size: 15px">第一反应是利用cron方案来实现：</span></p>
<p style=""><span style="font-size: 15px">启动一个cron定时任务，每隔一段时间执行一次，比如30分钟，找到那些超时的数据，直接更新状态，或者拿出来执行一些操作。如果数据量比较大，需要分页查询，分页update，这将是一个for循环更新操作。</span></p>
<p style=""><span style="font-size: 15px">cron方案是很常见的一种方案，但是常见的不一定是最好的，主要有以下几个问题：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">当数据量大的时候轮询效率低；</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">时效性不够好，如果每小时轮询一次，最差的情况时间误差会达到1小时；</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">如果通过增加cron轮询频率来减少时间误差，则会出现轮询低效和重复计算的问题；</span></p></li>
</ul>
<p style=""></p>
<p style=""><span style="font-size: 15px">那么有没其他解决方案？关键有2点设计要求：</span></p>
<ol>
 <li>
  <p style=""><span style="font-size: 15px">能够在指定时间间隔后触发某个业务操作</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">能够应对业务数据量特别大的特殊场景</span></p></li>
</ol>
<p style=""></p>
<p style=""><span style="font-size: 15px">RocketMQ延时消息能够完美的解决上述需求，正常的消息在投递后会立马被消费者所消费，而延时消息在投递时，需要设置指定的延时级别（不同延迟级别对应不同延迟时间），即等到特定的时间间隔后消息才会被消费者消费，这样就将数据库层面的压力转移到了MQ中，也不需要手写定时器，降低了业务复杂度，同时MQ自带削峰功能，能够很好的应对业务高峰。</span></p>
<h3 style="" id="%E5%BB%B6%E6%97%B6%E6%B6%88%E6%81%AF">延时消息</h3>
<p style=""><span style="font-size: 15px">Producer将消息发送到消息队列RocketMQ版服务端，但并不期望立马投递这条消息，而是延迟一定时间后才投递到Consumer进行消费，该消息即延时消息。</span></p>
<p style=""><span style="font-size: 15px">首先需要在RocketMQ配置文件中给出延时等级的定义，在broker.conf中指定以下配置：</span></p>
<pre><code>messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h</code></pre>
<p style=""><span style="font-size: 15px">测试代码：<br>
  控制器</span></p>
<pre><code>    /**
     * 定时消息发送
     */
    @RequestMapping("/send/delay")
    public void delay(){
        rocketMqProducer.sendDelay("test_delay","延时消息",3000L, 5);
    }</code></pre>
<p style=""><span style="font-size: 15px">生产者</span></p>
<pre><code>    /**
     * 发送延时消息
     */
    public void sendDelay(String topic, String msgBody, long timeout, Integer delayLevel) {
        SendResult sendResult = rocketMQTemplate.syncSend(topic, MessageBuilder.withPayload(msgBody).build(), timeout, delayLevel);
        if (ObjectUtils.isNotEmpty(sendResult)) {
            //sendResult不空则表示消息发送成功
            log.info("send success , send msg = {}, messageId = {}", msgBody, sendResult.getMsgId());
        }
    }</code></pre>
<p style=""><span style="font-size: 15px">消费者</span></p>
<pre><code>/**
 * RocketMqProducer
 * @date: 2020/11/26
 * @author weirx
 * @version 3.0
 */
@Slf4j
@Component
@RocketMQMessageListener(topic = "test_delay", selectorExpression = "*", consumerGroup = "test_delay")
public class SimpleDelayMessageListener implements RocketMQListener&lt;MessageExt&gt; {

    @Override
    public void onMessage(MessageExt messageExt) {
        byte[] body = messageExt.getBody();
        String msg = new String(body);
        log.info("receive async message：{}", msg);
    }
}</code></pre>
<p style=""><span style="font-size: 15px">结果，对比上面的配置，延时等级5是一分钟，消息发送和消费正好一分钟。</span></p>
<pre><code>2020-11-27 17:56:55.248  INFO 51704 --- [nio-8085-exec-1] c.c.b.m.r.producer.RocketMqProducer      : send success , send msg = 延时消息, messageId = AC100208C9F818B4AAC289BF48250000
2020-11-27 17:57:55.280  INFO 51704 --- [MessageThread_1] c.c.b.m.r.c.SimpleDelayMessageListener   : receive async message：延时消息</code></pre>
<h3 style="" id="rocketmq%E5%BB%B6%E6%97%B6%E6%B6%88%E6%81%AF%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF">RocketMQ延时消息实现思路</h3>
<ol>
 <li>
  <p style=""><span style="font-size: 15px">producer端设置消息delayLevel延迟级别，消息属性DELAY中存储了对应了延时级别</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">broker端收到消息后，判断延时消息延迟级别，如果大于0，则备份消息原始topic，queueId，并将消息topic改为延时消息队列特定topic(SCHEDULE_TOPIC)，queueId改为延时级别-1</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">mq服务端ScheduleMessageService中，为每一个延迟级别单独设置一个定时器，定时(每隔1秒)拉取对应延迟级别的消费队列</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">根据消费偏移量offset从commitLog中解析出对应消息</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">从消息tagsCode中解析出消息应当被投递的时间，与当前时间做比较，判断是否应该进行投递</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">若到达了投递时间，则构建一个新的消息，并从消息属性中恢复出原始的topic，queueId，并清除消息延迟属性，从新进行消息投递</span></p></li>
</ol>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702300005376</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>RocketMq技术</category><pubDate>Mon, 11 Dec 2023 13:06:00 GMT</pubDate></item><item><title><![CDATA[DefaultMQPushConsumer使用与流程原理分析]]></title><link>https://xiaoming728.com/archives/1702299897244</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=DefaultMQPushConsumer%E4%BD%BF%E7%94%A8%E4%B8%8E%E6%B5%81%E7%A8%8B%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90&amp;url=/archives/1702299897244" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="defaultmqpushconsumer%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B"><span fontsize="" color="rgb(77, 77, 77)" style="color: rgb(77, 77, 77)">DefaultMQPushConsumer使用示例</span></h3>
<pre><code>package com.swk.springboot.rocketmq;
import java.util.List;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
public class MQPushConsumer {
    public static void main(String[] args) throws MQClientException {
        String groupName = "rocketMqGroup1";
        // 用于把多个Consumer组织到一起，提高并发处理能力
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
        // 设置nameServer地址，多个以;分隔
        consumer.setNamesrvAddr("name-serverl-ip:9876;name-server2-ip:9876");
        /**
         * 1. CONSUME_FROM_LAST_OFFSET：第一次启动从队列最后位置消费，后续再启动接着上次消费的进度开始消费 
         * 2. CONSUME_FROM_FIRST_OFFSET：第一次启动从队列初始位置消费，后续再启动接着上次消费的进度开始消费 
         * 3. CONSUME_FROM_TIMESTAMP：第一次启动从指定时间点位置消费，后续再启动接着上次消费的进度开始消费 
         * 以上所说的第一次启动是指从来没有消费过的消费者，如果该消费者消费过，那么会在broker端记录该消费者的消费位置，如果该消费者挂了再启动，那么自动从上次消费的进度开始
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        /**
         * CLUSTERING：默认模式，同一个ConsumerGroup(groupName相同)每个consumer只消费所订阅消息的一部分内容，同一个ConsumerGroup里所有的Consumer消息加起来才是所订阅topic整体，从而达到负载均衡的目的
         * BROADCASTING：同一个ConsumerGroup每个consumer都消费到所订阅topic所有消息，也就是一个消费会被多次分发，被多个consumer消费。
         */
        consumer.setMessageModel(MessageModel.BROADCASTING);
        // 订阅topic，可以对指定消息进行过滤，例如："TopicTest","tagl||tag2||tag3",*或null表示topic所有消息
        consumer.subscribe("order-topic", "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List&lt;MessageExt&gt; mgs, ConsumeConcurrentlyContext consumeconcurrentlycontext) {
                System.out.println(Thread.currentThread().getName()+"Receive New Messages:"+mgs);
                // return ConsumeConcurrentlyStatus.RECONSUME_LATER; //boker会根据设置的messageDelayLevel发起重试，默认16次
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}</code></pre>
<p style="">DefaultMQPushConsumerImpl中各个对象的主要功能如下：</p>
<p style="">RebalancePushImpl：主要负责决定，当前的consumer应该从哪些Queue中消费消息；</p>
<p style="">1）PullAPIWrapper：长连接，负责从broker处拉取消息，然后利用ConsumeMessageService回调用户的Listener执行消息消费逻辑；</p>
<p style="">2）ConsumeMessageService：实现所谓的"Push-被动"消费机制；从Broker拉取的消息后，封装成ConsumeRequest提交给ConsumeMessageSerivce，此service负责回调用户的Listener消费消息；</p>
<p style="">3）OffsetStore：维护当前consumer的消费记录（offset）；有两种实现，Local和Rmote，Local存储在本地磁盘上，适用于BROADCASTING广播消费模式；而Remote则将消费进度存储在Broker上，适用于CLUSTERING集群消费模式；</p>
<p style="">4）MQClientFactory：负责管理client（consumer、producer），并提供多中功能接口供各个Service（Rebalance、PullMessage等）调用；大部分逻辑均在这个类中完成；</p>
<p style=""></p>
<h3 style="" id="%E5%88%86%E6%9E%90consumer.registermessagelistener%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B"><span fontsize="" color="rgb(77, 77, 77)" style="color: rgb(77, 77, 77)">分析consumer.registerMessageListener执行过程</span></h3>
<p style=""><span fontsize="" color="rgb(140, 140, 140)" style="color: rgb(140, 140, 140)">RocketMQ的源码基本上没有注释，阅读起来有点费劲</span></p>
<pre><code>	/**
 &nbsp; &nbsp; * Register a callback to execute on message arrival for concurrent consuming.
 &nbsp; &nbsp; *
 &nbsp; &nbsp; * @param messageListener message handling callback.
 &nbsp; &nbsp; */
 &nbsp; &nbsp;@Override
 &nbsp; &nbsp;public void registerMessageListener(MessageListenerConcurrently messageListener) {
 &nbsp; &nbsp; &nbsp; &nbsp;this.messageListener = messageListener;
 &nbsp; &nbsp; &nbsp; &nbsp;this.defaultMQPushConsumerImpl.registerMessageListener(messageListener);
 &nbsp;  }
	
	/**
     * 通过源码可以看出主要实现过程在DefaultMQPushConsumerImpl类中，
     * consumer.start后调用DefaultMQPushConsumerImpl的同步start方法
     */
	public synchronized void start() throws MQClientException {
 &nbsp; &nbsp; &nbsp; &nbsp;switch (this.serviceState) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case CREATE_JUST:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.serviceState = ServiceState.START_FAILED;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.checkConfig();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.copySubscription();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.defaultMQPushConsumer.changeInstanceNameToPID();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.pullAPIWrapper = new PullAPIWrapper(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;mQClientFactory,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (this.defaultMQPushConsumer.getOffsetStore() != null) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;switch (this.defaultMQPushConsumer.getMessageModel()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case BROADCASTING:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case CLUSTERING:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;default:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.offsetStore.load();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.consumeOrderly = true;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.consumeMessageService =
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.consumeOrderly = false;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.consumeMessageService =
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.consumeMessageService.start();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (!registerOK) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.serviceState = ServiceState.CREATE_JUST;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.consumeMessageService.shutdown();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;null);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;mQClientFactory.start();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.serviceState = ServiceState.RUNNING;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case RUNNING:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case START_FAILED:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case SHUTDOWN_ALREADY:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+ this.serviceState
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+ FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;null);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;default:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;this.updateTopicSubscribeInfoWhenSubscriptionChanged();
 &nbsp; &nbsp; &nbsp; &nbsp;this.mQClientFactory.checkClientInBroker();
 &nbsp; &nbsp; &nbsp; &nbsp;this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
 &nbsp; &nbsp; &nbsp; &nbsp;this.mQClientFactory.rebalanceImmediately();
 &nbsp;  }</code></pre>
<p style=""><span fontsize="" color="rgb(77, 77, 77)" style="color: rgb(77, 77, 77)">略过一些细节设置和校验，通过mQClientFactory.start();发我们发现他调用</span></p>
<pre><code>public void start() throws MQClientException {
 &nbsp; &nbsp; &nbsp; &nbsp;synchronized (this) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;switch (this.serviceState) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case CREATE_JUST:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.serviceState = ServiceState.START_FAILED;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// If not specified,looking address from name server
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (null == this.clientConfig.getNamesrvAddr()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.mQClientAPIImpl.fetchNameServerAddr();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// Start request-response channel
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.mQClientAPIImpl.start();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// Start various schedule tasks
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.startScheduledTask();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// Start pull service
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.pullMessageService.start();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// Start rebalance service
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.rebalanceService.start();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// Start push service
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.info("the client factory [{}] start OK", this.clientId);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.serviceState = ServiceState.RUNNING;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case RUNNING:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case SHUTDOWN_ALREADY:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case START_FAILED:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;default:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  }
 &nbsp;  }</code></pre>
<p style=""><span fontsize="" color="rgb(77, 77, 77)" style="color: rgb(77, 77, 77)">在这个方法中有多个start，我们主要看pullMessageService.start();通过这里我们发现RocketMQ的Push模式底层其实也是通过pull实现的，下面我们来看下pullMessageService处理了哪些逻辑：</span></p>
<pre><code>	private void pullMessage(final PullRequest pullRequest) {
 &nbsp; &nbsp; &nbsp; &nbsp;final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
 &nbsp; &nbsp; &nbsp; &nbsp;if (consumer != null) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;impl.pullMessage(pullRequest);
 &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
 &nbsp; &nbsp; &nbsp;  }
 &nbsp;  }</code></pre>
<p style=""><span fontsize="" color="rgb(77, 77, 77)" style="color: rgb(77, 77, 77)">我们发现其实他还是通过DefaultMQPushConsumerImpl类的pullMessage方法来进行消息的逻辑处理，这部分代码较多，我们只分析一些关键的步骤</span></p>
<h4 style="" id="%E6%B6%88%E6%81%AF%E6%8B%89%E5%8F%96%E5%85%A5%E5%8F%A3">消息拉取入口</h4>
<pre><code>/**
    PullRequest这里说明一下，上面我们已经提了一下rocketmq的push模式其实是通过pull模式封装实现的，pullrequest这里是通过长轮询的方式达到push效果。长轮询方式既有pull的优点又有push模式的实时性有点。
    push方式是server端接收到消息后，主动把消息推送给client端，实时性高。弊端是server端工作量大，影响性能，其次是client端处理能力不同且client端的状态不受server端的控制，如果client端不能及时处理消息容易导致消息堆积已经影响正常业务等。
    pull方式是client循环从server端拉取消息，主动权在client端，自己处理完一个消息再去拉取下一个，缺点是循环的时间不好设定，时间太短容易忙等，浪费CPU资源，时间间隔太长client的处理能力会下降，有时候有些消息会处理不及时。
    长轮询的方式可以结合两者优点
    1、检查PullRequest对象中的ProcessQueue对象的dropped是否为true（在RebalanceService线程中为topic下的MessageQueue创建拉取消息请求时要维护对应的ProcessQueue对象，若Consumer不再订阅该topic则会将该对象的dropped置为true）；若是则认为该请求是已经取消的，则直接跳出该方法；
    2、更新PullRequest对象中的ProcessQueue对象的时间戳（ProcessQueue.lastPullTimestamp）为当前时间戳；
    3、检查该Consumer是否运行中，即DefaultMQPushConsumerImpl.serviceState是否为RUNNING;若不是运行状态或者是暂停状态（DefaultMQPushConsumerImpl.pause=true），则调用PullMessageService.executePullRequestLater(PullRequest pullRequest, long timeDelay)方法延迟再拉取消息，其中timeDelay=3000；该方法的目的是在3秒之后再次将该PullRequest对象放入PullMessageService. pullRequestQueue队列中；并跳出该方法；
    4、进行流控。若ProcessQueue对象的msgCount大于了消费端的流控阈值（DefaultMQPushConsumer.pullThresholdForQueue，默认值为1000），则调用PullMessageService.executePullRequestLater方法，在50毫秒之后重新该PullRequest请求放入PullMessageService.pullRequestQueue队列中；并跳出该方法；
    5、若不是顺序消费（即DefaultMQPushConsumerImpl.consumeOrderly等于false），则检查ProcessQueue对象的msgTreeMap:TreeMap&lt;Long,MessageExt&gt;变量的第一个key值与最后一个key值之间的差额，该key值表示查询的队列偏移量queueoffset；若差额大于阈值（由DefaultMQPushConsumer. consumeConcurrentlyMaxSpan指定，默认是2000），则调用PullMessageService.executePullRequestLater方法，在50毫秒之后重新将该PullRequest请求放入PullMessageService.pullRequestQueue队列中；并跳出该方法；
    6、以PullRequest.messageQueue对象的topic值为参数从RebalanceImpl.subscriptionInner: ConcurrentHashMap, SubscriptionData&gt;中获取对应的SubscriptionData对象，若该对象为null，考虑到并发的关系，调用executePullRequestLater方法，稍后重试；并跳出该方法；
    7、若消息模型为集群模式（RebalanceImpl.messageModel等于CLUSTERING），则以PullRequest对象的MessageQueue变量值、type =READ_FROM_MEMORY（从内存中获取消费进度offset值）为参数调用DefaultMQPushConsumerImpl. offsetStore对象（初始化为RemoteBrokerOffsetStore对象）的readOffset(MessageQueue mq, ReadOffsetType type)方法从本地内存中获取消费进度offset值。若该offset值大于0 则置临时变量commitOffsetEnable等于true否则为false；该offset值作为pullKernelImpl方法中的commitOffset参数，在Broker端拉取消息之后根据commitOffsetEnable参数值决定是否用该offset更新消息进度。该readOffset方法的逻辑是：以入参MessageQueue对象从RemoteBrokerOffsetStore.offsetTable:ConcurrentHashMap &lt;MessageQueue,AtomicLong&gt;变量中获取消费进度偏移量；若该偏移量不为null则返回该值，否则返回-1；
    8、当每次拉取消息之后需要更新订阅关系（由DefaultMQPushConsumer. postSubscriptionWhenPull参数表示，默认为false）并且以topic值参数从RebalanceImpl.subscriptionInner获取的SubscriptionData对象的classFilterMode等于false（默认为false），则将sysFlag标记的第3个字节置为1，否则该字节置为0；
    9、该sysFlag标记的第1个字节置为commitOffsetEnable的值；第2个字节（suspend标记）置为1；第4个字节置为classFilterMode的值；
    10、 初始化匿名内部类PullCallback，实现了onSucess/onException方法； 该方法只有在异步请求的情况下才会回调；
    11、调用底层的拉取消息API接口：PullAPIWrapper.pullKernelImpl(MessageQueue mq, String subExpression, long subVersion,long offset, int maxNums, int sysFlag,long commitOffset,long brokerSuspendMaxTimeMillis, long timeoutMillis, CommunicationMode communicationMode, PullCallback pullCallback)方法进行消息拉取操作。将回调类PullCallback传入该方法中，当采用异步方式拉取消息时，在收到响应之后会回调该回调类的方法。
**/
public void pullMessage(final PullRequest pullRequest) {
 &nbsp; &nbsp; &nbsp; &nbsp;final ProcessQueue processQueue = pullRequest.getProcessQueue();
 &nbsp; &nbsp; &nbsp; &nbsp;if (processQueue.isDropped()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.info("the pull request[{}] is dropped.", pullRequest.toString());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
 &nbsp; &nbsp; &nbsp; &nbsp;try {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.makeSureStateOK();
 &nbsp; &nbsp; &nbsp;  } catch (MQClientException e) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("pullMessage exception, consumer state not ok", e);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;if (this.isPause()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;long cachedMessageCount = processQueue.getMsgCount().get();
 &nbsp; &nbsp; &nbsp; &nbsp;long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
 &nbsp; &nbsp; &nbsp; &nbsp;if (cachedMessageCount &gt; this.defaultMQPushConsumer.getPullThresholdForQueue()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if ((queueFlowControlTimes++ % 1000) == 0) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;if (cachedMessageSizeInMiB &gt; this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if ((queueFlowControlTimes++ % 1000) == 0) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;if (!this.consumeOrderly) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (processQueue.getMaxSpan() &gt; this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest, queueMaxSpanFlowControlTimes);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (processQueue.isLocked()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (!pullRequest.isLockedFirst()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;boolean brokerBusy = offset &lt; pullRequest.getNextOffset();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest, offset, brokerBusy);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (brokerBusy) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest, offset);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.setLockedFirst(true);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.setNextOffset(offset);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.info("pull message later because not locked in broker, {}", pullRequest);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
 &nbsp; &nbsp; &nbsp; &nbsp;if (null == subscriptionData) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("find the consumer's subscription failed, {}", pullRequest);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;final long beginTimestamp = System.currentTimeMillis();
 &nbsp; &nbsp; &nbsp; &nbsp;PullCallback pullCallback = new PullCallback() {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;@Override
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;public void onSuccess(PullResult pullResult) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (pullResult != null) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;subscriptionData);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;switch (pullResult.getPullStatus()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case FOUND:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;long prevRequestOffset = pullRequest.getNextOffset();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.setNextOffset(pullResult.getNextBeginOffset());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;long pullRT = System.currentTimeMillis() - beginTimestamp;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.getMessageQueue().getTopic(), pullRT);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;long firstMsgOffset = Long.MAX_VALUE;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullResult.getMsgFoundList(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;processQueue,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.getMessageQueue(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;dispatchToConsume);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() &gt; 0) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (pullResult.getNextBeginOffset() &lt; prevRequestOffset
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|| firstMsgOffset &lt; prevRequestOffset) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullResult.getNextBeginOffset(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;firstMsgOffset,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;prevRequestOffset);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case NO_NEW_MSG:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.setNextOffset(pullResult.getNextBeginOffset());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case NO_MATCHED_MSG:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.setNextOffset(pullResult.getNextBeginOffset());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case OFFSET_ILLEGAL:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("the pull request offset illegal, {} {}",
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.toString(), pullResult.toString());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.setNextOffset(pullResult.getNextBeginOffset());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.getProcessQueue().setDropped(true);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;@Override
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;public void run() {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;try {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.getNextOffset(), false);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("fix the pull request offset, {}", pullRequest);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } catch (Throwable e) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.error("executeTaskLater Exception", e);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }, 10000);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;default:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;@Override
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;public void onException(Throwable e) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("execute the pull request exception", e);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  };
 &nbsp; &nbsp; &nbsp; &nbsp;boolean commitOffsetEnable = false;
 &nbsp; &nbsp; &nbsp; &nbsp;long commitOffsetValue = 0L;
 &nbsp; &nbsp; &nbsp; &nbsp;if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (commitOffsetValue &gt; 0) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;commitOffsetEnable = true;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;String subExpression = null;
 &nbsp; &nbsp; &nbsp; &nbsp;boolean classFilter = false;
 &nbsp; &nbsp; &nbsp; &nbsp;SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
 &nbsp; &nbsp; &nbsp; &nbsp;if (sd != null) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull() &amp;&amp; !sd.isClassFilterMode()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;subExpression = sd.getSubString();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;classFilter = sd.isClassFilterMode();
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;int sysFlag = PullSysFlag.buildSysFlag(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;commitOffsetEnable, // commitOffset
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;true, // suspend
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;subExpression != null, // subscription
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;classFilter // class filter
 &nbsp; &nbsp; &nbsp;  );
 &nbsp; &nbsp; &nbsp; &nbsp;try {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 下面我们看继续跟进这个方法，这个方法已经就是客户端如何拉取消息
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.pullAPIWrapper.pullKernelImpl(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.getMessageQueue(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;subExpression,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;subscriptionData.getExpressionType(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;subscriptionData.getSubVersion(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullRequest.getNextOffset(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.defaultMQPushConsumer.getPullBatchSize(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;sysFlag,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;commitOffsetValue,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;BROKER_SUSPEND_MAX_TIME_MILLIS,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 消息的通信方式为异步
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;CommunicationMode.ASYNC,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullCallback
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  );
 &nbsp; &nbsp; &nbsp;  } catch (Exception e) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.error("pullKernelImpl exception", e);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
 &nbsp; &nbsp; &nbsp;  }
 &nbsp;  }</code></pre>
<h4 style="" id="%E6%8B%89%E5%8F%96%E6%B6%88%E6%81%AF%E5%BA%95%E5%B1%82api">拉取消息底层API</h4>
<pre><code>/**
    1、获取Broker的ID。以入参MessageQueue对象为参数调用PullAPIWrapper.recalculatePullFromWhichNode(MessageQueue mq)方法，在该方法中，先判断PullAPIWrapper.connectBrokerByUser变量是否为true（在FiltersrvController中启动时会设置为true，默认为false），若是则直接返回0（主用Broker的brokerId）；否则以MessageQueue对象为参数从PullAPIWrapper.pullFromWhichNodeTable:ConcurrentHashMap&lt;MessageQueue, AtomicLong获取brokerId，若该值不为null则返回该值，否则返回0（主用Broker的brokerId）；
    2、调用MQClientInstance.findBrokerAddressInSubscribe(String brokerName ,long brokerId,boolean onlyThisBroker) 方法查找Broker地址，其中onlyThisBroker=false，表示若指定的brokerId未获取到地址则获取其他BrokerId的地址也行。在该方法中根据brokerName和brokerId参数从MQClientInstance.brokerAddrTable: ConcurrentHashMap&lt;, HashMap变量中获取对应的Broker地址，若获取不到则从brokerName下面的Map列表中找其他地址返回即可；
    3、若在上一步未获取到Broker地址，则以topic参数调用MQClientInstance.updateTopicRouteInfoFromNameServer(String topic)方法，然后在执行第2步的操作，直到获取到Broker地址为止；
    4、若获取的Broker地址是备用Broker，则将标记位sysFlag的第1个字节置为0，即在消费完之后不提交消费进度；
    5、检查标记位sysFlag的第4个字节（即SubscriptionData. classFilterMode）是否为1；若等于1，则调用PullAPIWrapper.computPullFromWhichFilterServer(String topic, String brokerAddr)方法获取Filter服务器地址。大致逻辑如下：
    5.1)根据topic参数值从MQClientInstance.topicRouteTable: ConcurrentHashMapTopicRouteData&gt;变量中获取TopicRouteData对象，
    5.2)以Broker地址为参数从该TopicRouteData对象的filterServerTable:HashMap变量中获取该Broker下面的所有Filter服务器地址列表；
    5.3)若该地址列表不为空，则随机选择一个Filter服务器地址返回；否则向调用层抛出异常，该pullKernelImpl方法结束；
    6、构建PullMessageRequestHeader对象，其中queueOffset变量值等于入参offset；
    7、若执行了第5步则向获取的Filter服务器发送PULL_MESSAGE请求信息，否则向Broker发送PULL_MESSAGE请求信息。初始化PullMessageRequestHeader对象，然后调用MQClientAPIImpl.pullMessage(String addr, PullMessageRequestHeader requestHeader, long timeoutMillis, CommunicationMode communicationMode, PullCallback pullCallback)方法向Broker地址或者Filter地址发送PULL_MESSAGE请求信息
**/
public PullResult pullKernelImpl(
 &nbsp; &nbsp; &nbsp; &nbsp;final MessageQueue mq,
 &nbsp; &nbsp; &nbsp; &nbsp;final String subExpression,
 &nbsp; &nbsp; &nbsp; &nbsp;final String expressionType,
 &nbsp; &nbsp; &nbsp; &nbsp;final long subVersion,
 &nbsp; &nbsp; &nbsp; &nbsp;final long offset,
 &nbsp; &nbsp; &nbsp; &nbsp;final int maxNums,
 &nbsp; &nbsp; &nbsp; &nbsp;final int sysFlag,
 &nbsp; &nbsp; &nbsp; &nbsp;final long commitOffset,
 &nbsp; &nbsp; &nbsp; &nbsp;final long brokerSuspendMaxTimeMillis,
 &nbsp; &nbsp; &nbsp; &nbsp;final long timeoutMillis,
 &nbsp; &nbsp; &nbsp; &nbsp;final CommunicationMode communicationMode,
 &nbsp; &nbsp; &nbsp; &nbsp;final PullCallback pullCallback
 &nbsp;  ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
 &nbsp; &nbsp; &nbsp; &nbsp;FindBrokerResult findBrokerResult =
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.recalculatePullFromWhichNode(mq), false);
 &nbsp; &nbsp; &nbsp; &nbsp;if (null == findBrokerResult) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;findBrokerResult =
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.recalculatePullFromWhichNode(mq), false);
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;if (findBrokerResult != null) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// check version
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (!ExpressionType.isTagType(expressionType)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; findBrokerResult.getBrokerVersion() &lt; MQVersion.Version.V4_1_0_SNAPSHOT.ordinal()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw new MQClientException("The broker[" + mq.getBrokerName() + ", "
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+ findBrokerResult.getBrokerVersion() + "] does not upgrade to support for filter message by " + expressionType, null);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int sysFlagInner = sysFlag;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (findBrokerResult.isSlave()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;PullMessageRequestHeader requestHeader = new PullMessageRequestHeader();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setConsumerGroup(this.consumerGroup);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setTopic(mq.getTopic());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setQueueId(mq.getQueueId());
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setQueueOffset(offset);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setMaxMsgNums(maxNums);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setSysFlag(sysFlagInner);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setCommitOffset(commitOffset);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 设置broker的最长阻塞时间，默认是15秒，broker只有在没有消息的时候才会阻塞，如果阻塞超过设定时间会返回null，如果有消息会立即返回
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setSubscription(subExpression);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setSubVersion(subVersion);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader.setExpressionType(expressionType);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;String brokerAddr = findBrokerResult.getBrokerAddr();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;brokerAddr,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;requestHeader,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;timeoutMillis,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;communicationMode,
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pullCallback);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return pullResult;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
 &nbsp;  }</code></pre>
<h4 style="" id="%E5%8F%91%E9%80%81%E8%BF%9C%E7%A8%8B%E8%AF%B7%E6%B1%82%E6%8B%89%E5%8F%96%E6%B6%88%E6%81%AF">发送远程请求拉取消息</h4>
<pre><code>/**
    在MQClientAPIImpl.pullMessage方法中，根据入参communicationMode的值分为异步拉取和同步拉取方式两种。
    无论是异步方式拉取还是同步方式拉取，在发送拉取请求之前都会构造一个ResponseFuture对象，以请求消息的序列号为key值，存入NettyRemotingAbstract.responseTable:ConcurrentHashMap, ResponseFuture&gt;变量中，对该变量有几种情况会处理：
    1、发送失败后直接删掉responseTable变量中的相应记录；
    2、收到响应消息之后，会以响应消息中的序列号（由服务端根据请求消息的序列号原样返回）从responseTable中查找ResponseFuture对象，并设置该对象的responseCommand变量。若是同步发送会唤醒等待响应的ResponseFuture.waitResponse方法；若是异步发送会调用ResponseFuture.executeInvokeCallback()方法完成回调逻辑处理；
    3、在NettyRemotingClient.start()启动时，也会初始化定时任务，该定时任务每隔1秒定期扫描responseTable列表，遍历该列表中的ResponseFuture对象，检查等待响应是否超时，若超时，则调用ResponseFuture. executeInvokeCallback()方法，并将该对象从responseTable列表中删除；
**/
public PullResult pullMessage(
 &nbsp; &nbsp; &nbsp; &nbsp;final String addr,
 &nbsp; &nbsp; &nbsp; &nbsp;final PullMessageRequestHeader requestHeader,
 &nbsp; &nbsp; &nbsp; &nbsp;final long timeoutMillis,
 &nbsp; &nbsp; &nbsp; &nbsp;final CommunicationMode communicationMode,
 &nbsp; &nbsp; &nbsp; &nbsp;final PullCallback pullCallback
 &nbsp;  ) throws RemotingException, MQBrokerException, InterruptedException {
 &nbsp; &nbsp; &nbsp; &nbsp;RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader);
 &nbsp; &nbsp; &nbsp; &nbsp;switch (communicationMode) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case ONEWAY:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;assert false;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return null;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case ASYNC:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.pullMessageAsync(addr, request, timeoutMillis, pullCallback);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return null;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;case SYNC:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return this.pullMessageSync(addr, request, timeoutMillis);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;default:
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;assert false;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;return null;
 &nbsp;  }</code></pre>
<h4 style="" id="%E5%90%8C%E6%AD%A5%E6%8B%89%E5%8F%96">同步拉取</h4>
<pre><code>/**
    对于同步发送方式，调用MQClientAPIImpl.pullMessageSync(String addr, RemotingCommand request, long timeoutMillis)方法。大致步骤如下：
    1、调用RemotingClient.invokeSync(String addr, RemotingCommand request, long timeoutMillis)方法：
    1.1）获取Broker地址的Channel信息。根据broker地址从RemotingClient.channelTables:ConcurrentHashMap, ChannelWrapper&gt;变量中获取ChannelWrapper对象并返回该对象的Channel变量；若没有ChannelWrapper对象则与broker地址建立新的连接并将连接信息存入channelTables变量中，便于下次使用；
    1.2）若NettyRemotingClient.rpcHook:RPCHook变量不为空（该变量在应用层初始化DefaultMQPushConsumer或者DefaultMQPullConsumer对象传入该值），则调用RPCHook.doBeforeRequest(String remoteAddr, RemotingCommand request)方法；
    1.3)调用NettyRemotingAbstract.invokeSyncImpl(Channel channel, RemotingCommand request, long timeoutMillis)方法，该方法的逻辑如下：
    A）使用请求的序列号（opaue）、超时时间初始化ResponseFuture对象；并将该ResponseFuture对象存入NettyRemotingAbstract.responseTable: ConcurrentHashMap变量中；
    B）调用Channel.writeAndFlush(Object msg)方法将请求对象RemotingCommand发送给Broker；然后调用addListener(GenericFutureListener&lt;? extends Future&lt;? super Void&gt;&gt; listener)方法添加内部匿名类：该内部匿名类实现了ChannelFutureListener接口的operationComplete方法，在发送完成之后回调该监听类的operationComplete方法，在该方法中，首先调用ChannelFuture. isSuccess()方法检查是否发送成功，若成功则置ResponseFuture对象的sendRequestOK等于true并退出此回调方法等待响应结果；若不成功则置ResponseFuture对象的sendRequestOK等于false，然后从NettyRemotingAbstract.responseTable中删除此请求序列号（opaue）的记录，置ResponseFuture对象的responseCommand等于null，并唤醒ResponseFuture.waitResponse(long timeoutMillis)方法的等待；
    C）调用ResponseFuture.waitResponse(long timeoutMillis)方法等待响应结果；在发送失败或者收到响应消息（详见5.10.3小节）或者超时的情况下会唤醒该方法返回ResponseFuture.responseCommand变量值；
    D）若上一步返回的responseCommand值为null，则抛出异常:若ResponseFuture.sendRequestOK为true，则抛出RemotingTimeoutException异常，否则抛出RemotingSendRequestException异常；
    E）若上一步返回的responseCommand值不为null，则返回responseCommand变量值；
    1.4）若NettyRemotingClient.rpcHook: RPCHook变量不为空，则调用RPCHook.doAfterResponse(String remoteAddr, RemotingCommand request)方法；
    2、以上一步的返回值RemotingCommand对象为参数调用MQClientAPIImpl. processPullResponse (RemotingCommand response)方法将返回对象解析并封装成PullResultExt对象然后返回给调用者，响应消息的结果状态转换如下：
    2.1）若RemotingCommand对象的Code等于SUCCESS，则PullResultExt.pullStatus=FOUND；
    2.2）若RemotingCommand对象的Code等于PULL_NOT_FOUND，则PullResultExt.pullStatus= NO_NEW_MSG；
    2.3）若RemotingCommand对象的Code等于PULL_RETRY_IMMEDIATELY，则PullResultExt.pullStatus= NO_MATCHED_MSG；
    2.3）若RemotingCommand对象的Code等于PULL_OFFSET_MOVED，则PullResultExt.pullStatus= OFFSET_ILLEGAL；
**/
 @Override
 &nbsp; &nbsp;public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
 &nbsp; &nbsp; &nbsp; &nbsp;throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
 &nbsp; &nbsp; &nbsp; &nbsp;long beginStartTime = System.currentTimeMillis();
 &nbsp; &nbsp; &nbsp; &nbsp;final Channel channel = this.getAndCreateChannel(addr);
 &nbsp; &nbsp; &nbsp; &nbsp;if (channel != null &amp;&amp; channel.isActive()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;try {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (this.rpcHook != null) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.rpcHook.doBeforeRequest(addr, request);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;long costTime = System.currentTimeMillis() - beginStartTime;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (timeoutMillis &lt; costTime) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw new RemotingTimeoutException("invokeSync call timeout");
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (this.rpcHook != null) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.rpcHook.doAfterResponse(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return response;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } catch (RemotingSendRequestException e) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("invokeSync: send request exception, so close the channel[{}]", addr);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.closeChannel(addr, channel);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw e;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } catch (RemotingTimeoutException e) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (nettyClientConfig.isClientCloseSocketIfTimeout()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.closeChannel(addr, channel);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw e;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.closeChannel(addr, channel);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw new RemotingConnectException(addr);
 &nbsp; &nbsp; &nbsp;  }
 &nbsp;  }</code></pre>
<h4 style="" id="%E5%BC%82%E6%AD%A5%E6%8B%89%E5%8F%96">异步拉取</h4>
<pre><code>/**
    对于异步方式拉取消息，调用MQClientAPIImpl.pullMessageAsync(String addr, RemotingCommand request, long timeoutMillis, PullCallback pullCallback)方法。大致逻辑如下：
    1、定义了一个内部匿名InvokeCallback类并实现operationComplete (ResponseFuture responseFuture)方法；该方法的大致逻辑如下：
    1.1）从入参ResponseFuture对象中获取传输的响应对象RemotingCommand；
    1.2）若该响应对象RemotingCommand不为空；则首先调用MQClientAPIImpl. processPullResponse (RemotingCommand response)方法对返回对象解析并封装成PullResultExt对象，其中PullResultExt.messageBinary等于响应消息的body；然后以PullResultExt对象为参数调用回调类PullCallback对象的onSuccess方法（调用应用层定义的回调方法，详见5.5.2小节），若在此过程中出现异常则调用PullCallback对象的onException方法（调用应用层定义的回调方法）；
    1.3）若该返回对象RemotingCommand为空；则检查ResponseFuture.sendRequestOK是否为true，若不是则发送请求失败；若发生成功再检查是否等待超时；对于每种异常情况均调用PullCallback对象的onException方法由应用层来处理异常情况；
    2、以匿名类InvokeCallback为参数调用NettyRemotingClient.invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback)方法，大致逻辑如下：
    2.1）获取Broker地址的Channel信息。根据broker地址从RemotingClient.channelTables: ConcurrentHashMap, ChannelWrapper&gt;变量中获取ChannelWrapper对象并返回该对象的Channel变量；若没有ChannelWrapper对象则与broker地址建立新的连接并将连接信息存入channelTables变量中，便于下次使用；
    2.2）若NettyRemotingClient.rpcHook: RPCHook变量不为空（该变量在应用层初始化DefaultMQPushConsumer或者DefaultMQPullConsumer对象传入该值），则调用RPCHook.doBeforeRequest(String remoteAddr, RemotingCommand request)方法；
    2.3）调用NettyRemotingAbstract.invokeAsyncImpl(Channel channel, RemotingCommand request,long timeoutMillis,InvokeCallback invokeCallback)方法，该方法的大致逻辑如下：
    A）利用java.util.concurrent.Semaphore.tryAcquire(long timeout,TimeUnitunit)获取信号量，保证该方法的业务逻辑同时执行的线程个数；
    B）使用请求的序列号（opaue）、超时时间、InvokeCallback对象、 用Semaphore初始化的SemaphoreReleaseOnlyOnce对象（该对象是确保在释放信号量是只释放一次）初始化ResponseFuture对象，并根据请求的序列号（opaue）作为key值，将该ResponseFuture对象存入NettyRemotingAbstract. responseTable对象中；
    C）调用Channel.writeAndFlush(Object msg)方法将请求对象发送给Broker，并且添加监听器，再消息发送完成之后回调该监听器，该监听器是内部匿名类，该类实现了ChannelFutureListener接口的operationComplete(ChannelFuture f)方法，该方法的逻辑如下：
    C.1）首先调用ChannelFuture.isSuccess()方法检查是否发送成功，若成功则置ResponseFuture对象的sendRequestOK等于true并退出此回调方法等待对方的响应消息；若不成功则置ResponseFuture对象的sendRequestOK等于false，然后继续执行下面的逻辑，主要目的是立即向应用层返回发送失败的响应消息，无需再等待对方的响应结果；
    C.2）根据请求的序列号（opaue）从responseTable中删除相应的ResponseFuture对象记录；
    C.3）将ResponseFuture.responseCommand变量置为null；
    C.4）调用ResponseFuture.executeInvokeCallback()方法，在该方法中执行InvokeCallback.OperationComplete(ResponseFuture)方法完成回调工作，在executeInvokeCallback方法之前，确保ResponseFuture. executeCallbackOnlyOnce的值为false并且成功更新为true，由于executeCallbackOnlyOnce在初始化时为false，若更新失败说明该回调方法已经执行过了，故不在执行；
    C.5）最后调用SemaphoreReleaseOnlyOnce对象的realse，释放信号量；
**/
 &nbsp; &nbsp;public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
 &nbsp; &nbsp; &nbsp; &nbsp;throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
 &nbsp; &nbsp; &nbsp; &nbsp;long beginStartTime = System.currentTimeMillis();
 &nbsp; &nbsp; &nbsp; &nbsp;final Channel channel = this.getAndCreateChannel(addr);
 &nbsp; &nbsp; &nbsp; &nbsp;if (channel != null &amp;&amp; channel.isActive()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;try {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (this.rpcHook != null) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.rpcHook.doBeforeRequest(addr, request);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;long costTime = System.currentTimeMillis() - beginStartTime;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (timeoutMillis &lt; costTime) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw new RemotingTimeoutException("invokeSync call timeout");
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (this.rpcHook != null) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.rpcHook.doAfterResponse(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return response;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } catch (RemotingSendRequestException e) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("invokeSync: send request exception, so close the channel[{}]", addr);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.closeChannel(addr, channel);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw e;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  } catch (RemotingTimeoutException e) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (nettyClientConfig.isClientCloseSocketIfTimeout()) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.closeChannel(addr, channel);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw e;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.closeChannel(addr, channel);
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;throw new RemotingConnectException(addr);
 &nbsp; &nbsp; &nbsp;  }
 &nbsp;  }</code></pre>
<p style="">getMQClientAPIImpl().pullMessage最终通过channel写入并刷新队列中。然后在消息服务端大体的处理逻辑是服务端收到新消息请求后，如果队列中没有消息不急于返回，通过一个循环状态，每次waitForRunning一段时间默认5秒，然后再check，如果broker一直没有新新消息，第三次check的时间等到时间超过SuspendMaxTimeMills就返回空，如果在等待过程中收到了新消息直接调用notifyMessageArriving函数返回请求结果。“长轮询”的核心是，Broker端HOLD住客户端过来的请求一小段时间，在这个时间内有新消息到达，就利用现有的连接立刻返回消息给 Consumer 。长轮询的主动权掌握在consumer中，即使broker有大量的消息堆积也不会主动推送给consumer。</p>
<p style=""></p>
<blockquote>
 <p style="">参考资料：</p>
 <p style="">RocketMQ源码-<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq">https://github.com/apache/rocketmq</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq">https://blog.csdn.net/meilong_whpu/article/details/77076298</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq">https://blog.csdn.net/a417930422/article/details/50700281</a></p>
 <p style=""></p>
 <p style="">版权声明：本文为CSDN博主「拉里·佩奇」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。</p>
 <p style="">原文链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq">https://blog.csdn.net/fuyuwei2015/article/details/85219829</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299897244</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>RocketMq技术</category><pubDate>Mon, 11 Dec 2023 13:05:04 GMT</pubDate></item><item><title><![CDATA[Spring Boot整合RocketMQ之事务消息]]></title><link>https://xiaoming728.com/archives/1702299918707</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Spring%20Boot%E6%95%B4%E5%90%88RocketMQ%E4%B9%8B%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF&amp;url=/archives/1702299918707" width="1" height="1" alt="" style="opacity:0;">
<p style="">上次简单的了解了一下在<code>Spring Boot</code>下通过使用<code>rocketmq-spring-boot-starter</code>进行普通消息的发送、接收以及使用集群模式来模拟实现广播模式，<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.jianshu.com/p/a780386ec36b">文章链接</a>。今天来学习一下<code>RocketMQ</code>事务消息的发送。</p>
<p style=""><code>RocketMQ</code>的事务消息分为3种状态，分别是提交状态、回滚状态、中间状态：</p>
<blockquote>
 <p style="">TransactionStatus.CommitTransaction: 提交事务，它允许消费者消费此消息。</p>
 <p style="">TransactionStatus.RollbackTransaction: 回滚事务，它代表该消息将被删除，不允许被消费。</p>
 <p style="">TransactionStatus.Unknown: 中间状态，它代表需要检查消息队列来确定状态。</p>
</blockquote>
<p style="">当然因为在项目中我使用的是<code>rocketmq-spring-boot-starter</code>，所以表述上略有不同，但是本质是一样的。</p>
<p style="">事务消息在解决分布式事务的场景中感觉还是很有用的，虽然我们现在项目的分布式事务是通过<code>Seata</code>来实现的，但是通过事务消息或者消息的最终一次性也是可以的。</p>
<p style="">事务消息总共分为3个阶段：发送Prepared消息、执行本地事务、发送确认消息。这三个阶段是前后关联的，只有发送Prepared消息成功，才会执行本地事务，本地事务返回的状态是提交，那么就会发送最终的确认消息。如果在结束消息事务时，本地事务状态失败，那么<code>Broker</code>回查线程定时（默认1分钟）扫描每个存储事务状态的表格文件，如果是已经提交或者回滚的消息直接跳过，如果是Prepared状态则会向生产者发起一个检查本地事务的请求。</p>
<h3 style="" id="%E4%B8%80%E3%80%81%E4%BB%A3%E7%A0%81%E4%BF%AE%E6%94%B9">一、代码修改</h3>
<p style=""><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">首先我创建有一个</span><code>Service</code><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">来发送事务消息，代码没有什么特殊的含义，只是拿来当一个demo，代码如下：</span></p>
<pre><code>public Boolean save(OrderEntity orderEntity) {
    Message&lt;OrderEntity&gt; message = MessageBuilder.withPayload(orderEntity).build();

    log.info("&gt;&gt;&gt;&gt; send tx message start,tx_group={},destination={},payload={} &lt;&lt;&lt;&lt;",TX_GROUP,ORDER_TOPIC + ORDER_TAG,orderEntity);
    TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction("tx_order","order_topic:" + "tx_tag",message,orderEntity.getUserName());
    String sendStatus = sendResult.getSendStatus().name();
    String localTXState = sendResult.getLocalTransactionState().name();
    log.info("&gt;&gt;&gt;&gt; send status={},localTransactionState={} &lt;&lt;&lt;&lt;",sendStatus,localTXState);
    return Boolean.TRUE;
}</code></pre>
<p style="">使用<code>RocketMQTemplate</code>发送事务消息和普通消息略有不同的是，需要指一个事务生产者组，当然如果传入<code>null</code>，则会使用默认值<code>rocketmq_transaction_default_global_name</code>，发生消息的地址和普通消息一样都<code>Topic:Tag</code>，另外一点不同的是除了发生的<code>Message</code>之外，还可以发送其他的额外参数，不过这些参数只会在执行本地事务的时候会用到。</p>
<p style="">接下来我们创建一个消息的监听器（消费者），这个和普通消息的监听器一样，代码如下：</p>
<pre><code>@Component
@RocketMQMessageListener(consumerGroup = "tx_consumer",topic = "order_topic")
public class OrderListener implements RocketMQListener&lt;String&gt;{

    @Override
    public void onMessage(String message) {
        log.info("&gt;&gt;&gt;&gt; message={} &lt;&lt;&lt;&lt;",message);
    }
}</code></pre>
<p style=""><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">除了消费者之外，我们还需要创建事务消息生产者端的消息监听器，注意是生产者，不是消费者，我们需要实现的是</span><code>RocketMQLocalTransactionListener</code><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">接口，代码如下：</span></p>
<pre><code>@RocketMQTransactionListener(txProducerGroup = "tx_order")
public class OrderTXMsgListener implements RocketMQLocalTransactionListener {

    @Autowired
    private UserRepository userRepository;

    private static final Gson GSON = new Gson();

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        log.info("&gt;&gt;&gt;&gt; TX message listener execute local transaction, message={},args={} &lt;&lt;&lt;&lt;",msg,arg);
        // 执行本地事务
        RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
        try {
            String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
            OrderEntity orderEntity = GSON.fromJson(jsonString, OrderEntity.class);
            String userName = (String) arg;
        } catch (Exception e) {
            log.error("&gt;&gt;&gt;&gt; exception message={} &lt;&lt;&lt;&lt;",e.getMessage());
            result = RocketMQLocalTransactionState.UNKNOWN;
        }
        return result;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        log.info("&gt;&gt;&gt;&gt; TX message listener check local transaction, message={} &lt;&lt;&lt;&lt;",msg.getPayload());
        // 检查本地事务
        RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
        try {
            String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
            OrderEntity orderEntity = GSON.fromJson(jsonString, OrderEntity.class);
        } catch (Exception e) {
            // 异常就回滚
            log.error("&gt;&gt;&gt;&gt; exception message={} &lt;&lt;&lt;&lt;",e.getMessage());
            result = RocketMQLocalTransactionState.ROLLBACK;
        }
        return result;
    }
}</code></pre>
<p style=""><code>@RocketMQTransactionListener</code>表明这个一个生产端的消息监听器，需要配置监听的事务消息生产者组。而实现<code>RocketMQLocalTransactionListener</code>接口，重写执行本地事务的方法和检查本地事务方法。下面，我们通过修改生产者端事务监听器的代码来观察代码的执行情况。</p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E6%B6%88%E6%81%AF%E4%BA%8B%E5%8A%A1%E6%B5%8B%E8%AF%95">二、消息事务测试</h3>
<p style=""><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">首先还是正常的启动项目，在执行本地事务方法中正常情况下返回的值是</span><code>COMMIT</code><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">，即提交事务，这种情况下消费者会直接消费消息，而略过检查本地事务的方法。调用该接口，项目日志输出如下：</span></p>
<pre><code>&gt;&gt;&gt;&gt; send tx message start,tx_group=tx_order,destination=order_topic:tx_tag,payload=OrderEntity(id=null, userName=lisi, price=8848.00, address=CN-SC-CD-05, createTime=null, updateTime=null, status=20) &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; TX message listener execute local transaction, message=GenericMessage [payload=byte[119], headers={rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_TRANSACTION_ID=C0A800690C3418B4AAC2842438960000, rocketmq_TAGS=tx_tag, id=f32f4848-9acf-20bb-2501-0e6088765897, contentType=application/json, timestamp=1595749766307}],args=lisi &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; send status=SEND_OK,localTransactionState=COMMIT_MESSAGE &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; message={"id":null,"userName":"lisi","price":8848.00,"address":"CN-SC-CD-05","createTime":null,"updateTime":null,"status":"20"} &lt;&lt;&lt;&lt;</code></pre>
<p style="">通过日志分析可以看出，在执行完本地事务方法之后，返回的本地事务状态是<code>COMMIT_MESSAGE</code>，接着消费者消费消息，和我们的预期是一样的。</p>
<p style="">接下来我们修改下执行本地事务的方法，让该方法返回状态为<code>RocketMQLocalTransactionState.UNKNOWN</code>，修改之后如下：</p>
<pre><code>    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        log.info("&gt;&gt;&gt;&gt; TX message listener execute local transaction, message={},args={} &lt;&lt;&lt;&lt;",msg,arg);
        // 执行本地事务
        RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
        try {
            String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
            OrderEntity orderEntity = GSON.fromJson(jsonString, OrderEntity.class);
            String userName = (String) arg;
            int r = 11 / 0;
        } catch (Exception e) {
            log.error("&gt;&gt;&gt;&gt; exception message={} &lt;&lt;&lt;&lt;",e.getMessage());
            result = RocketMQLocalTransactionState.UNKNOWN;
        }
        return result;
    }</code></pre>
<p style=""><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">这样因为发生异常，该方法返回的结果是</span><code>UNKNOWN</code><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">，根据上文的分析，执行本地事务方法之后应该会执行检查本地事务方法，重启项目之后，再次调用一下接口，查看日志输出如下：</span></p>
<pre><code>&gt;&gt;&gt;&gt; send tx message start,tx_group=tx_order,destination=order_topic:tx_tag,payload=OrderEntity(id=null, userName=zhangsan, price=90001.00, address=CN-SC-CD-02, createTime=null, updateTime=null, status=10) &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; TX message listener execute local transaction, message=GenericMessage [payload=byte[124], headers={rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC2842BF39A0000, rocketmq_TAGS=tx_tag, id=dfd215f4-2aa6-f377-d1a7-ebbe3875769a, contentType=application/json, timestamp=1595750272928}],args=zhangsan &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; exception message=/ by zero &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; send status=SEND_OK,localTransactionState=UNKNOW &lt;&lt;&lt;&lt;
HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m22s578ms430µs170ns).
&gt;&gt;&gt;&gt; TX message listener check local transaction, message=GenericMessage [payload=byte[124], headers={rocketmq_QUEUE_ID=0, TRANSACTION_CHECK_TIMES=1, rocketmq_TAGS=tx_tag, rocketmq_BORN_TIMESTAMP=1595750272923, rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_MESSAGE_ID=C0A8006900002A9F00000000000156AB, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC2842BF39A0000, rocketmq_SYS_FLAG=0, id=ea3c3a7a-23c6-5acf-4c0f-0fa42f795b41, rocketmq_BORN_HOST=192.168.0.105, contentType=application/json, timestamp=1595750310890}] &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; TX message listener check local transaction, message=GenericMessage [payload=byte[124], headers={rocketmq_QUEUE_ID=0, TRANSACTION_CHECK_TIMES=2, rocketmq_TAGS=tx_tag, rocketmq_BORN_TIMESTAMP=1595750272923, rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_MESSAGE_ID=C0A8006900002A9F0000000000015892, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC2842BF39A0000, rocketmq_SYS_FLAG=0, id=cddfa35c-c8b2-cb1b-dce7-a26c6888b99a, rocketmq_BORN_HOST=192.168.0.105, contentType=application/json, timestamp=1595750374536}] &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; message={"id":null,"userName":"zhangsan","price":90001.00,"address":"CN-SC-CD-02","createTime":null,"updateTime":null,"status":"10"} &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; message={"id":null,"userName":"zhangsan","price":90001.00,"address":"CN-SC-CD-02","createTime":null,"updateTime":null,"status":"10"} &lt;&lt;&lt;&lt;</code></pre>
<p style="">根据日志输出，在<code>Service</code>中返回的事务消息发送状态是<code>SEND_OK</code>，但是返回的本地事务状态是<code>UNKNOW</code>，所以需要执行检查本地事务方法，但是这里出现了一个问题就是检查本地事务方法执行了两次，而且事务消息也被消费了两次，感觉有点不正常了，但是检查发现两条信息日志中<code>rocketmq_TRANSACTION_ID</code>是一样的，这是什么情况？？会不会和<code>HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m22s578ms430µs170ns).</code>有关呢，因为当时自己使用的<code>DEBUG</code>模式，看代码停留了一段时间，这样导致<code>Broker</code>发起的第一个回查线程挂起，而这时<code>Broker</code>又启动了一个线程，从而执行了两次检查事务的代码，而该方法返回的是<code>COMMIT</code>，所以。</p>
<p style="">不使用<code>DEBUG</code>模式重新测试一下，日志如下：</p>
<pre><code>&gt;&gt;&gt;&gt; send tx message start,tx_group=tx_order,destination=order_topic:tx_tag,payload=OrderEntity(id=null, userName=wangwu, price=9876.00, address=CN-SC-CD-00, createTime=null, updateTime=null, status=10) &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; TX message listener execute local transaction, message=GenericMessage [payload=byte[121], headers={rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC28432E4130005, rocketmq_TAGS=tx_tag, id=464edcfe-09c1-cc4a-5ac3-f3df888b0102, contentType=application/json, timestamp=1595750727701}],args=wangwu &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; exception message=/ by zero &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; send status=SEND_OK,localTransactionState=UNKNOW &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; TX message listener check local transaction, message=GenericMessage [payload=byte[121], headers={rocketmq_QUEUE_ID=3, TRANSACTION_CHECK_TIMES=1, rocketmq_TAGS=tx_tag, rocketmq_BORN_TIMESTAMP=1595750727699, rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_MESSAGE_ID=C0A8006900002A9F0000000000016109, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC28432E4130005, rocketmq_SYS_FLAG=0, id=77765356-fc4d-6d05-3531-6a67fbbed7f7, rocketmq_BORN_HOST=192.168.0.105, contentType=application/json, timestamp=1595750790917}] &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; message={"id":null,"userName":"wangwu","price":9876.00,"address":"CN-SC-CD-00","createTime":null,"updateTime":null,"status":"10"} &lt;&lt;&lt;&lt;</code></pre>
<p style="">这里输出的日志信息又没有问题了，我个人认为上面应该就是<code>DEBUG</code>导致的，这里就不再探讨了。</p>
<p style="">接下来测试一下，在执行本地事务方法中返回<code>ROLLBACK</code>的情况，这里代码就省略了，直接返回<code>ROLLBACK</code>。日志输出如下：</p>
<pre><code>&gt;&gt;&gt;&gt; send tx message start,tx_group=tx_order,destination=order_topic:tx_tag,payload=OrderEntity(id=null, userName=zhaoliu, price=10000.00, address=CN-SC-CD-03, createTime=null, updateTime=null, status=10) &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; TX message listener execute local transaction, message=GenericMessage [payload=byte[123], headers={rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_TRANSACTION_ID=C0A800691A3A18B4AAC284F72B910000, rocketmq_TAGS=tx_tag, id=d5b24a82-8d8b-90ad-7322-adfe2c4f3026, contentType=application/json, timestamp=1595763591062}],args=zhaoliu &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; exception message=/ by zero &lt;&lt;&lt;&lt;
&gt;&gt;&gt;&gt; send status=SEND_OK,localTransactionState=ROLLBACK_MESSAGE &lt;&lt;&lt;&lt;</code></pre>
<p style="">没有执行检验本地事务的方法，和之前说的一样。到这里我觉得应该基本上可以明白生产者端消息监听器中两个方法的具体作用了，主要还是理解<code>RocketMQ</code>事务消息的基本原理。</p>
<p style="">校验本地事务方法的返回值和执行本地事务方法的返回值的作用是一样的，这里就不再测试了。</p>
<p style="">网上找了一个图，感觉非常的直观：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ymqi.png&amp;size=m" style="display: inline-block"></p>
<blockquote>
 <p style="">来源：简书 -非典型_程序员</p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.jianshu.com/p/a780386ec36b">https://www.jianshu.com/p/4d6329281a1e</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299918707</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>RocketMq技术</category><category>Java技术</category><pubDate>Mon, 11 Dec 2023 13:05:00 GMT</pubDate></item><item><title><![CDATA[RocketMQ如何保证消息的可靠性]]></title><link>https://xiaoming728.com/archives/1702299800700</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=RocketMQ%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E6%B6%88%E6%81%AF%E7%9A%84%E5%8F%AF%E9%9D%A0%E6%80%A7&amp;url=/archives/1702299800700" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><span style="font-size: 15px; color: rgb(136, 136, 136)">导读：消息的发送方式有哪几种？存储消息的可靠性面临哪些挑战？消费消息的确认机制是怎样的？本文通过分析消息流转的整个过程，从消息发送、消息存储和消息消费三个阶段介绍RocketMQ是如何保证消息的可靠性的。</span></p>
</blockquote>
<p style=""></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">分布式系统中一个重要的前提假设是所有的网络传输都是不可靠的，在网络传输不可靠的情况下，保证消息的可靠传输，除了进行重试投递别无他法。常用的绝大多数消息队列RocketMQ、RabbitMQ等在消息传输上都只能保证至少传输成功一次，也即（At least once），而不能保证只传输成功一次（Exactly once）。由于分布式系统网络的不可靠，可能就会出现消息丢失的现象，那么RocketMQ是如何最大限度的保证消息不丢失的呢？那就需要从消息的产生到最终消费的整个过程来分析，消息完整链路可以划分为以下三个阶段：</span></p>
<p style="text-align: justify; "></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">生产阶段：消息在 Producer 发送端创建出来，经过网络传输发送到 Broker 存储端。</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">存储阶段：消息在 Broker 端存储，如果是主备或者多副本，消息会在这个阶段被复制到其他的节点或者副本上。</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">消费阶段：Consumer 消费端从 Broker存储端拉取消息，经过网络传输发送到 Consumer 消费端上，并通过重试来最大限度的保证消息的消费。</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<h3 style="" id="%E4%B8%80-%E5%8F%91%E9%80%81%E7%AB%AF%E6%B6%88%E6%81%AF%E5%8F%AF%E9%9D%A0%E6%80%A7">一&nbsp; 发送端消息可靠性</h3>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">发送端Producer发送消息Broker端的核心逻辑如下图所示：</span></p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-hetf.png&amp;size=m" style="display: inline-block"></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)"><br></span></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">消息发送一般有以下几种方式：同步发送、异步发送以及单向发送，业务具体选择哪种方式进行消息发送，需要根据情况进行判断，下面具体介绍不同的发送方式实现的消息可靠性保证。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">1&nbsp; 同步发送</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">同步发送是指发送端在发送消息时，阻塞线程进行等待，直到服务器返回发送的结果。发送端如果需要保证消息的可靠性，防止消息发送失败，可以采用同步阻塞式的发送，然后同步检查Brocker返回的状态来判断消息是否持久化成功。如果发送超时或者失败，则会默认重试2次，RocketMQ选择至少传输成功一次的消息模型，但是有可能发生重复投递，因为网络传输是不可靠的，具体的重试策略可以参照第四小节。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">2&nbsp; 异步发送</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">异步发送是指发送端在发送消息时，传入回调接口实现类，调用该发送接口后不会阻塞，发送方法会立即返回，回调任务会在另一个线程中执行，消息发送结果会回传给相应的回调函数。具体的业务实现可以根据发送的结果信息来判断是否需要重试来保证消息的可靠性。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">3&nbsp; 单向发送</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">单向发送是指发送端发送完成之后，调用该发送接口后立刻返回，并不返回发送的结果，业务方无法根据发送的状态来判断消息是否发送成功，单向发送相对前两种发送方式来说是一种不可靠的消息发送方式，因此要保证消息发送的可靠性，不推荐采用这种方式来发送消息。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">4&nbsp; 发送重试策略</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">RocketMQ架构模型中会有多个Borker为某个topic提供服务，一个topic下的消息分散存储在多个Broker存储端，它们是多对多关系。Broker会将其提供存储服务的topic的元数据信息上报到NameServer，对等NameServer节点组成的高可用服务会维护topic与Broker之间的映射关系，多对多的映射关系为消息可以重试发送到多个Broker端提供了前提与基础。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">当发送端需要发送消息时，如果发送端中缓存了topic的路由信息，并包含了消息队列,则直接返回该路由信息,如果没有缓存或没有消息队列，则向NameServer查询该topic的路由信息，查询到路由消息之后，采用指定的队列选择策略选择相应的queue发送消息，默认是采用轮询策略，发送成功则返回, 收到异常则根据相应的策略进行重试，可以根据发送端感知到的Broker的时延、上次发送失败的Broker信息和发送端配置的是否重试不同Broker的参数以及发送端设置的最大超时时间等等策略来灵活地实现不同等级的消息发送可靠性保证。重试策略可以有效的保证消息发送成功的概率，最终提高消息发送的可靠性。</span></p>
<p style="text-align: justify; "></p>
<h3 style="" id="%E4%BA%8C-%E5%AD%98%E5%82%A8%E7%AB%AF%E6%B6%88%E6%81%AF%E5%8F%AF%E9%9D%A0%E6%80%A7">二&nbsp; 存储端消息可靠性</h3>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">RocketMQ的消息存储结构如下图所示：</span></p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ojuz.png&amp;size=m" style="display: inline-block"></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">消息队列存储的最小单位是消息Message。</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">同一个Topic下的消息映射成多个逻辑队列。</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">不同Topic的消息按照到达broker的先后顺序以Append的方式添加至CommitLog，顺序写，随机读。</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">目前RocketMQ存储模型使用本地磁盘进行存储，数据写入为producer -&gt; direct memory -&gt; pagecache -&gt; 磁盘，数据读取如果pagecache有数据则直接从pagecache读，否则需要先从磁盘加载到pagecache中。Broker存储节点的文件存储模式如下图所示：</span></p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-nfcq.png&amp;size=m" style="display: inline-block"></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">Broker端CommitLog采用顺序写，可以大大提高写入效率，同时采用不同的刷盘模式提供不同的数据可靠性保证，此外采用了ConsumeQueue中间结构来存储偏移量信息，实现消息的分发。由于ConsumeQueue结构固定且大小有限，在实际情况中，大部分的ConsumeQueue 能够被全部读入内存，可以达到内存读取的速度。此外为了保证CommitLog和ConsumeQueue的一致性， CommitLog里存储了Consume Queues 、Message Key、Tag等所有信息，即使ConsumeQueue丢失，也可以通过 commitLog完全恢复出来，这样只要保证commitLog数据的可靠性，就可以保证Consume Queue的可靠性。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">RocketMQ存储端采用本地磁盘进行CommitLog消息数据的存储，不可避免的就会带来存储可靠性的挑战，如何保证消息不丢失，RocketMQ消息服务一直在不断提高数据的可靠性。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">1&nbsp; 存储可靠性挑战</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">RocketMQ存储端也即Broker端在存储消息的时候会面临以下的存储可靠性挑战：</span></p>
<p style="text-align: justify; "></p>
<ol>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">Broker正常关闭</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">Broker异常Crash</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">OS Crash</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">机器掉电，但是能立即恢复供电情况</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">机器无法开机（可能是cpu、主板、内存等关键设备损坏）</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">磁盘设备损坏</span></p></li>
</ol>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">1正常关闭，Broker 可以正常启动并恢复所有数据。2、3、4同步刷盘可以保证数据不丢失，异步刷盘可能导致少量数据丢失。5、6属于单点故障，且无法恢复。解决单点故障可以采用增加Slave节点，主从异步复制仍然可能有极少量数据丢失，同步复制可以完全避免单点问题。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">这里一般来说就需要在性能和可靠性之间做出取舍，对于RocketMQ来说，Broker的可靠性主要由两个方面保障：</span></p>
<p style="text-align: justify; "></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">单机的刷盘机制</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">主从之间的数据复制</span></p></li>
</ul>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">如果设置为每条消息都强制刷盘、主从复制，那么性能无疑会降低；如果不这样设置，就会有一定的可能性丢失消息。RocketMQ一般都是先把消息写到PageCache中，然后再持久化到磁盘上，数据从pagecache刷新到磁盘有两种方式，同步和异步。整体的消息写入和读取如下图所示：</span></p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-prww.png&amp;size=m" style="display: inline-block"></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">针对broker端单机存储可靠性，主要依赖单机的刷盘策略，主从之间的副本复制可以参考下一章节的主从模式。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">2&nbsp; 同步刷盘</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">消息写入内存的 PageCache后，立刻通知刷盘线程刷盘，然后等待刷盘完成，刷盘线程执行完成后唤醒等待的线程，返回消息写成功的状态。这种方式可以保证数据绝对安全，但是吞吐量不大。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">3&nbsp; 异步刷盘（默认）</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">消息写入到内存的 PageCache中，就立刻给客户端返回写操作成功，当 PageCache中的消息积累到一定的量时，触发一次写操作，或者定时等策略将 PageCache中的消息写入到磁盘中。这种方式吞吐量大，性能高，但是 PageCache中的数据可能丢失，不能保证数据绝对的安全。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">实际应用中要结合业务场景，合理设置刷盘方式，尤其是同步刷盘的方式，由于频繁的触发磁盘写动作，会明显降低性能。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">4&nbsp; 过期文件删除</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">由于RocketMQ操作CommitLog、ConsumeQueue文件是基于文件内存映射机制，并且在启动的时候会将所有的文件加载，为了避免内存与磁盘的浪费、能够让磁盘能够循环利用、避免因为磁盘不足导致消息无法写入等引入了文件过期删除机制。最终使得磁盘水位保持在一定水平，最终保证新写入消息的可靠存储。</span></p>
<p style="text-align: justify; "></p>
<h3 style="" id="%E4%B8%89-%E6%B6%88%E8%B4%B9%E7%AB%AF%E6%B6%88%E6%81%AF%E5%8F%AF%E9%9D%A0%E6%80%A7">三&nbsp; 消费端消息可靠性</h3>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">RockerMQ默认提供了至少消费一次的消费语义来保证消息的可靠消费。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">通常消费消息的确认机制一般分为两种思路：</span></p>
<p style="text-align: justify; "></p>
<ol>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">先提交后消费</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(62, 62, 62)">先消费，消费成功后再提交</span></p></li>
</ol>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">思路1可以解决重复消费的问题但是会丢失消息，因此RocketMQ默认实现的是思路2，由各自consumer业务方保证幂等来解决重复消费问题。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">消费端Consumer消费消息核心逻辑如下图所示：</span></p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-shfa.png&amp;size=m" style="display: inline-block"></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">1&nbsp; 消费重试</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">消费者从RocketMQ拉取到消息之后，需要返回消费成功来表示业务方正常消费完成。因此只有返回CONSUME_SUCCESS才算消费完成，如果返回CONSUME_LATER则会按照不同的messageDelayLevel时间进行再次消费，时间分级从秒到小时，最长时间为2个小时后再次进行消费重试，如果消费满16次之后还是未能消费成功，则不再重试，会将消息发送到死信队列，从而保证消息存储的可靠性。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">2&nbsp; 死信队列</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">未能成功消费的消息，消息队列并不会立刻将消息丢弃，而是将消息发送到死信队列，其名称是在原队列名称前加%DLQ%，如果消息最终进入了死信队列，则可以通过RocketMQ提供的相关接口从死信队列获取到相应的消息，保证了消息消费的可靠性。</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(255, 106, 0)">3&nbsp; 消息回溯</span></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">回溯消费是指Consumer已经消费成功的消息，或者之前消费业务逻辑有问题，现在需要重新消费。要支持此功能，则Broker存储端在向Consumer消费端投递成功消息后，消息仍然需要保留。重新消费一般是按照时间维度，例如由于Consumer系统故障，恢复后需要重新消费1小时前的数据。RocketMQ Broker提供了一种机制，可以按照时间维度来回退消费进度，这样就可以保证只要发送成功的消息，只要消息没有过期，消息始终是可以消费到的。</span></p>
<p style="text-align: justify; "></p>
<h3 style="" id="%E5%9B%9B-%E6%80%BB%E7%BB%93">四&nbsp; 总结</h3>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 15px; color: rgb(62, 62, 62)">本文从消息流转的整个过程分析了RocketMQ如何保证消息的可靠性，消息发送通过不同的重试策略保证了消息的可靠发送，消息存储通过不同的刷盘机制以及多副本来保证消息的可靠存储，消息消费通过至少消费成功一次以及消费重试机制来保证消息的可靠消费，RocketMQ在保证消息的可靠性上做到了全链路闭环，最大限度的保证了消息不丢失。</span></p>
<p style="text-align: justify; "></p>
<blockquote>
 <p style="text-align: justify; "><span style="font-size: 12px; color: rgba(0, 0, 0, 0.3)">原创</span><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span><span style="font-size: 15px; color: rgba(0, 0, 0, 0.3)">修戟</span><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span><span style="font-size: 15px; color: rgb(51, 51, 51)">阿里技术</span><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span>2月2日</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;amp;mid=2247502152&amp;amp;idx=1&amp;amp;sn=3c356a4b65d50e964f0350a13ba08df3&amp;amp;chksm=e92af447de5d7d510b5c5f8e0e850d0c1ed717f74c503642ff3a879d204900f7015d03bded06&amp;amp;scene=178&amp;amp;cur_album_id=1409150425835831296#rd">https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&amp;mid=2247502152&amp;idx=1&amp;sn=3c356a4b65d50e964f0350a13ba08df3&amp;chksm=e92af447de5d7d510b5c5f8e0e850d0c1ed717f74c503642ff3a879d204900f7015d03bded06&amp;scene=178&amp;cur_album_id=1409150425835831296#rd</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299800700</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>RocketMq技术</category><pubDate>Mon, 11 Dec 2023 13:04:12 GMT</pubDate></item><item><title><![CDATA[RocketMQ中的Consumer]]></title><link>https://xiaoming728.com/archives/1702299864190</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=RocketMQ%E4%B8%AD%E7%9A%84Consumer&amp;url=/archives/1702299864190" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="mq%E4%B8%AD%E6%9C%89%E5%85%B3consumer%E7%AE%80%E4%BB%8B">MQ中有关Consumer简介</h3>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-jwcw.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">上述就是MQ中有关Consumer的类图，下面来介绍一下每个类：</span></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">MQConsumer：Consumer公共的接口，常用的方法如下</span></p>
<p style="margin-left: 2px!important;"><span style="font-size: 16px; color: rgb(77, 77, 77)">1、MQAdmin：底层类</span></p>
<p style="margin-left: 2px!important;"><span style="font-size: 16px; color: rgb(77, 77, 77)">2、MQConsumer：Consumer公共的接口</span></p>
<p style="margin-left: 2px!important;"><span style="font-size: 16px; color: rgb(77, 77, 77)">3、MQPushConsumer：Consumer的一种，应用通常向Consumer对象注册一个Listener接口，一旦收到消息，Consumer对象立刻回调Listener接口方法</span></p>
<p style="margin-left: 2px!important;"><span style="font-size: 16px; color: rgb(77, 77, 77)">4、MQPullConsumer：Consumer的一种，应用通常主动调用Consumer的拉消息方法从Broker拉消息，主动权由应用控制</span></p>
<p style=""><span style="font-size: 16px; color: rgba(0, 0, 0, 0.75)">一般在应用中都会采用push的方法来自动的消费信息&nbsp;&nbsp;&nbsp;</span></p>
<p style=""></p>
<h3 style="" id="pushconsumer%EF%BC%9A%E9%80%9A%E8%BF%87%E6%B3%A8%E5%86%8C%E7%9B%91%E5%90%AC%E7%9A%84%E6%96%B9%E5%BC%8F%E6%9D%A5%E6%B6%88%E8%B4%B9%E4%BF%A1%E6%81%AF">PushConsumer：通过注册监听的方式来消费信息</h3>
<pre><code>package&nbsp;com.test;&nbsp;&nbsp;
&nbsp;&nbsp;
import&nbsp;java.util.List;&nbsp;&nbsp;
&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.common.message.Message;&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.common.message.MessageExt;&nbsp;&nbsp;
&nbsp;&nbsp;
/**&nbsp;
&nbsp;*&nbsp;@ClassName:&nbsp;Consumer&nbsp;
&nbsp;*&nbsp;@Description:&nbsp;模拟消费者&nbsp;
&nbsp;*&nbsp;@author:&nbsp;LUCKY&nbsp;
&nbsp;*&nbsp;@date:2015年12月28日&nbsp;下午2:43:23&nbsp;
&nbsp;*/&nbsp;&nbsp;
public&nbsp;class&nbsp;ConsumerTest&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;main(String[]&nbsp;args)&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DefaultMQPushConsumer&nbsp;consumer=new&nbsp;DefaultMQPushConsumer("broker-a");&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;consumer.setNamesrvAddr("100.66.154.81:9876");&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;订阅PushTopic下Tag为push的消息,都订阅消息&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;consumer.subscribe("PushTopic",&nbsp;"push");&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;程序第一次启动从消息队列头获取数据&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//可以修改每次消费消息的数量，默认设置是每次消费一条&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;consumer.setConsumeMessageBatchMaxSize(10);&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//注册消费的监听&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;consumer.registerMessageListener(new&nbsp;MessageListenerConcurrently()&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//在此监听中消费信息，并返回消费的状态信息&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;ConsumeConcurrentlyStatus&nbsp;consumeMessage(List&lt;MessageExt&gt;&nbsp;msgs,&nbsp;ConsumeConcurrentlyContext&nbsp;context)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;msgs中只收集同一个topic，同一个tag，并且key相同的message&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;会把不同的消息分别放置到不同的队列中&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for(Message&nbsp;msg:msgs){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(new&nbsp;String(msg.getBody()));&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;ConsumeConcurrentlyStatus.CONSUME_SUCCESS;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;});&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;consumer.start();&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Thread.sleep(5000);&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//5秒后挂载消费端消费&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;consumer.suspend();&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;catch&nbsp;(Exception&nbsp;e)&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;e.printStackTrace();&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;
}&nbsp;&nbsp;</code></pre>
<h3 style="" id="pullconsumer%EF%BC%9A%E9%80%9A%E8%BF%87%E6%8B%89%E5%8E%BB%E7%9A%84%E6%96%B9%E5%BC%8F%E6%9D%A5%E6%B6%88%E8%B4%B9%E6%B6%88%E6%81%AF">PullConsumer：通过拉去的方式来消费消息</h3>
<pre><code>/**&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;*&nbsp;@FileName:&nbsp;Consumer.java&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;*&nbsp;@Package:com.test&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;*&nbsp;@Description:&nbsp;TODO&nbsp;&nbsp;&nbsp;
&nbsp;*&nbsp;@author:&nbsp;LUCKY&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;*&nbsp;@date:2015年12月28日&nbsp;下午2:43:23&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;*&nbsp;@version&nbsp;V1.0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;*/&nbsp;&nbsp;
package&nbsp;com.test;&nbsp;&nbsp;
&nbsp;&nbsp;
import&nbsp;java.util.Set;&nbsp;&nbsp;
&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.client.consumer.DefaultMQPullConsumer;&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.client.consumer.MessageQueueListener;&nbsp;&nbsp;
import&nbsp;com.alibaba.rocketmq.common.message.MessageQueue;&nbsp;&nbsp;
&nbsp;&nbsp;
/**&nbsp;
&nbsp;*&nbsp;@ClassName:&nbsp;Consumer&nbsp;
&nbsp;*&nbsp;@Description:&nbsp;模拟消费者&nbsp;
&nbsp;*&nbsp;@author:&nbsp;LUCKY&nbsp;
&nbsp;*&nbsp;@date:2015年12月28日&nbsp;下午2:43:23&nbsp;
&nbsp;*/&nbsp;&nbsp;
public&nbsp;class&nbsp;ConsumerPullTest&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;main(String[]&nbsp;args)&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DefaultMQPullConsumer&nbsp;consumer=new&nbsp;DefaultMQPullConsumer();&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;consumer.setNamesrvAddr("100.66.154.81:9876");&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	consumer.setConsumerGroup("broker");&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;consumer.start();&nbsp;&nbsp;
            Set&lt;MessageQueue&gt;&nbsp;messageQueues =&nbsp;consumer.fetchSubscribeMessageQueues("PushTopic");&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

            for(MessageQueue&nbsp;messageQueue:messageQueues){&nbsp;&nbsp;
                System.out.println(messageQueue.getTopic());&nbsp;&nbsp;
            }&nbsp;&nbsp;

            //消息队列的监听&nbsp;&nbsp;
            consumer.registerMessageQueueListener("",&nbsp;new&nbsp;MessageQueueListener()&nbsp;{
                @Override&nbsp;&nbsp;
                //消息队列有改变，就会触发&nbsp;&nbsp;
                public&nbsp;void&nbsp;messageQueueChanged(String&nbsp;topic,&nbsp;Set&lt;MessageQueue&gt;&nbsp;mqAll, Set&lt;MessageQueue&gt;&nbsp;mqDivided)&nbsp;{&nbsp;&nbsp;
                    //&nbsp;TODO&nbsp;Auto-generated&nbsp;method&nbsp;stub
                }&nbsp;&nbsp;
            });
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;catch&nbsp;(Exception&nbsp;e)&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;e.printStackTrace();&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;
}</code></pre>
<p style=""></p>
<blockquote>
 <p style="">版权声明：本文为CSDN博主「很漂亮的孔雀」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。</p>
 <p style="">原文链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/qq_43747119/article/details/86061818">https://blog.csdn.net/qq_43747119/article/details/86061818</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299864190</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>RocketMq技术</category><pubDate>Mon, 11 Dec 2023 13:04:00 GMT</pubDate></item><item><title><![CDATA[RocketMQ控制台使用文档]]></title><link>https://xiaoming728.com/archives/1702299779022</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=RocketMQ%E6%8E%A7%E5%88%B6%E5%8F%B0%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3&amp;url=/archives/1702299779022" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="%E8%BF%90%E7%BB%B4%E9%A1%B5%E9%9D%A2">运维页面</h2>
<ul>
 <li>
  <p style="">你可以修改这个服务使用的namesrv的地址</p></li>
 <li>
  <p style="">你可以修改这个服务是否使用VIPChannel(如果你的mq server版本小于3.5.8，请设置不使用)</p></li>
</ul>
<h2 style="" id="%E9%A9%BE%E9%A9%B6%E8%88%B1">驾驶舱</h2>
<ul>
 <li>
  <p style="">查看broker的消息量（总量/5分钟图）</p></li>
 <li>
  <p style="">查看单一主题的消息量（总量/趋势图）</p></li>
</ul>
<h2 style="" id="%E9%9B%86%E7%BE%A4%E9%A1%B5%E9%9D%A2">集群页面</h2>
<ul>
 <li>
  <p style="">查看集群的分布情况</p></li>
</ul>
<ul>
 <li>
  <p style="">cluster与broker关系</p></li>
 <li>
  <p style="">broker</p></li>
</ul>
<ul>
 <li>
  <p style="">查看broker具体信息/运行信息</p></li>
 <li>
  <p style="">查看broker配置信息</p></li>
</ul>
<h2 style="" id="%E4%B8%BB%E9%A2%98%E9%A1%B5%E9%9D%A2">主题页面</h2>
<ul>
 <li>
  <p style="">展示所有的主题，可以通过搜索框进行过滤</p></li>
 <li>
  <p style="">筛选 普通/重试/死信 主题</p></li>
 <li>
  <p style="">添加/更新主题</p></li>
</ul>
<ul>
 <li>
  <p style="">clusterName 创建在哪几个cluster上</p></li>
 <li>
  <p style="">brokerName 创建在哪几个broker上</p></li>
 <li>
  <p style="">topicName 主题名</p></li>
 <li>
  <p style="">writeQueueNums 写队列数量</p></li>
 <li>
  <p style="">readQueueNums 读队列数量</p></li>
 <li>
  <p style="">perm //2是写 4是读 6是读写</p></li>
</ul>
<ul>
 <li>
  <p style="">状态 查询消息投递状态（投递到哪些broker/哪些queue/多少量等）</p></li>
 <li>
  <p style="">路由 查看消息的路由（现在你发这个主题的消息会发往哪些broker，对应broker的queue信息）</p></li>
 <li>
  <p style="">CONSUMER管理（这个topic都被哪些group消费了，消费情况何如）</p></li>
 <li>
  <p style="">topic配置（查看变更当前的配置）</p></li>
 <li>
  <p style="">发送消息（向这个主题发送一个测试消息）</p></li>
 <li>
  <p style="">重置消费位点(分为在线和不在线两种情况，不过都需要检查重置是否成功)</p></li>
 <li>
  <p style="">删除主题 （会删除掉所有broker以及namesrv上的主题配置和路由信息）</p></li>
</ul>
<h2 style="" id="%E6%B6%88%E8%B4%B9%E8%80%85%E9%A1%B5%E9%9D%A2">消费者页面</h2>
<ul>
 <li>
  <p style="">展示所有的消费组，可以通过搜索框进行过滤</p></li>
 <li>
  <p style="">刷新页面/每隔五秒定时刷新页面</p></li>
 <li>
  <p style="">按照订阅组/数量/TPS/延迟 进行排序</p></li>
 <li>
  <p style="">添加/更新消费组</p></li>
</ul>
<ul>
 <li>
  <p style="">clusterName 创建在哪几个集群上</p></li>
 <li>
  <p style="">brokerName 创建在哪几个broker上</p></li>
 <li>
  <p style="">groupName 消费组名字</p></li>
 <li>
  <p style="">consumeEnable //是否可以消费 FALSE的话将无法进行消费</p></li>
 <li>
  <p style="">consumeBroadcastEnable //是否可以广播消费</p></li>
 <li>
  <p style="">retryQueueNums //重试队列的大小</p></li>
 <li>
  <p style="">brokerId //正常情况从哪消费</p></li>
 <li>
  <p style="">whichBrokerWhenConsumeSlowly//出问题了从哪消费</p></li>
</ul>
<ul>
 <li>
  <p style="">终端 在线的消费客户端查看，包括版本订阅信息和消费模式</p></li>
 <li>
  <p style="">消费详情 对应消费组的消费明细查看，这个消费组订阅的所有Topic的消费情况，每个queue对应的消费client查看（包括Retry消息）</p></li>
 <li>
  <p style="">配置 查看变更消费组的配置</p></li>
 <li>
  <p style="">删除 在指定的broker上删除消费组</p></li>
</ul>
<h2 style="" id="%E5%8F%91%E5%B8%83%E7%AE%A1%E7%90%86%E9%A1%B5%E9%9D%A2">发布管理页面</h2>
<ul>
 <li>
  <p style="">通过Topic和Group查询在线的消息生产者客户端</p></li>
</ul>
<ul>
 <li>
  <p style="">信息包含客户端主机 版本</p></li>
</ul>
<h2 style="" id="%E6%B6%88%E6%81%AF%E6%9F%A5%E8%AF%A2%E9%A1%B5%E9%9D%A2">消息查询页面</h2>
<ul>
 <li>
  <p style="">根据Topic和时间区间查询 *由于数据量大 最多只会展示2000条，多的会被忽略</p></li>
 <li>
  <p style="">根据Topic和Key进行查询</p></li>
</ul>
<ul>
 <li>
  <p style="">最多只会展示64条</p></li>
</ul>
<ul>
 <li>
  <p style="">根据消息主题和消息Id进行消息的查询</p></li>
 <li>
  <p style="">消息详情可以展示这条消息的详细信息，查看消息对应到具体消费组的消费情况（如果异常，可以查看具体的异常信息）。可以向指定的消费组重发消息。</p></li>
</ul>
<h2 style="" id="https-%E6%96%B9%E5%BC%8F%E8%AE%BF%E9%97%AEconsole">HTTPS 方式访问Console</h2>
<ul>
 <li>
  <p style="">HTTPS功能实际上是使用SpringBoot提供的配置功能即可完成，首先，需要有一个SSL KeyStore来存放服务端证书，可以使用本工程所提供的测试密钥库: resources/rmqcngkeystore.jks, 它可以通过如下keytool命令生成</p></li>
</ul>
<pre><code>#生成库并以rmqcngKey别名添加秘钥
keytool -genkeypair -alias rmqcngKey  -keyalg RSA -validity 3650 -keystore rmqcngkeystore.jks 
#查看keystore内容
keytool -list -v -keystore rmqcngkeystore.jks 
#转换库格式
keytool -importkeystore -srckeystore rmqcngkeystore.jks -destkeystore rmqcngkeystore.jks -deststoretype pkcs12</code></pre>
<ul>
 <li>
  <p style="">配置resources/<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq-externals/blob/master/rocketmq-console/doc/1_0_0/UserGuide_CN.md#%E9%A9%BE%E9%A9%B6%E8%88%B1">application.properties</a>, 打开SSL的相关选项, 启动console后即开启了HTTPS.</p></li>
</ul>
<pre><code>#设置https端口
server.port=8443
### SSL setting
#server.ssl.key-store=classpath:rmqcngkeystore.jks
#server.ssl.key-store-password=rocketmq
#server.ssl.keyStoreType=PKCS12
#server.ssl.keyAlias=rmqcngkey</code></pre>
<h2 style="" id="%E7%99%BB%E5%BD%95%E8%AE%BF%E9%97%AEconsole">登录访问Console</h2>
<p style="">在访问Console时支持按用户名和密码登录控制台，在操作完成后登出。需要做如下的设置:</p>
<ul>
 <li>
  <p style="">1.在Spring配置文件resources/<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq-externals/blob/master/rocketmq-console/doc/1_0_0/UserGuide_CN.md#%E9%A9%BE%E9%A9%B6%E8%88%B1">application.properties</a>中修改 开启登录功能</p></li>
</ul>
<pre><code># 开启登录功能
rocketmq.config.loginRequired=true
# Dashboard文件目录，登录用户配置文件所在目录
rocketmq.config.dataPath=/tmp/rocketmq-console/data</code></pre>
<ul>
 <li>
  <p style="">2.确保${rocketmq.config.dataPath}定义的目录存在，并且该目录下创建登录配置文件"<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq-externals/blob/master/rocketmq-console/doc/1_0_0/UserGuide_CN.md#%E9%A9%BE%E9%A9%B6%E8%88%B1">users.properties</a>", 如果该目录下不存在此文件，则默认使用resources/<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq-externals/blob/master/rocketmq-console/doc/1_0_0/UserGuide_CN.md#%E9%A9%BE%E9%A9%B6%E8%88%B1">users.properties</a>文件。 <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq-externals/blob/master/rocketmq-console/doc/1_0_0/UserGuide_CN.md#%E9%A9%BE%E9%A9%B6%E8%88%B1">users.properties</a>文件格式为:</p></li>
</ul>
<pre><code># 该文件支持热修改，即添加和修改用户时，不需要重新启动console
# 格式， 每行定义一个用户， username=password[,N]  #N是可选项，可以为0 (普通用户)； 1 （管理员）  
#定义管理员 
admin=admin,1
#定义普通用户
user1=user1
user2=user2</code></pre>
<ol>
 <li>
  <p style="">启动控制台则开启了登录功能</p></li>
</ol>
<p style="margin-left: 2px!important;"></p>
<blockquote>
 <p style="">文章引用</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/apache/rocketmq-externals/blob/master/rocketmq-console/doc/1_0_0/UserGuide_CN.md#%E9%A9%BE%E9%A9%B6%E8%88%B1">https://github.com/apache/rocketmq-externals/blob/master/rocketmq-console/doc/1_0_0/UserGuide_CN.md</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299779022</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>RocketMq技术</category><pubDate>Mon, 11 Dec 2023 13:03:00 GMT</pubDate></item><item><title><![CDATA[RocketMQ消息队列系统架构]]></title><link>https://xiaoming728.com/archives/1702299661100</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=RocketMQ%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84&amp;url=/archives/1702299661100" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="%EF%BC%881%EF%BC%89rocketmq%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84">（1）RocketMQ整体架构</h2>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">如今阿里的开源项目越来越多，比如消息中间件领域的RocketMQ，分布式事务领域的Fescar，熔断限流领域的Sentinal，微服务领域的Dubbo、Nacos等等。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">而现在越来越多的中小型公司也开始使用阿里开源的各种技术到自己的系统，因此有必要对阿里开源的一些技术的核心工作原理进行了解。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">本文就对消息中间件领域的 RocketMQ 进行原理的分析。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">首先，RocketMQ整体架构中包含了4种角色：</p>
<ul>
 <li>
  <p style="">NameServer</p></li>
 <li>
  <p style="">Broker</p></li>
 <li>
  <p style="">Producer</p></li>
 <li>
  <p style="">Consumer</p></li>
</ul>
<p style="text-align: justify; ">其中NameServer相当于集群管理服务，主要用于管理整个集群的元数据以及对集群进行管理的。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">Broker相当于单个节点负责数据的读写的，而Producer就是负责生产消息后写入Broker的，Consumer则是从Broker来消费消息。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">而且Broker在部署的时候，一般是分为主备角色的。也就是说，每个Broker都部署一个Master角色在一台机器上，然后另外会部署一个Slave角色在另外一台机器上。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">这样如果某个Broker Master挂了，他的Broker Slave就可以接替继续工作，保证了高可用。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">下图阐述了RocketMQ的整体架构原理：</p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ipcq.png&amp;size=m" style="display: inline-block"></p>
<p style="text-align: justify; "></p>
<h2 style="" id="%EF%BC%882%EF%BC%89rocketmq-%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B">（2）RocketMQ 工作流程</h2>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">首先需要先启动NameServer，接着启动Broker</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">Broker启动之后就会跟NameServer建立长连接，每隔一段时间发送心跳包过去</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">心跳包里需要包含自己当前存储的数据信息，让NameServer感知到各个Broker的情况，如下图：</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-dnjs.png&amp;size=m" style="display: inline-block"></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">然后呢？你可以创建 Topic，创建 Topic 的时候就会决定这个 Topic 的数据会放在哪些Broker上。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">每个Broker在进行心跳的时候，就可以上报自己当前存储的Topic相关的数据信息给NameServer知道了。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">而NameServer就保存了整个集群的元数据，知道每个Topic当前数据保存在哪些Broker上，如下图：</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-mrky.png&amp;size=m" style="display: inline-block"></p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">Producer在生产消息到Topic的时候，先得找NameServer问一下Topic存放在哪些Broker上，然后就可以跟那些Broker建立连接发送消息过去了。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">Consumer要从Topic消费消息，也会找NameServer问一下可以从哪些Broker上消费消息，接着同样会跟Broker建立连接并且开始消费消息，如下图。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-offr.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%EF%BC%883%EF%BC%89nameserver%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90">（3）NameServer原理分析</h2>
<p style=""></p>
<p style="text-align: justify; ">NameServer一般是集群化部署，各个机器上部署的NameServer会互相同步收到的元数据，保证各个NameServer上包含的集群元数据都是相同的。这样的架构可以保证即使任何一个NameServer宕机了也不要紧。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">NameServer主要的责任就是管理Broker的心跳，如果有人一段时间内没心跳，那么就认为那个Broker已经宕机了，此时要触发对应的Slave切换为Master保证高可用。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">此外，NameServer还要负责管理和维护集群元数据，比如有哪些Topic，每个Topic在哪些Broker上。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">NameServer承受的请求压力是很小的，因为心跳和拉取元数据，其实是很低频的行为，所以一般<strong>部署两三台机器</strong>就足够了。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">但是需要注意的是 ，如果集群里维护了几十万个，甚至几百万个Topic，会导致Broker每次心跳上报Topic数据时，带上几十万个Topic的数据信息，可能多达几十MB，那会导致心跳时网络负载过重。</p>
<p style="text-align: justify; "></p>
<h2 style="" id="%EF%BC%884%EF%BC%89%E5%88%86%E5%B8%83%E5%BC%8F%E6%9E%B6%E6%9E%84%E5%88%86%E6%9E%90">（4）分布式架构分析</h2>
<p style=""></p>
<p style="text-align: justify; ">再说说Topic，每个Topic都由很多个队列组成，一个队列代表了这个Topic的部分数据，然后这个Topic的多个队列会均匀分散在多个Broker上。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">比如说Topic有6个队列，对应有三台Broker，那么每个Broker上就可以放2个队列的数据。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">这样就可以做到每个Topic的数据都可以分布式存储在集群中的多台Broker上，形成了分布式的存储架构，这样就可以利用多台机器的资源来存储数据。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">同时，可以利用多台机器的资源来应对高并发的请求，因为对一个Topic的读写请求，就会均匀的落到多台Broker机器上去了。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">如下图所示，可以看到数据分布式存储的架构。</p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qive.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id=""></h2>
<h2 style="" id="%EF%BC%885%EF%BC%89broker%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90">（5）Broker原理分析</h2>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">Broker主要就是两点：负责接收Producer写入的消息，同时提供消息给Consumer来读取。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><strong><span fontsize="" color="rgb(201, 56, 28)" style="color: rgb(201, 56, 28)">写入消息时</span></strong>，Broker可以实现高并发高性能的写入。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">这个主要采用的是对磁盘文件进行顺序写的方式，磁盘顺序写的性能是很高的，几乎跟内存随机读写的性能差不多。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">另外，就是优先把数据写入os page cahce，也就是操作系统管理的缓存中。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">写操作系统的内存缓存，那性能就更高了，不需要每次都直接写入物理磁盘中。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><strong><span fontsize="" color="rgb(201, 56, 28)" style="color: rgb(201, 56, 28)">读取消息时</span></strong>，Broker也是主要基于os page cahce读取消息，也就是优先从操作系统管理的内存缓存中读取数据，提供给Consumer来消费。因为是优先读内存，所以同样性能也是很高的。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">通过这种架构，Broker就可以保证高并发以及高性能。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">另外，Broker还实现了高可用的架构。也就是说每个Broker Master都会把自己接收到的数据同步给Broker Slave，实现每个Broker上的数据备份。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">如果Broker Master宕机了，会切换Broker Slave对外提供服务，保证了高可用的架构。</p>
<p style="text-align: justify; "></p>
<h2 style="" id="%EF%BC%886%EF%BC%89consumer%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90">（6）Consumer原理分析</h2>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">如果我们部署了Consumer在多台机器上，默认也会把Topic下的多个队列的数据均匀的分配给各个Consumer实例来进行消费。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">比如Topic下一共6个队列，假设部署了3个Consumer实例，此时每个Consumer实例就会消费2个队列的数据，如下图：</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-aieb.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%EF%BC%887%EF%BC%89%E6%80%BB%E7%BB%93">（7）总结</h2>
<p style="text-align: justify; "></p>
<p style="text-align: justify; ">对于任何一个消息系统，对他的原理进行研究的时候，都应该关注几点：</p>
<p style="text-align: justify; "></p>
<ul>
 <li>
  <p style="">集群架构（元数据如何管理、集群如何管理）</p></li>
 <li>
  <p style="">数据模型（Topic、队列等概念）</p></li>
 <li>
  <p style="">分布式架构（消息如何分布式存储）</p></li>
 <li>
  <p style="">消息写入以及读取的原理</p></li>
 <li>
  <p style="">高可用架构（如何进行数据冗余）</p></li>
 <li>
  <p style="">客户端原理</p></li>
</ul>
<p style="text-align: justify; ">把这些弄明白之后，才算对一个消息系统有了一定的了解，才能开始尝试把这个消息系统应用于自己公司的生产项目中。</p>]]></description><guid isPermaLink="false">/archives/1702299661100</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>RocketMq技术</category><pubDate>Mon, 11 Dec 2023 13:02:06 GMT</pubDate></item><item><title><![CDATA[RocketMQ 解决：There is insufficient memory for the Java Runtime Environment to continue]]></title><link>https://xiaoming728.com/archives/1702299743976</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=RocketMQ%20%E8%A7%A3%E5%86%B3%EF%BC%9AThere%20is%20insufficient%20memory%20for%20the%20Java%20Runtime%20Environment%20to%20continue&amp;url=/archives/1702299743976" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1.%E5%9C%BA%E6%99%AF%E6%8F%8F%E8%BF%B0"><strong>1.场景描述</strong></h3>
<p style="">linux 安装 rocketmq 启动&nbsp;mqnameserver、mqbroker 以及运行测试类生产者时报错。</p>
<p style="">运行命令为：</p>
<pre><code>nohup sh bin/mqnamesrv &amp;
 
或者
nohup sh bin/mqbroker -n localhost:9876 &amp;
 
或者
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">报错内容为：</span></p>
<pre><code># There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 805306368 bytes for committing reserved memory.</code></pre>
<h3 style="" id="2.-%E5%8E%9F%E5%9B%A0">2. 原因</h3>
<p style="">默认运行内存设置过大，我查看我的好多默认设置都是大于1G ，甚至有的是 2G 、4G ....</p>
<p style=""></p>
<h3 style="" id="3.%E8%A7%A3%E5%86%B3">3.解决</h3>
<p style="">修改目录&nbsp;/distribution/target/apache-rocketmq/bin 下的 3 个配置文件：&nbsp;<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">runserver.sh</a>、<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">runbroker.sh</a> 、<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">tools.sh</a> 。</p>
<p style="">设置 <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">runserver.sh</a> 中此项配置 为：</p>
<pre><code>JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">设置 </span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html"><span style="font-size: 16px; color: rgb(77, 77, 77)">runbroker.sh</span></a><span style="font-size: 16px; color: rgb(77, 77, 77)"> 中此项配置 为：</span></p>
<pre><code> JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m"</code></pre>
<p style="">（我改了&nbsp;<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">runserver.sh</a>、<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">runbroker.sh</a> 依旧不行，后来注意到 <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">tools.sh</a> 中也有配置，3个都改就行了）</p>
<p style="">设置 <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">tools.sh</a> 中此项配置 为：</p>
<pre><code>JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128m"</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">完成以上操作，后续测试就成功运行了。</span></p>
<p style=""></p>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">参考：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">https://www.cnblogs.com/dingkailinux/p/8874726.html</a></p>
 <p style=""></p>
 <p style="">版权声明：本文为CSDN博主「微风--轻许--」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。</p>
 <p style="">原文链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/dingkailinux/p/8874726.html">https://blog.csdn.net/jiangyu1013/article/details/81486374</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299743976</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>RocketMq技术</category><pubDate>Mon, 11 Dec 2023 13:02:00 GMT</pubDate></item><item><title><![CDATA[Linux JAVA Web 环境搭建]]></title><link>https://xiaoming728.com/archives/1702299637081</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%20JAVA%20Web%20%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA&amp;url=/archives/1702299637081" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="%E5%AE%89%E8%A3%85jdk-17">安装JDK 17</h1>
<p style=""></p>
<h2 style="" id="%E4%B8%8B%E8%BD%BDjdk">下载JDK</h2>
<p style=""><span style="font-size: 14px">JDK下载地址：</span></p>
<pre><code>https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
https://repo.huaweicloud.com/java/jdk/11.0.2+9/jdk-11.0.2_osx-x64_bin.dmg
https://repo.huaweicloud.com/artifactory/java-local/jdk/8u202-b08/</code></pre>
<p style=""><span style="font-size: 14px">JDK版本：Java SE Development Kit 8u51&nbsp;&nbsp;Linux x64 &nbsp;&nbsp;jdk-8u51-linux-x64.tar.gz&nbsp;</span></p>
<p style=""></p>
<h2 style="" id="%E5%AE%89%E8%A3%85jdk">安装JDK</h2>
<p style=""><span style="font-size: 14px">安装步骤：</span></p>
<pre><code>1.&nbsp;mkdir&nbsp;java －在/opt下建立java目录下

2.&nbsp;tar zxvf ~/jdk-8u45-linux-x64.tar.gz &nbsp;-C /opt/java&nbsp;－解压jdk到java文件下</code></pre>
<h2 style="" id="%E9%85%8D%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F">配置环境变量</h2>
<p style=""><span style="font-size: 14px">操作步骤：</span></p>
<p style=""><span style="font-size: 14px; color: rgb(0, 0, 0)">1.&nbsp;打开.bashrc文件</span></p>
<pre><code>vi ~/.bashrc&nbsp;</code></pre>
<p style=""><span style="font-size: 14px">2.&nbsp;文件的末尾添加：</span></p>
<pre><code>export JAVA_HOME=/opt/java/jdk1.8.0_45

export JRE_HOME=${JAVA_HOME}/jre

export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib

export PATH=${JAVA_HOME}/bin:$PATH</code></pre>
<p style=""><span style="font-size: 14px">3.&nbsp;</span><code>:wq</code><span style="font-size: 14px"> －保存退出</span></p>
<p style=""><span style="font-size: 14px">4.&nbsp;运行命令来使之生效</span></p>
<pre><code>source ~/.bashrc </code></pre>
<h1 style="" id="%E5%AE%89%E8%A3%85-java-1.8">安装 java 1.8</h1>
<h2 style="" id="%E4%B8%8B%E8%BD%BD-jdk">下载 JDK</h2>
<pre><code>wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163/jdk-8u131-linux-x64.rpm</code></pre>
<p style="">添加执行权限命令</p>
<pre><code>chmod +x jdk-8u131-linux-x64.rpm</code></pre>
<p style="line-height: 1.74">执行 rpm 进行安装命令</p>
<pre><code>rpm -ivh jdk-8u131-linux-x64.rpm</code></pre>
<p style="line-height: 1.74">查看JDK是否安装成功命令</p>
<pre><code>java -version</code></pre>
<p style=""><span style="font-size: 14px; color: rgb(38, 38, 38)">查看JDK的安装路径（一般默认的路径：/usr/java/jdk1.8.0_131）</span></p>
<h2 style="" id="%E6%B5%8B%E8%AF%95">测试</h2>
<p style=""><span style="font-size: 14px">运行命令：</span></p>
<pre><code>java -version</code></pre>
<p style=""><span style="font-size: 14px">显示结果：</span></p>
<pre><code>java version "1.8.0_45"

Java(TM) SE Runtime Environment (build 1.8.0_45-b14)

Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)</code></pre>
<p style=""><span style="font-size: 14px">表示安装成功。</span></p>
<h2 style="" id=""></h2>
<h1 style="" id="%E5%AE%89%E8%A3%85tomcat">安装Tomcat</h1>
<h2 style="" id="%E4%B8%8B%E8%BD%BDtomcat">下载Tomcat</h2>
<pre><code>下载地址：http://tomcat.apache.org/download-70.cgi

版本：7.0.63 &nbsp;Core tar.gz</code></pre>
<h2 style="" id="%E5%AE%89%E8%A3%85tomcat-1">安装Tomcat</h2>
<p style=""><span style="font-size: 14px; color: rgb(0, 0, 0)">操作步骤：</span></p>
<p style=""><span style="font-size: 14px; color: rgb(0, 0, 0)">1.&nbsp;解压Tomcat到java</span><span style="font-size: 14px">目录</span><span style="font-size: 14px; color: rgb(0, 0, 0)">下</span></p>
<p style=""><span style="font-size: 14px">2.&nbsp;运行命令&nbsp;启动Tomcat</span></p>
<pre><code>sh /opt/java/apache-tomcat-7.0.63/bin/startup.sh</code></pre>
<p style=""><span style="font-size: 14px">3.&nbsp;返回结果：</span></p>
<pre><code>Using CATALINA_BASE: &nbsp;&nbsp;/opt/java/apache-tomcat-7.0.63

Using CATALINA_HOME: &nbsp;&nbsp;/opt/java/apache-tomcat-7.0.63

Using CATALINA_TMPDIR: /opt/java/apache-tomcat-7.0.63/temp

Using JRE_HOME: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/opt/java/jdk1.8.0_45/jre

Using CLASSPATH: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/opt/java/apache-tomcat-7.0.63/bin/bootstrap.jar:/opt/java/apache-tomcat-7.0.63/bin/tomcat-juli.jar

Tomcat started.</code></pre>
<p style=""><span style="font-size: 14px">4.&nbsp;测试：在浏览器运行</span><a rel="noopener noreferrer nofollow" href="http://www.oracle.com/technetwork/java/javase/downloads/index.html" target="_blank"><u>http://IP:8080</u></a><span style="font-size: 14px">&nbsp;是否能正确刷出网页</span></p>
<h2 style="" id="-1"></h2>
<h1 style="" id="%E5%AE%89%E8%A3%85maven">安装maven</h1>
<h2 style="" id="%E4%B8%8B%E8%BD%BDmaven">下载maven</h2>
<p style=""><span style="font-size: 14px">下载地址：</span><a rel="noopener noreferrer nofollow" href="http://www.oracle.com/technetwork/java/javase/downloads/index.html" target="_blank"><span style="font-size: 14px">http://maven.apache.org/download.cgi</span></a></p>
<p style=""><span style="font-size: 14px">版本：apache-maven-3.1.1-bin.tar.gz</span></p>
<h2 style="" id="%E5%AE%89%E8%A3%85maven-1">安装maven</h2>
<p style=""><span style="font-size: 14px">操作步骤：</span></p>
<p style=""><span style="font-size: 14px">1.&nbsp;解压maven到java目录下</span></p>
<p style=""><span style="font-size: 14px">2.&nbsp;配置环境变量：</span></p>
<pre><code>export MAVEN_HOME=/opt/java/apache-maven-3.1.1

export PATH=${MAVEN_HOME}/bin:$PATH</code></pre>
<p style=""><span style="font-size: 14px">3.&nbsp;:wq 保存退出</span></p>
<p style=""><span style="font-size: 14px">4.&nbsp;运行命令来使之生效</span></p>
<pre><code>source ~/.bashrc</code></pre>
<p style=""></p>
<h2 style="" id="%E6%B5%8B%E8%AF%95-1">测试</h2>
<p style=""><span style="font-size: 14px">运行命令：</span></p>
<pre><code>mvn -version</code></pre>
<p style=""><span style="font-size: 14px">显示结果：</span></p>
<pre><code>Apache Maven 3.1.1 (0728685237757ffbf44136acec0402957f723d9a; 2013-09-17 23:22:22+0800)

Maven home: /opt/java/apache-maven-3.1.1

Java version: 1.8.0_45, vendor: Oracle Corporation

Java home: /opt/java/jdk1.8.0_45/jre

Default locale: zh_CN, platform encoding: UTF-8

OS name: "linux", version: "2.6.32-431.23.3.el6.x86_64", arch: "amd64", family: "unix"</code></pre>
<p style=""><span style="font-size: 14px">表示安装成功</span></p>
<p style=""></p>
<h1 style="" id="%E5%AE%89%E8%A3%85mysql%E6%95%B0%E6%8D%AE%E5%BA%93">安装MySql数据库</h1>
<h2 style="" id="%E4%B8%80%E3%80%81%E5%AE%89%E8%A3%85yum-repo">一、安装YUM Repo</h2>
<h3 style="" id="1%E3%80%81%E7%94%B1%E4%BA%8Ecentos-%E7%9A%84yum%E6%BA%90%E4%B8%AD%E6%B2%A1%E6%9C%89mysql%EF%BC%8C%E9%9C%80%E8%A6%81%E5%88%B0%E4%B8%8B%E8%BD%BDyum-repo%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">1、由于CentOS 的yum源中没有mysql，需要到下载yum repo配置文件</h3>
<p style="">下载命令：</p>
<pre><code>wget https://dev.mysql.com/get/mysql57-community-release-el7-9.noarch.rpm</code></pre>
<h3 style="" id="2%E3%80%81%E7%84%B6%E5%90%8E%E8%BF%9B%E8%A1%8Crepo%E7%9A%84%E5%AE%89%E8%A3%85%EF%BC%9A">2、然后进行repo的安装：</h3>
<pre><code>rpm -ivh mysql57-community-release-el7-9.noarch.rpm</code></pre>
<p style="">执行完成后会在/etc/yum.repos.d/目录下生成两个repo文件mysql-community.repo&nbsp;mysql-community-source.repo</p>
<h2 style="" id="%E4%BA%8C%E3%80%81%E4%BD%BF%E7%94%A8yum%E5%91%BD%E4%BB%A4%E5%8D%B3%E5%8F%AF%E5%AE%8C%E6%88%90%E5%AE%89%E8%A3%85">二、使用yum命令即可完成安装</h2>
<p style=""><strong>注意：必须进入到 /etc/yum.repos.d/目录后再执行以下脚本</strong></p>
<h3 style="" id="1%E3%80%81%E5%AE%89%E8%A3%85%E5%91%BD%E4%BB%A4%EF%BC%9A">1、安装命令：</h3>
<pre><code>yum install mysql-server</code></pre>
<h3 style="" id="2%E3%80%81%E5%90%AF%E5%8A%A8msyql%EF%BC%9A">2、启动msyql：</h3>
<pre><code>systemctl start mysqld #启动MySQL</code></pre>
<h3 style="" id="3%E3%80%81%E8%8E%B7%E5%8F%96%E5%AE%89%E8%A3%85%E6%97%B6%E7%9A%84%E4%B8%B4%E6%97%B6%E5%AF%86%E7%A0%81%EF%BC%88%E5%9C%A8%E7%AC%AC%E4%B8%80%E6%AC%A1%E7%99%BB%E5%BD%95%E6%97%B6%E5%B0%B1%E6%98%AF%E7%94%A8%E8%BF%99%E4%B8%AA%E5%AF%86%E7%A0%81%EF%BC%89%EF%BC%9A">3、获取安装时的临时密码（在第一次登录时就是用这个密码）：</h3>
<pre><code>grep 'temporary password' /var/log/mysqld.log</code></pre>
<h3 style="" id="4%E3%80%81%E5%80%98%E8%8B%A5%E6%B2%A1%E6%9C%89%E8%8E%B7%E5%8F%96%E4%B8%B4%E6%97%B6%E5%AF%86%E7%A0%81">4、倘若没有获取临时密码</h3>
<p style="margin-left: 2px!important"><strong>1、删除原来安装过的mysql残留的数据</strong></p>
<pre><code>rm -rf /var/lib/mysql</code></pre>
<p style="margin-left: 2px!important"><strong>2.再启动mysql</strong></p>
<pre><code>systemctl start mysqld #启动MySQL</code></pre>
<h2 style="" id="%E4%B8%89%E3%80%81%E7%99%BB%E5%BD%95mysql">三、登录MySql</h2>
<pre><code>mysql -u root -p</code></pre>
<p style="">然后输入密码（刚刚获取的临时密码）</p>
<h2 style="" id="%E5%9B%9B%E3%80%81%E5%88%9B%E5%BB%BA%E6%95%B0%E6%8D%AE%E5%BA%93%EF%BC%8C%E6%B7%BB%E5%8A%A0%E7%94%A8%E6%88%B7%E5%92%8C%E6%8E%88%E6%9D%83%E7%99%BB%E5%BD%95">四、创建数据库，添加用户和授权登录</h2>
<h3 style="" id="1.-%E5%88%9B%E5%BB%BA%E6%95%B0%E6%8D%AE%E5%BA%93jjhuzhu%EF%BC%8C%E5%B9%B6%E5%88%B6%E5%AE%9A%E9%BB%98%E8%AE%A4%E7%9A%84%E5%AD%97%E7%AC%A6%E9%9B%86%E6%98%AFutf8">1.&nbsp;创建数据库jjhuzhu，并制定默认的字符集是utf8</h3>
<pre><code>mysql&gt; CREATE DATABASE IF NOT EXISTS jjhuzhu&nbsp;DEFAULT CHARSET utf8 COLLATE utf8_general_ci;</code></pre>
<h3 style="" id="2.-%E5%88%9B%E5%BB%BA%E7%94%A8%E6%88%B7">2.&nbsp;创建用户</h3>
<pre><code>mysql&gt; CREATE USER 'jjhuzhu'@'%' IDENTIFIED BY 'jjhuzhu';</code></pre>
<h3 style="" id="3.-%E5%88%B7%E6%96%B0%E7%B3%BB%E7%BB%9F">3.&nbsp;刷新系统</h3>
<pre><code>mysql&gt; flush privileges;</code></pre>
<h3 style="" id="4.-%E6%8E%88%E6%9D%83%E7%94%A8%E6%88%B7">4.&nbsp;授权用户</h3>
<pre><code>mysql&gt; grant all on jjhuzhu.* to 'jjhuzhu'@'%' identified by 'jjhuzhu';</code></pre>
<h3 style="" id="5.-%E5%88%B7%E6%96%B0%E7%B3%BB%E7%BB%9F">5.&nbsp;刷新系统</h3>
<pre><code>mysql&gt; flush privileges;</code></pre>
<h2 style="" id="%E4%BA%94%E3%80%81%E5%85%B6%E4%BB%96%E9%85%8D%E7%BD%AE">五、其他配置</h2>
<h3 style="" id="1%E3%80%81%E8%AE%BE%E7%BD%AE%E5%AE%89%E5%85%A8%E9%80%89%E9%A1%B9%EF%BC%9A">1、设置安全选项：</h3>
<pre><code>mysql_secure_installation</code></pre>
<h3 style="" id="2%E3%80%81%E5%85%B3%E9%97%ADmysql">2、关闭MySQL</h3>
<pre><code>systemctl stop mysqld&nbsp;</code></pre>
<h3 style="" id="3%E3%80%81%E9%87%8D%E5%90%AFmysql">3、重启MySQL</h3>
<pre><code>systemctl restart mysqld&nbsp;</code></pre>
<h3 style="" id="4%E3%80%81%E6%9F%A5%E7%9C%8Bmysql%E8%BF%90%E8%A1%8C%E7%8A%B6%E6%80%81">4、查看MySQL运行状态</h3>
<pre><code>systemctl status mysqld&nbsp;</code></pre>
<h3 style="" id="5%E3%80%81%E8%AE%BE%E7%BD%AE%E5%BC%80%E6%9C%BA%E5%90%AF%E5%8A%A8">5、设置开机启动</h3>
<pre><code>systemctl enable mysqld&nbsp;</code></pre>
<h3 style="" id="6%E3%80%81%E5%85%B3%E9%97%AD%E5%BC%80%E6%9C%BA%E5%90%AF%E5%8A%A8">6、关闭开机启动</h3>
<pre><code>systemctl disable mysqld&nbsp;</code></pre>
<h3 style="" id="7%E3%80%81%E9%85%8D%E7%BD%AE%E9%BB%98%E8%AE%A4%E7%BC%96%E7%A0%81%E4%B8%BAutf8%EF%BC%9A">7、配置默认编码为utf8：</h3>
<pre><code>vi /etc/my.cnf #添加 [mysqld] character_set_server=utf8 init_connect='SET NAMES utf8'</code></pre>
<p style="">其他默认配置文件路径：&nbsp;</p>
<p style="">配置文件：/etc/my.cnf</p>
<p style="">日志文件：/var/log//var/log/mysqld.log</p>
<p style="">服务启动脚本：/usr/lib/systemd/system/mysqld.service</p>
<p style="">socket文件：/var/run/mysqld/<a rel="noopener noreferrer nofollow" href="http://www.oracle.com/technetwork/java/javase/downloads/index.html" target="_blank">mysqld.pid</a></p>
<h3 style="" id="8%E3%80%81%E6%9F%A5%E7%9C%8B%E7%89%88%E6%9C%AC">8、查看版本</h3>
<pre><code>select version();</code></pre>
<p style=""></p>
<h1 style="" id="%E5%AE%89%E8%A3%85svn">安装SVN</h1>
<p style=""><span style="font-size: 14px">运行命令：</span></p>
<pre><code>yum install subversion</code></pre>
<p style=""></p>
<h1 style="" id="%E5%AE%89%E8%A3%85nginx">安装nginx</h1>
<p style=""><span style="font-size: 14px">运行命令：</span></p>
<pre><code>yum install nginx</code></pre>
<p style=""><span style="font-size: 14px">启动：</span></p>
<pre><code>service nginx start</code></pre>
<p style=""><span style="font-size: 14px; color: rgb(51, 51, 51)">重新加载：</span></p>
<pre><code>service nginx reload</code></pre>
<p style=""></p>
<h1 style="" id="%E5%AE%89%E8%A3%85redis">安装redis</h1>
<p style=""><span style="font-size: 14px">运行命令：</span></p>
<pre><code>yum install redis</code></pre>
<p style=""><span style="font-size: 14px">启动：</span></p>
<pre><code>service redis&nbsp;start</code></pre>
<p style=""><span style="font-size: 14px; color: rgb(51, 51, 51)">重新启动：</span></p>
<pre><code>service redis&nbsp;restart</code></pre>
<p style=""></p>
<h1 style="" id="%E9%85%8D%E7%BD%AE%E9%98%B2%E7%81%AB%E5%A2%99">配置防火墙</h1>
<p style=""><span style="font-size: 14px">1.安装iptables服务</span></p>
<pre><code>yum install -y iptables-services</code></pre>
<p style=""><span style="font-size: 14px">2.写入过滤规则</span></p>
<pre><code>vi /etc/sysconfig/iptables</code></pre>
<p style=""><span style="font-size: 14px">3.启动iptables服务</span></p>
<pre><code>service iptables start</code></pre>
<p style=""><span style="font-size: 14px">4.查看当前iptables规则：</span></p>
<pre><code>iptables -L -n</code></pre>
<p style=""><span style="font-size: 14px">5.查看当前监听端口：</span></p>
<pre><code>netstat –tnlp</code></pre>
<pre><code>------------------------------------------------------------

# sample configuration for iptables service

# you can edit this manually or use system-config-firewall

# please do not ask us to add additional ports/services to this default configuration

*filter

:INPUT ACCEPT [0:0]

:FORWARD ACCEPT [0:0]

:OUTPUT ACCEPT [0:0]

-A INPUT -s 127.0.0.1/32 -j ACCEPT

-A INPUT -p tcp --dport 22 -j ACCEPT

-A INPUT -p tcp --dport 80 -j ACCEPT

-A INPUT -p tcp --dport 443 -j ACCEPT

-A INPUT -p tcp --dport 3306 -j ACCEPT

-A INPUT -p tcp --dport 23450 -j ACCEPT

-A INPUT &nbsp;-m state --state INVALID -j DROP

-A FORWARD -m state --state INVALID -j DROP

-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

-A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

-A INPUT -i lo -p all -j ACCEPT

-A INPUT -j REJECT

COMMIT

----------------------------------------------------------</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299637081</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><category>Java技术</category><pubDate>Mon, 11 Dec 2023 13:00:00 GMT</pubDate></item><item><title><![CDATA[linux安装zookeeper及使用]]></title><link>https://xiaoming728.com/archives/1702299614424</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=linux%E5%AE%89%E8%A3%85zookeeper%E5%8F%8A%E4%BD%BF%E7%94%A8&amp;url=/archives/1702299614424" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="%E4%B8%80%E3%80%81%E5%AE%89%E8%A3%85%E6%9D%A1%E4%BB%B6">一、安装条件</h3>
<p style="">想要安装zookeeper，必须先在linux中安装好jdk。安装步骤见：</p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html">https://www.cnblogs.com/expiator/p/9987351.html</a></p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E4%B8%8B%E8%BD%BD%E5%B9%B6%E8%A7%A3%E5%8E%8Bzookeeper%E5%8E%8B%E7%BC%A9%E5%8C%85">二、下载并解压zookeeper压缩包</h3>
<p style="">1. 先进入/usr/local/目录，也可以是其他的目录：</p>
<pre><code>cd /usr/local</code></pre>
<p style="">2. zookeeper安装包可以在官网下载。</p>
<p style="">也可以在后面这个地址下载&nbsp; <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html">http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.4.13/zookeeper-3.4.13.tar.gz</a></p>
<p style="">如果链接打不开，就先打开 <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html">http://mirror.bit.edu.cn/apache/zookeeper</a>&nbsp;， 再选择版本。</p>
<p style="">在此目录下载zookeeper安装包：</p>
<pre><code>wget http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.4.13/zookeeper-3.4.13.tar.gz</code></pre>
<p style="">3. 解压：</p>
<pre><code>tar -zxvf zookeeper-3.4.13.tar.gz</code></pre>
<h3 style="" id="%E4%B8%89%E3%80%81%E7%BC%96%E8%BE%91%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">三、编辑配置文件</h3>
<p style="">1.进入conf目录：</p>
<pre><code>cd zookeeper-3.4.13/conf</code></pre>
<p style="">2. 将zoo_sample.cfg这个文件复制为zoo.cfg (必须是这个文件名)</p>
<pre><code>cp  zoo_sample.cfg  zoo.cfg</code></pre>
<p style="">3. 进入zoo.cfg文件进行编辑</p>
<pre><code>vi zoo.cfg</code></pre>
<p style="">4. 按 i 进入编辑模式，修改以下内容：</p>
<pre><code>dataDir=/tmp/zookeeper/data
dataLogDir=/tmp/zookeeper/log</code></pre>
<p style="">注意：如果想配置集群的话，请在clientPort下面添加服务器的ip。如</p>
<pre><code>server.1=192.168.180.132:2888:3888
server.2=192.168.180.133:2888:3888
server.3=192.168.180.134:2888:3888</code></pre>
<p style="">如果电脑内存比较小，zookeeper还可以设置成伪集群。也就是全部服务器采用同一个ip，但是使用不同的端口。</p>
<p style="">5. 在tmp目录创建目录。</p>
<pre><code>[root@localhost conf]# mkdir /tmp/zookeeper
[root@localhost conf]# mkdir /tmp/zookeeper/data
[root@localhost conf]# mkdir /tmp/zookeeper/log</code></pre>
<p style="">&nbsp;6.如果是配置集群，还需要在前面配置过的dataDir路径下新增myid文件</p>
<pre><code>[root@localhost conf]# cd /tmp/zookeeper/data

[root@localhost data]# touch myid
[root@localhost data]# vim myid</code></pre>
<p style="">在data目录下创建文件，文件名为“myid”, 编辑该“myid”文件，并在对应的IP的机器上输入对应的编号。</p>
<p style="">如在192.168.180.132上，“myid”文件内容就是1。在192.168.180.133上，内容就是2。</p>
<p style=""></p>
<h3 style="" id="%E5%9B%9B%E3%80%81%E9%85%8D%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F">四、配置环境变量</h3>
<p style="">1.上面的操作都完事之后，我们需要配置一下环境变量，配置环境变量的命令如下：</p>
<pre><code>export ZOOKEEPER_INSTALL=/usr/local/zookeeper-3.4.13/
export PATH=$PATH:$ZOOKEEPER_INSTALL/bin</code></pre>
<p style="">&nbsp;</p>
<h3 style="" id="%E4%BA%94%E3%80%81%E5%90%AF%E5%8A%A8zookeeper">五、启动zookeeper</h3>
<p style="">1.进入bin目录，并启动zookeep。如果不是在bin目录下执行，启动zookeeper时会报错： bash: ./<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html">zkServer.sh</a>:&nbsp; No such file or directory</p>
<p style="">注意：&nbsp; <span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">./</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">zkServer.sh</span></a><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)"> start</span>前面的 .&nbsp; 不可忽略。</p>
<pre><code>cd /usr/local/zookeeper-3.4.13/bin
./zkServer.sh start</code></pre>
<p style="">2.启动成功效果如下：</p>
<pre><code>ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper-3.4.13/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED</code></pre>
<p style="">3.zookeeper的服务端启动后，还需要启动zookeeper的客户端：</p>
<pre><code>./zkCli.sh</code></pre>
<p style="">如果是连接多个不同的主机节点，可以使用如下命令：</p>
<pre><code>./zkCli.sh -server 192.168.180.132:2888</code></pre>
<p style="">启动成功效果如下：</p>
<pre><code>Connecting to localhost:2181
..........
..........
..........
Welcome to ZooKeeper!
2018-10-25 21:04:54,407 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1029] - Opening socket connection to server localhost/0:0:0:0:0:0:0:1:2181. Will not attempt to authenticate using SASL (unknown error)
JLine support is enabled
2018-10-25 21:04:54,471 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@879] - Socket connection established to localhost/0:0:0:0:0:0:0:1:2181, initiating session
[zk: localhost:2181(CONNECTING) 0] 2018-10-25 21:04:54,501 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1303] - Session establishment complete on server localhost/0:0:0:0:0:0:0:1:2181, sessionid = 0x10000712e6f0000, negotiated timeout = 30000

WATCHER::

WatchedEvent state:SyncConnected type:None path:null</code></pre>
<p style="">4.查看状态：</p>
<pre><code>[root@localhost bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper-3.4.13/bin/../conf/zoo.cfg
Mode: standalone</code></pre>
<p style=""></p>
<h4 style="" id="%E9%81%87%E5%88%B0%E9%97%AE%E9%A2%98%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%EF%BC%9F">遇到问题怎么解决？</h4>
<p style="">zookeeper的出错日志会记录在&nbsp;zookeeper.out。</p>
<p style="">当前处于哪个目录，<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html">执行完zkServer.sh</a> start命令，&nbsp;zookeeper.out就会写在哪个目录。</p>
<p style="">vim zookeeper.out 可以查看报错信息。然后再搜索解决。</p>
<h3 style="" id="%E5%85%AD%E3%80%81zookeeper%E4%BD%BF%E7%94%A8">六、zookeeper使用</h3>
<p style="">通过 ./<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html">zkCli.sh</a> 进入客户端后，就可以使用命令来操作zookeeper了。</p>
<p style=""><strong>1.创建节点</strong></p>
<p style="">使用create命令，可以创建一个zookeeper节点。</p>
<p style="">create [-s] &nbsp; [-e]&nbsp; path&nbsp; data&nbsp; acl</p>
<p style="">其中-s表示顺序节点，-e表示临时节点。默认情况下，创建的是持久节点。</p>
<p style="">path是节点路径，data是节点数据，acl是用来进行权限控制的。</p>
<p style="">如下：</p>
<p style="">创建一个叫做/zk-test的节点，内容是"123"</p>
<pre><code>[zk: localhost:2181(CONNECTED) 0] create /zk-test 123

Created /zk-test</code></pre>
<p style="">创建/zk-test的子节点book，内容是"233"</p>
<pre><code>[zk: localhost:2181(CONNECTED) 7] create  /zk-test/book  233
Created /zk-test/book</code></pre>
<p style=""><strong>2.查看节点内容</strong></p>
<p style="">使用get命令，可以获取zookeeper指定节点的内容和属性信息。</p>
<p style="">如下：</p>
<pre><code>[zk: localhost:2181(CONNECTED) 1] get /zk-test

123
cZxid = 0x3a
ctime = Sun Nov 11 21:50:44 CST 2018
mZxid = 0x3a
mtime = Sun Nov 11 21:50:44 CST 2018
pZxid = 0x3a
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0</code></pre>
<p style=""><strong>3.查看子节点</strong></p>
<p style="">使用ls命令可以查看指定节点下的所有子节点</p>
<p style="">以下查看根目录下的所有子节点：</p>
<pre><code>[zk: localhost:2181(CONNECTED) 2] ls /

[zk-test, zookeeper]</code></pre>
<p style="">查看zk-test节点的子节点：</p>
<pre><code>[zk: localhost:2181(CONNECTED) 3] ls /zk-test
[book]</code></pre>
<p style="">&nbsp;</p>
<p style=""><strong>4.更新节点内容</strong></p>
<p style="">使用set命令，更新节点内容。格式为：</p>
<p style="">set&nbsp;&nbsp; path&nbsp; data&nbsp;</p>
<p style="">其中的data就是要更新的新内容。</p>
<pre><code>[zk: localhost:2181(CONNECTED) 4] set /zk-test 456

cZxid = 0x3a
ctime = Sun Nov 11 21:50:44 CST 2018
mZxid = 0x3b
mtime = Sun Nov 11 22:05:20 CST 2018
pZxid = 0x3a
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0</code></pre>
<p style="">在输出的信息中，可以发现，dataVersion的值由原来的0 变成了 1，这是因为刚才的更新操作导致该节点的数据版本也发生变更。</p>
<p style=""><strong>5.删除节点</strong></p>
<p style="">使用delete命令来删除节点，如下：</p>
<pre><code>[zk: localhost:2181(CONNECTED) 11] delete /zk-test
Node not empty: /zk-test</code></pre>
<p style="">可以发现，一个节点存在子节点时，无法删除该节点。</p>
<p style="">删除子节点/zk-test/book，如下：</p>
<pre><code>[zk: localhost:2181(CONNECTED) 12] delete /zk-test/book

WATCHER::

WatchedEvent state:SyncConnected type:NodeDeleted path:/zk-test/book</code></pre>
<p style="">zookeeper中的watcher会监控节点，当子节点发生变化时会发出通知。此时提示子节点 /zk-test/book删除成功。</p>
<p style="">继续尝试删除节点 /zk-test，</p>
<pre><code>[zk: localhost:2181(CONNECTED) 13] ls /zk-test
[]
[zk: localhost:2181(CONNECTED) 14] delete /zk-test
[zk: localhost:2181(CONNECTED) 15] ls /
[]</code></pre>
<p style="">删除成功。</p>
<p style="">&nbsp;</p>
<blockquote>
 <p style="">参考资料 ：</p>
 <p style="">《从Paxos到zookeeper分布式一致性原理与实践》</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html">https://blog.csdn.net/zknxx/article/details/52601554</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html">https://blog.csdn.net/21aspnet/article/details/18990891</a></p>
 <p style=""></p>
 <p style="">开源：乐之者v - 博客园</p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/expiator/p/9987351.html">https://www.cnblogs.com/expiator/p/9853378.html</a></p>
 <p style=""></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299614424</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fzookeeper.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 13:00:00 GMT</pubDate></item><item><title><![CDATA[Linux进程之如何查看进程详情（ps命令）]]></title><link>https://xiaoming728.com/archives/1702299433958</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%E8%BF%9B%E7%A8%8B%E4%B9%8B%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8B%E8%BF%9B%E7%A8%8B%E8%AF%A6%E6%83%85%EF%BC%88ps%E5%91%BD%E4%BB%A4%EF%BC%89&amp;url=/archives/1702299433958" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="1.-ps%E6%98%AF%E4%BB%80%E4%B9%88">1. ps是什么</h2>
<p style=""><span style="font-size: 16px">要对进程进行监测和控制，首先必须要了解当前进程的情况,也就是需要查看当前进程，ps命令就是最基本进程查看命令。使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵尸、哪些进程占用了过多的资源等等.总之大部分信息都是可以通过执行该命令得到。</span></p>
<p style=""></p>
<p style=""><strong><span style="font-size: 16px">ps是显示瞬间进程的状态，并不动态连续</span></strong><span style="font-size: 16px">；如果想对进程进行实时监控应该用top命令。</span></p>
<p style=""></p>
<p style=""><strong><span style="font-size: 16px">基本参数：</span></strong></p>
<p style=""><span style="font-size: 16px">-A ：所有的进程均显示出来，与 -e 具有同样的效用；</span></p>
<p style=""><span style="font-size: 16px">-a ： 显示现行终端机下的所有进程，包括其他用户的进程；</span></p>
<p style=""><span style="font-size: 16px">-u ：以用户为主的进程状态 ；</span></p>
<p style=""><span style="font-size: 16px">x ：通常与 a 这个参数一起使用，可列出较完整信息。</span></p>
<p style=""></p>
<p style=""><strong><span style="font-size: 16px">输出格式规划：</span></strong></p>
<p style=""><span style="font-size: 16px">l ：较长、较详细的将该PID 的的信息列出；</span></p>
<p style=""><span style="font-size: 16px">j ：工作的格式 (jobs format)</span></p>
<p style=""><span style="font-size: 16px">-f ：做一个更为完整的输出</span></p>
<p style=""></p>
<h2 style="" id="2.-%E4%B8%8D%E5%8A%A0%E5%8F%82%E6%95%B0%E6%89%A7%E8%A1%8Cps%E5%91%BD%E4%BB%A4%E4%BC%9A%E8%BE%93%E5%87%BA%E4%BB%80%E4%B9%88">2. 不加参数执行ps命令会输出什么</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">这是一个基本的&nbsp;</span><strong>ps</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;使用，我们来看看控制台中执行这个命令并查看结果。</span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-hfzz.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style=""><span style="font-size: 16px">结果默认会显示4列信息：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 16px">PID: 运行着的命令(CMD)的进程编号</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px">TTY: 命令所运行的位置（终端）</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px">TIME: 运行着的该命令所占用的CPU处理时间</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px">CMD: 该进程所运行的命令</span></p></li>
</ul>
<p style=""></p>
<p style=""><span style="font-size: 16px">这些信息在显示时未排序。</span></p>
<p style=""></p>
<h2 style="" id="3.-%E5%A6%82%E4%BD%95%E6%98%BE%E7%A4%BA%E6%89%80%E6%9C%89%E5%BD%93%E5%89%8D%E8%BF%9B%E7%A8%8B">3. 如何显示所有当前进程</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">使用&nbsp;</span><strong>-a</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;参数，</span><strong>-a 代表 all</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">。同时加上x参数会显示没有控制终端的进程。</span></p>
<pre><code>ps -ax</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">这个命令的结果或许会很长。为了便于查看，可以结合less命令和管道来使用。</span></p>
<pre><code>ps -ax | less</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-wrhu.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h2 style="" id="4.-%E5%A6%82%E4%BD%95%E6%A0%B9%E6%8D%AE%E8%BF%9B%E7%A8%8B%E7%9A%84%E7%94%A8%E6%88%B7%E8%BF%9B%E8%A1%8C%E4%BF%A1%E6%81%AF%E8%BF%87%E6%BB%A4">4. 如何根据进程的用户进行信息过滤</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">在需要查看特定用户进程的情况下，我们可以使用&nbsp;</span><strong>-u</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;参数。比如我们要查看用户’pungki’的进程，可以通过下面的命令：</span></p>
<pre><code>ps -u pungki</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-eodi.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h2 style="" id="5.-%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87cpu%E5%92%8C%E5%86%85%E5%AD%98%E4%BD%BF%E7%94%A8%E6%9D%A5%E8%BF%87%E6%BB%A4%E8%BF%9B%E7%A8%8B">5. 如何通过cpu和内存使用来过滤进程</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">也许你希望把结果按照 CPU 或者内存用量来筛选，这样你就找到哪个进程占用了你的资源。要做到这一点，我们可以使用&nbsp;</span><strong>aux</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;参数，来显示全面的信息:</span></p>
<pre><code>ps -aux | less</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qdmc.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style=""><span style="font-size: 16px">当结果很长时，我们可以使用管道和less命令来筛选。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px">默认的结果集是未排好序的。可以通过 </span><strong><span style="font-size: 16px">–sort</span></strong><span style="font-size: 16px">命令来排序。</span></p>
<p style=""></p>
<h3 style="" id="5.1-%E6%A0%B9%E6%8D%AEcpu%E4%BD%BF%E7%94%A8%E7%8E%87%E6%9D%A5%E5%8D%87%E5%BA%8F%E6%8E%92%E5%BA%8F">5.1 根据CPU使用率来升序排序</h3>
<pre><code>ps -aux --sort -pcpu | less</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-krtv.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="5.2-%E6%A0%B9%E6%8D%AE%E5%86%85%E5%AD%98%E4%BD%BF%E7%94%A8%E7%8E%87%E6%9D%A5%E5%8D%87%E5%BA%8F%E6%8E%92%E5%BA%8F">5.2 根据内存使用率来升序排序</h3>
<pre><code>ps -aux --sort -pmem | less</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xzmb.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h3 style="" id="5.3-%E6%88%91%E4%BB%AC%E4%B9%9F%E5%8F%AF%E4%BB%A5%E5%B0%86%E5%AE%83%E4%BB%AC%E5%90%88%E5%B9%B6%E5%88%B0%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%EF%BC%8C%E5%B9%B6%E9%80%9A%E8%BF%87%E7%AE%A1%E9%81%93%E6%98%BE%E7%A4%BA%E5%89%8D10%E4%B8%AA%E7%BB%93%E6%9E%9C">5.3 我们也可以将它们合并到一个命令，并通过管道显示前10个结果</h3>
<pre><code>ps -aux --sort -pcpu,+pmem | head -n 10</code></pre>
<h2 style="" id="6.-%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87%E8%BF%9B%E7%A8%8B%E5%90%8D%E5%92%8Cpid%E8%BF%9B%E8%A1%8C%E8%BF%87%E6%BB%A4">6. 如何通过进程名和PID进行过滤</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">使用&nbsp;</span><strong>-C</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;参数，后面跟你要找的进程的名字。比如想显示一个名为getty的进程的信息，就可以使用下面的命令：</span></p>
<pre><code>ps -C getty</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-rinr.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">如果想要看到更多的细节，我们可以使用-f参数来查看格式化的信息列表：</span></p>
<pre><code>ps -f -C getty</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-cfgf.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h2 style="" id="7.-%E5%A6%82%E4%BD%95%E6%A0%B9%E6%8D%AE%E7%BA%BF%E7%A8%8B%E6%9D%A5%E8%BF%87%E6%BB%A4%E8%BF%9B%E7%A8%8B">7. 如何根据线程来过滤进程</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">如果我们想知道特定进程的线程，可以使用&nbsp;</span><strong>-L</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;参数，后面加上特定的PID。</span></p>
<pre><code>ps -L 1213</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xpjz.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h2 style="" id="8.-%E5%A6%82%E4%BD%95%E6%A0%91%E5%BD%A2%E7%9A%84%E6%98%BE%E7%A4%BA%E8%BF%9B%E7%A8%8B">8. 如何树形的显示进程</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">有时候我们希望以树形结构显示进程，可以使用&nbsp;</span><strong>-axjf</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;参数。</span></p>
<pre><code>ps -axjf</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-kgnj.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">或者可以使用另一个命令。</span></p>
<pre><code>pstree</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ifyd.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h2 style="" id="9.-%E5%A6%82%E4%BD%95%E6%98%BE%E7%A4%BA%E5%AE%89%E5%85%A8%E4%BF%A1%E6%81%AF">9. 如何显示安全信息</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">如果想要查看现在有谁登入了你的服务器。可以使用ps命令加上相关参数:</span></p>
<pre><code>ps -eo pid,user,args</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">参数&nbsp;</span><strong>-e</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;显示所有进程信息，</span><strong>-o</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;参数控制输出。</span><strong>Pid</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">,</span><strong>User</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;和&nbsp;</span><strong>Args</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">参数显示PID，</span><strong>运行应用的用户</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">和</span><strong>该应用</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-wryt.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">能够与&nbsp;</span><strong>-e</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;参数 一起使用的关键字是</span><strong>args, cmd, comm, command, fname, ucmd, ucomm, lstart, bsdstart 和 start</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">。</span></p>
<p style=""></p>
<h2 style="" id="10.-%E5%A6%82%E4%BD%95%E6%A0%BC%E5%BC%8F%E5%8C%96%E8%BE%93%E5%87%BAroot%E7%94%A8%E6%88%B7%EF%BC%88%E7%9C%9F%E5%AE%9E%E7%9A%84%E6%88%96%E6%9C%89%E6%95%88%E7%9A%84uid%EF%BC%89%E5%88%9B%E5%BB%BA%E7%9A%84%E8%BF%9B%E7%A8%8B">10. 如何格式化输出root用户（真实的或有效的UID）创建的进程</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">系统管理员想要查看由root用户运行的进程和这个进程的其他相关信息时，可以通过下面的命令:</span></p>
<pre><code>ps -U root -u root u</code></pre>
<p style=""><strong><span style="font-size: 16px">-U</span></strong><span style="font-size: 16px"> 参数按真实用户ID(RUID)筛选进程，它会从用户列表中选择真实用户名或 ID。真实用户即实际创建该进程的用户。</span></p>
<p style=""><strong><span style="font-size: 16px">-u</span></strong><span style="font-size: 16px"> 参数用来筛选有效用户ID（EUID）。</span></p>
<p style=""><span style="font-size: 16px">最后的 </span><strong><span style="font-size: 16px">u</span></strong><span style="font-size: 16px"> 参数用来决定以针对用户的格式输出，由</span><strong><span style="font-size: 16px">User, PID, %CPU, %MEM, VSZ, RSS, TTY, STAT, START, TIME </span></strong><span style="font-size: 16px">和 </span><strong><span style="font-size: 16px">COMMAND</span></strong><span style="font-size: 16px">这几列组成。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">这里有上面的命令的输出结果：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ldta.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h2 style="" id="11.-%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8ps%E5%AE%9E%E6%97%B6%E7%9B%91%E6%8E%A7%E8%BF%9B%E7%A8%8B%E7%8A%B6%E6%80%81">11. 如何使用PS实时监控进程状态</h2>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">ps 命令会显示你系统当前的进程状态，但是这个结果是静态的。</span></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">当有一种情况，我们需要像上面第四点中提到的通过CPU和内存的使用率来筛选进程，并且我们希望结果能够每秒刷新一次。为此，我们可以</span><strong>将ps命令和watch命令结合起来</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">。</span></p>
<pre><code>watch -n 1 ‘ps -aux --sort -pmem, -pcpu’</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-rlfw.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(77, 77, 77)">如果输出太长，我们也可以限制它，比如前20条，我们可以使用&nbsp;</span><strong>head</strong><span style="font-size: 16px; color: rgb(77, 77, 77)">&nbsp;命令来做到。</span></p>
<pre><code>watch -n 1 ‘ps -aux --sort -pmem, -pcpu | head 20’</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-gskz.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 16px">这里的动态查看并不像top或者htop命令一样。但是使用ps的好处是你能够定义显示的字段，你能够选择你想查看的字段。</span></p>
<p style=""><span style="font-size: 16px">举个例子，如果你只需要看名为’pungki’用户的信息，你可以使用下面的命令：</span></p>
<pre><code>watch -n 1 ‘ps -aux -U pungki u --sort -pmem, -pcpu | head 20’</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qwsq.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="12.-%E6%9C%80%E5%90%8E">12. 最后</h2>
<p style=""><span style="font-size: 16px">你也许每天都会使用ps命令来监控你的Linux系统。但是事实上，你可以通过ps命令的参数来生成各种你需要的报表。</span></p>
<p style=""><span style="font-size: 16px">ps命令的另一个优势是ps是各种 Linux系统都默认安装的，因此你只要用就行了。不要忘了通过 man ps来查看更多的参数。</span></p>
<p style=""></p>
<blockquote>
 <p style="">版权声明：本文为博主原创文章，遵循 CC 4.0 BY-SA 版权协议，转载请附上原文出处链接和本声明。</p>
 <p style="">本文链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/baidu_34122324/article/details/84451687">https://blog.csdn.net/baidu_34122324/article/details/84451687</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299433958</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:59:00 GMT</pubDate></item><item><title><![CDATA[Linux查看和设置环境变量]]></title><link>https://xiaoming728.com/archives/1702299416926</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%E6%9F%A5%E7%9C%8B%E5%92%8C%E8%AE%BE%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F&amp;url=/archives/1702299416926" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">来源：博客园-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.cnblogs.com/qiuhong10/">hongma</a></p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/qiuhong10/">https://www.cnblogs.com/qiuhong10/p/7815943.html</a></p>
</blockquote>
<p style=""></p>
<h3 style="" id="linux%E7%9A%84%E5%8F%98%E9%87%8F%E7%A7%8D%E7%B1%BB">Linux的变量种类</h3>
<p style="">按变量的生存周期来划分，Linux变量可分为两类：&nbsp;</p>
<p style="">1 永久的：需要修改配置文件，变量永久生效。&nbsp;</p>
<p style="">2 临时的：使用export命令声明即可，变量在关闭shell时失效。</p>
<h3 style="" id="%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E7%9A%84%E6%9F%A5%E7%9C%8B">环境变量的查看</h3>
<p style="">1 使用echo命令查看单个环境变量</p>
<pre><code>echo $PATH</code></pre>
<p style="">2 使用env查看所有环境变量</p>
<pre><code>env</code></pre>
<p style="">3 使用set查看所有本地定义的环境变量</p>
<pre><code># 查看所有环境变量
set</code></pre>
<p style="">4 使用unset删除指定的环境变量</p>
<p style="">清除环境变量的值用unset命令。如果未指定值，则该变量值将被设为NULL。示例如下：&nbsp;</p>
<pre><code>export TEST="Test..." #增加一个环境变量TEST
env|grep TEST #此命令有输入，证明环境变量TEST已经存在了 
TEST=Test... 
unset  TEST #删除环境变量TEST 
env|grep TEST #此命令没有输出，证明环境变量TEST已经删除</code></pre>
<h3 style="" id="%E5%B8%B8%E7%94%A8%E7%9A%84%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F">常用的环境变量</h3>
<ul>
 <li>
  <p style="">PATH 决定了shell将到哪些目录中寻找命令或程序&nbsp;</p></li>
 <li>
  <p style="">HOME 当前用户主目录&nbsp;</p></li>
 <li>
  <p style="">HISTSIZE　历史记录数&nbsp;</p></li>
 <li>
  <p style="">LOGNAME 当前用户的登录名&nbsp;</p></li>
 <li>
  <p style="">HOSTNAME　指主机的名称&nbsp;</p></li>
 <li>
  <p style="">SHELL 当前用户Shell类型&nbsp;</p></li>
 <li>
  <p style="">LANGUGE 　语言相关的环境变量，多语言可以修改此环境变量&nbsp;</p></li>
 <li>
  <p style="">MAIL　当前用户的邮件存放目录&nbsp;</p></li>
 <li>
  <p style="">PS1　基本提示符，对于root用户是#，对于普通用户是$</p></li>
</ul>
<h3 style="" id="%E8%AE%BE%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F">设置环境变量</h3>
<p style=""><strong>1 在/etc/profile文件中添加变量【对所有用户生效(永久的)】&nbsp;</strong></p>
<p style="">用VI在文件/etc/profile文件中增加变量，该变量将会对Linux下所有用户有效，并且是“永久的”。&nbsp;</p>
<p style="">例如：编辑/etc/profile文件，添加CLASSPATH变量&nbsp;</p>
<pre><code># vi /etc/profile 
export CLASSPATH=./JAVA_HOME/lib;$JAVA_HOME/jre/lib</code></pre>
<p style="">注：修改文件后要想马上生效还要运行# source /etc/profile不然只能在下次重进此用户时生效。</p>
<p style=""><strong>2 在用户目录下的.bash_profile文件中增加变量【对单一用户生效(永久的)】&nbsp;</strong></p>
<p style="">用VI在用户目录下的.bash_profile文件中增加变量，改变量仅会对当前用户有效，并且是“永久的”。&nbsp;</p>
<p style="">例如：编辑guok用户目录(/home/guok)下的.bash_profile&nbsp;</p>
<pre><code># vi /home/guok/.bash_profile
export CLASSPATH=./JAVA_HOME/lib;$JAVA_HOME/jre/lib</code></pre>
<p style="">注：修改文件后要想马上生效还要运行$ source /home/guok/.bash_profile不然只能在下次重进此用户时生效。</p>
<p style=""><strong>3 直接运行export命令定义变量【只对当前shell(BASH)有效(临时的)】&nbsp;</strong></p>
<p style="">在shell的命令行下直接使用[export 变量名=变量值] 定义变量</p>
<pre><code>export CLASSPATH=./JAVA_HOME/lib;$JAVA_HOME/jre/lib</code></pre>
<p style="">该变量只在当前的shell(BASH)或其子shell(BASH)下是有效的</p>
<p style="">shell关闭了，变量也就失效了，再打开新shell时就没有这个变量，需要使用的话还需要重新定义。</p>]]></description><guid isPermaLink="false">/archives/1702299416926</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:57:00 GMT</pubDate></item><item><title><![CDATA[firewall-cmd 防火墙常用命令]]></title><link>https://xiaoming728.com/archives/1702299398320</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=firewall-cmd%20%E9%98%B2%E7%81%AB%E5%A2%99%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4&amp;url=/archives/1702299398320" width="1" height="1" alt="" style="opacity:0;">
<pre><code>

# 开启防火墙
systemctl start firewalld.service


# 防火墙开机启动
systemctl enable firewalld.service


# 关闭防火墙
systemctl stop firewalld.service


# 查看防火墙状态
firewall-cmd --state


# 查看现有的规则
iptables -nL
firewall-cmd --zone=public --list-ports


# 重载防火墙配置
firewall-cmd --reload


# 添加单个单端口
firewall-cmd --permanent --zone=public --add-port=81/tcp


# 添加多个端口
firewall-cmd --permanent --zone=public --add-port=8080-8083/tcp


# 删除某个端口
firewall-cmd --permanent --zone=public --remove-port=81/tcp


# 针对某个 IP开放端口
firewall-cmd --permanent --add-rich-rule="rule family="ipv4" source address="192.168.142.166" port protocol="tcp" port="6379" accept"
firewall-cmd --permanent --add-rich-rule="rule family="ipv4" source address="192.168.0.233" accept"


# 删除某个IP
firewall-cmd --permanent --remove-rich-rule="rule family="ipv4" source address="192.168.1.51" accept"


# 针对一个ip段访问
firewall-cmd --permanent --add-rich-rule="rule family="ipv4" source address="192.168.0.0/16" accept"
firewall-cmd --permanent --add-rich-rule="rule family="ipv4" source address="192.168.1.0/24" port protocol="tcp" port="9200" accept"


# 添加操作后别忘了执行重载
firewall-cmd --reload
</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299398320</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:56:00 GMT</pubDate></item><item><title><![CDATA[Linux查看、修改SELinux防火墙]]></title><link>https://xiaoming728.com/archives/1702299378601</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%E6%9F%A5%E7%9C%8B%E3%80%81%E4%BF%AE%E6%94%B9SELinux%E9%98%B2%E7%81%AB%E5%A2%99&amp;url=/archives/1702299378601" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px; color: rgb(79, 79, 79)">SELinux(Security-Enhanced Linux) 是美国国家安全局（NSA）对于强制访问控制的实现，是 Linux历史上最杰出的新安全子系统。但是我们一般都不用它，因为它管的东西太多了，想做安全可以用防火墙等其他措施。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(79, 79, 79)">我们可以通过查看配置文件的命令</span><strong><span style="font-size: 15px; color: rgb(79, 79, 79)">cat /etc/selinux/config</span></strong><span style="font-size: 15px; color: rgb(79, 79, 79)">来查看状态</span></p>
<pre><code>[root@lill ~]# cat /etc/selinux/config

# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
# enforcing - SELinux security policy is enforced.
# permissive - SELinux prints warnings instead of enforcing.
# disabled - No SELinux policy is loaded.
SELINUX=enforcing
# SELINUXTYPE= can take one of these two values:
# targeted - Targeted processes are protected,
# mls - Multi Level Security protection.
SELINUXTYPE=targeted</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(79, 79, 79)">发现SELinux共有3个状态enforcing （执行中）、permissive （不执行但产生警告）、disabled（关闭）。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(79, 79, 79)">我们可以通过修改配置文件来修改SELinux的状态</span></p>
<pre><code>[root@lill ~]# sed -i s#SELINUX=enforcing#SELINUX=disabled# /etc/selinux/config
[root@lill ~]# cat /etc/selinux/config

# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
# enforcing - SELinux security policy is enforced.
# permissive - SELinux prints warnings instead of enforcing.
# disabled - No SELinux policy is loaded.
SELINUX=disabled
# SELINUXTYPE= can take one of these two values:
# targeted - Targeted processes are protected,
# mls - Multi Level Security protection.
SELINUXTYPE=targeted</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(79, 79, 79)">最后我们重启服务器，SELinux状态由enforcing变为disabled 。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(79, 79, 79)">当然，一般我们的Linux都不能重启，我们可以使用这个命令</span><strong><span style="font-size: 15px; color: rgb(79, 79, 79)">setenforce 1</span></strong><span style="font-size: 15px; color: rgb(79, 79, 79)">临时（重启即失效）设置SELinux 成为enforcing模式（</span><strong><span style="font-size: 15px; color: rgb(79, 79, 79)">setenforce 0</span></strong><span style="font-size: 15px; color: rgb(79, 79, 79)">设置SELinux 成为permissive模式），用getenforce这个命令来检查。</span></p>
<pre><code>root@lill ~]# setenforce 0
[root@lill ~]# getenforce
Permissive</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(79, 79, 79)">我们还可以用</span><strong><span style="font-size: 15px; color: rgb(79, 79, 79)">/usr/sbin/sestatus</span></strong><span style="font-size: 15px; color: rgb(79, 79, 79)">命令来查看SELinux的状态。</span></p>
<pre><code>[root@lill ~]# /usr/sbin/sestatus
SELinux status: enabled
SELinuxfs mount: /selinux
Current mode: enforcing
Mode from config file: disabled
Policy version: 24
Policy from config file: targeted</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299378601</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:56:00 GMT</pubDate></item><item><title><![CDATA[Linux用户、用户组、权限]]></title><link>https://xiaoming728.com/archives/1702299321136</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%E7%94%A8%E6%88%B7%E3%80%81%E7%94%A8%E6%88%B7%E7%BB%84%E3%80%81%E6%9D%83%E9%99%90&amp;url=/archives/1702299321136" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="linux%E7%94%A8%E6%88%B7%E7%BB%84%E4%BF%AE%E6%94%B9%E5%91%BD%E4%BB%A4"><strong>linux用户组修改命令</strong></h1>
<p style=""></p>
<h3 style="" id="linux%E4%BF%AE%E6%94%B9%E7%94%A8%E6%88%B7%E9%BB%98%E8%AE%A4%E7%BB%84">linux修改用户默认组</h3>
<p style="">usermod -g&nbsp;groupName&nbsp;userName</p>
<p style=""></p>
<h3 style="" id="linux%E4%BF%AE%E6%94%B9%E6%96%87%E4%BB%B6%E6%89%80%E5%B1%9E%E7%94%A8%E6%88%B7%E5%8F%8A%E7%94%A8%E6%88%B7%E7%BB%84">linux修改文件所属用户及用户组</h3>
<p style="">chown -R userName:groupName&nbsp; fileName</p>
<p style=""></p>
<h3 style="" id="linux%E4%BF%AE%E6%94%B9%E6%96%87%E4%BB%B6%E6%89%80%E5%B1%9E%E7%94%A8%E6%88%B7%E7%BB%84">linux修改文件所属用户组</h3>
<p style="">chown -R :groupName&nbsp; fileName</p>
<p style=""></p>
<h3 style="" id="linux%E5%88%A0%E9%99%A4%E7%94%A8%E6%88%B7%E5%8F%8A%E7%94%A8%E6%88%B7%E7%9B%AE%E5%BD%95">linux删除用户及用户目录</h3>
<p style="">userdel -r userName</p>
<h1 style="" id="linux%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%94%A8%E6%88%B7"><strong>linux查询所有用户</strong></h1>
<p style=""><br>
 使用任何一个像 <span style="font-size: 12px; color: rgb(199, 37, 78)">cat</span>、<span style="font-size: 12px; color: rgb(199, 37, 78)">more</span>、<span style="font-size: 12px; color: rgb(199, 37, 78)">less</span> 等文件操作命令来打印 Linux 系统上创建的用户列表。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 12px; color: rgb(199, 37, 78)">/etc/passwd</span> 是一个文本文件，其中包含了登录 Linux 系统所必需的每个用户的信息。它保存用户的有用信息，如用户名、密码、用户 ID、群组 ID、用户 ID 信息、用户的家目录和 Shell 。</p>
<p style="text-align: justify; "></p>
<p style="text-align: justify; "><span style="font-size: 12px; color: rgb(199, 37, 78)">/etc/passwd</span> 文件将每个用户的详细信息写为一行，其中包含七个字段，每个字段之间用冒号 <span style="font-size: 12px; color: rgb(199, 37, 78)">:</span> 分隔：</p>
<pre><code># cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
2gadmin:x:500:10::/home/viadmin:/bin/bash
apache:x:48:48:Apache:/var/www:/sbin/nologin
zabbix:x:498:499:Zabbix Monitoring System:/var/lib/zabbix:/sbin/nologin
mysql:x:497:502::/home/mysql:/bin/bash
zend:x:502:503::/u01/zend/zend/gui/lighttpd:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/cache/rpcbind:/sbin/nologin
2daygeek:x:503:504::/home/2daygeek:/bin/bash
named:x:25:25:Named:/var/named:/sbin/nologin
mageshm:x:506:507:2g Admin - Magesh M:/home/mageshm:/bin/bash</code></pre>
<p style=""><strong>7 个字段的详细信息如下：</strong></p>
<ul>
 <li>
  <p style=""><strong>用户名</strong> （<span style="font-size: 12px; color: rgb(199, 37, 78)">magesh</span>）： 已创建用户的用户名，字符长度 1 个到 12 个字符。</p></li>
 <li>
  <p style=""><strong>密码</strong>（<span style="font-size: 12px; color: rgb(199, 37, 78)">x</span>）：代表加密密码保存在 `/etc/shadow 文件中。</p></li>
 <li>
  <p style="">**用户 ID（<span style="font-size: 12px; color: rgb(199, 37, 78)">506</span>）：代表用户的 ID 号，每个用户都要有一个唯一的 ID 。UID 号为 0 的是为 <span style="font-size: 12px; color: rgb(199, 37, 78)">root</span> 用户保留的，UID 号 1 到 99 是为系统用户保留的，UID 号 100-999 是为系统账户和群组保留的。</p></li>
 <li>
  <p style="">**群组 ID （<span style="font-size: 12px; color: rgb(199, 37, 78)">507</span>）：代表群组的 ID 号，每个群组都要有一个唯一的 GID ，保存在 <span style="font-size: 12px; color: rgb(199, 37, 78)">/etc/group</span>文件中。</p></li>
 <li>
  <p style="">**用户信息（<span style="font-size: 12px; color: rgb(199, 37, 78)">2g Admin - Magesh M</span>）：代表描述字段，可以用来描述用户的信息（LCTT 译注：此处原文疑有误）。</p></li>
 <li>
  <p style="">**家目录（<span style="font-size: 12px; color: rgb(199, 37, 78)">/home/mageshm</span>）：代表用户的家目录。</p></li>
 <li>
  <p style="">**Shell（<span style="font-size: 12px; color: rgb(199, 37, 78)">/bin/bash</span>）：代表用户使用的 shell 类型。</p></li>
</ul>
<h1 style="" id="linux%E7%94%A8%E6%88%B7%E3%80%81%E7%94%A8%E6%88%B7%E7%BB%84%E3%80%81%E6%96%87%E4%BB%B6%E6%9D%83%E9%99%90"><strong>Linux用户、用户组、文件权限</strong></h1>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">最近打算更仔细学习一下linux操作系统。先是恶补了一下用户、用户组、文件权限这三样比较重要的知识。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">学习这几样东西，得先掌握linux的权限系统相关知识。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">linux的权限系统主要是由用户、用户组和权限组成。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户就是一个个的登录并使用linux的用户。linux内部用UID表示。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户组就是用户的分组。linux内部用GID表示。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">权限分为读、写、执行三种权限。</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">linux的用户信息保存在/etc/passwd文件中，另外，/etc/shadow文件存放的是用户密码相关信息。</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">/etc/passwd文件格式：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户名:密码:UID:GID:用户信息:HOME目录路径:用户shell</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">其中UID为0则是用户root，1～499为系统用户，500以上为普通用户</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">/etc/shadow保存用户密码信息，包括加密后的密码，密码过期时间，密码过期提示天数等。</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户组信息保存在/etc/group文件中.</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">格式如下：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户组名:组密码:GID:组内帐号（多个帐号用逗号分隔）</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户登录后，/etc/passwd文件里的GID为用户的初始用户组。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户的初始用户组这一事实不会再/etc/group中体现。</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">查看当前用户的用户组命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#groups</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">root bin daemon sys adm disk wheel</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">输出的信息中，第一个用户组为当前用户的有效用户组（当前用户组）</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">切换有效用户组命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#newgrp 用户组名</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">要离开新的有效用户组，则输入exit回车。</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">新建用户命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#useradd 用户名 -g 初始用户组 -G 其他用户组（修改/etc/group） -c 用户说明 -u 指定UID</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">建完用户需要为用户设置密码：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#passwd 用户名</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户要修改自己密码命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#passwd</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">修改用户信息命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#usermod 参数 用户名</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参数:</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">&nbsp;-c 说明</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">&nbsp;-g 组名 初始用户组</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-e 过期日期 格式：YYYY-MM-DD</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">&nbsp;-G 组名 其他用户组</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">&nbsp;-l 修改用户名</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">&nbsp;-L 锁定账号（在/etc/shadow文件中用户对应密码密码串的前面加上两个叹号(!!)）</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">&nbsp;-U 解锁</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">删除用户命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#userdel [-r] 用户名</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">其中，参数-r为删除用户的home目录。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">其实，可能在系统其他地方也有该用户文件，要完整删除一个用户和其文件要先找到属于他的文件：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#find / -user 用户名</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">然后删除，再运行userdel删除用户。</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">查看可用shell命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#chsh -l</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">修改自己的shell命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#chsh -s</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">查看自己或某人UID/GID信息：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#id [用户名]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">返回信息中groups为有效用户组</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">新增用户组命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#groupadd 用户组名</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">修改用户组名命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#groupmod -n 名称</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">删除用户组命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#groupdel 用户组名</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">设置用户组密码命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#gpasswd 用户组名</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">如果gpasswd加上参数则有其他功能</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">设置用户组管理员命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#gpasswd -A 用户名 用户组名</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">添加某帐号到组命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#gpasswd -M 用户名 用户组名</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">从组中删除某帐号命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#gpasswd -d 用户名 用户组名</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">passwd相关参数操作：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-l 锁用户</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-u 解锁用户</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-n 天数&nbsp; 密码不可改天数</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-x 天数&nbsp; 密码过期天数</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-w 天数&nbsp; 警告天数</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">&nbsp;文件权限知识</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">先看个实例：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#ls -al</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">ls -al 命令是列出目录的所有文件，包括隐藏文件。隐藏文件的文件名第一个字符为'.'</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-rw-r--r--&nbsp; 1 root root&nbsp;&nbsp;&nbsp; 81 08-02 14:54 gtkrc-1.2-gnome2</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-rw-------&nbsp; 1 root root&nbsp;&nbsp; 189 08-02 14:54 ICEauthority</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-rw-------&nbsp; 1 root root&nbsp;&nbsp;&nbsp; 35 08-05 10:02 .lesshst</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">drwx------&nbsp; 3 root root&nbsp; 4096 08-02 14:54 .metacity</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">drwxr-xr-x&nbsp; 3 root root&nbsp; 4096 08-02 14:54 nautilus</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">列表的列定义如下：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[权限属性信息] [连接数] [拥有者] [拥有者所属用户组] [大小] [最后修改时间] [文件名]</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">权限属性列表为10个字符：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第一个字符表示文件类型，d为目录 -为普通文件 l为连接 b为可存储的接口设备 c为键盘鼠标等输入设备</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">2、3、4个字符表示所有者权限，5、6、7个字符表示所有者同组用户权限，8、9、10为其他用户权限</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第二个字符表示所有者读权限，如果有权限则为r，没有权限则为-</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第三个字符表示所有者写权限，如果有权限则为w，没有权限则为-</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第四个字符表示所有者执行权限，如果有权限则为x，没有权限则为-</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第五个字符表示所有者同组用户读权限，如果有权限则为r，没有权限则为-</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第六个字符表示所有者同组用户写权限，如果有权限则为w，没有权限则为-</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第七个字符表示所有者同组用户执行权限，如果有权限则为x，没有权限则为-</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第八个字符表示其他非同组读权限，如果有权限则为r，没有权限则为-</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第九个字符表示其他非同组写权限，如果有权限则为w，没有权限则为-</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">第十个字符表示其他非同组执行权限，如果有权限则为x，没有权限则为-</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">修改文件所属组命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#chgrp [-R] 组名 文件名</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">其中-R为递归设置</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">修改文件的所有者和组命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#chown [-R] 用户[:用户组] 文件名</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">修改文件访问权限命令：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[root@local opt]#chmod [-R] 0777 文件名</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">至此，用户、文件和权限相关的东西，就总结个7788了，接下来的就是，平常要敢于用各种命令，勤于看看本篇总结啦。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">====================================================================</span></p>
<p style=""><strong>linux 查看用户及用户组的方法</strong></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">whois</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">功能说明：查找并显示用户信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">语　　法：whois [帐号名称]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">补充说明：whois指令会去查找并显示指定帐号的用户相关信息，因为它是到Network Solutions 的WHOIS数据库去查找，所以该帐号名称必须在上面注册方能寻获，且名称没有大小写的差别。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">---------------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">whoami</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">功能说明：先似乎用户名称。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">语　　法：whoami [--help][--version]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">补充说明：显示自身的用户名称，本指令相当于执行"id -un"指令。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参　　数：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--help 　在线帮助。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--version 　显示版本信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">---------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">who</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">功能说明：显示目前登入系统的用户信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">语　　法：who [-Himqsw][--help][--version][am i][记录文件]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">补充说明：执行这项指令可得知目前有那些用户登入系统，单独执行who指令会列出登入帐号，使用的 　　　终端机，登入时间以及从何处登入或正在使用哪个X显示器。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参　　数：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-H或--heading 　显示各栏位的标题信息列。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-i或-u或--idle 　显示闲置时间，若该用户在前一分钟之内有进行任何动作，将标示成"."号，如果该用户已超过24小时没有任何动作，则标示出"old"字符串。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-m 　此参数的效果和指定"am i"字符串相同。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-q或--count 　只显示登入系统的帐号名称和总人数。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-s 　此参数将忽略不予处理，仅负责解决who指令其他版本的兼容性问题。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-w或-T或--mesg或--message或--writable 　显示用户的信息状态栏。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--help 　在线帮助。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--version 　显示版本信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">----------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">w</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">功能说明：显示目前登入系统的用户信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">语　　法：w [-fhlsuV][用户名称]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">补充说明：执行这项指令可得知目前登入系统的用户有那些人，以及他们正在执行的程序。单独执行w</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">指令会显示所有的用户，您也可指定用户名称，仅显示某位用户的相关信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参　　数：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-f 　开启或关闭显示用户从何处登入系统。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-h 　不显示各栏位的标题信息列。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-l 　使用详细格式列表，此为预设值。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-s 　使用简洁格式列表，不显示用户登入时间，终端机阶段作业和程序所耗费的CPU时间。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-u 　忽略执行程序的名称，以及该程序耗费CPU时间的信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-V 　显示版本信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-----------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">finger命令</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">finger 命令的功能是查询用户的信息，通常会显示系统中某个用户的用户名、主目录、停滞时间、登录时间、登录shell等信息。如果要查询远程机上的用户信息，需要在用户名后面接“@主机名”，采用[用户名@主机名]的格式，不过要查询的网络主机需要运行finger守护进程。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">该命令的一般格式为：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">finger [选项] [使用者] [用户@主机]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">命令中各选项的含义如下：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-s 显示用户的注册名、实际姓名、终端名称、写状态、停滞时间、登录时间等信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-l 除了用-s选项显示的信息外，还显示用户主目录、登录shell、邮件状态等信息，以及用户主目录下的.plan、.project和.forward文件的内容。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-p 除了不显示.plan文件和.project文件以外，与-l选项相同。　</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">[例]在本地机上使用finger命令。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">$ finger xxq</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">Login: xxq Name:</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">Directory: /home/xxq Shell: /bin/bash</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">Last login Thu Jan 1 21:43 （CST） on tty1</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">No mail.</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">No Plan.　</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">$ finger</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">Login Name Tty Idle Login Time Office Office Phone</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">root root *1 28 Nov 25 09:17</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">……</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">------------------------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">/etc/group文件包含所有组</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">/etc/shadow和/etc/passwd系统存在的所有用户名</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">修改当前用户所属组的方法</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">usermod 或者可以直接修改 /etc/paaawd文件即可</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">----------------------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">vlock(virtual console lock)</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">功能说明：锁住虚拟终端。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">语　　法：vlock [-achv]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">补充说明：执行vlock指令可锁住虚拟终端，避免他人使用。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参　　数：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-a或--all 　锁住所有的终端阶段作业，如果您在全屏幕的终端中使用本参数，则会将用键盘</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">切换终端机的功能一并关闭。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-c或--current 　锁住目前的终端阶段作业，此为预设值。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-h或--help 　在线帮助。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-v或--version 　显示版本信息。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--------------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">usermod</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">功能说明：修改用户帐号。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">语　　法：usermod [-LU][-c &lt;备注&gt;][-d &lt;登入目录&gt;][-e &lt;有效期限&gt;][-f &lt;缓冲天数&gt;][-g &lt;群组&gt;][-G &lt;群组&gt;][-l &lt;帐号名称&gt;][-s ][-u ][用户帐号]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">补充说明：usermod可用来修改用户帐号的各项设定。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参　　数：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-c&lt;备注&gt; 　修改用户帐号的备注文字。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-d登入目录&gt; 　修改用户登入时的目录。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-e&lt;有效期限&gt; 　修改帐号的有效期限。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-f&lt;缓冲天数&gt; 　修改在密码过期后多少天即关闭该帐号。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-g&lt;群组&gt; 　修改用户所属的群组。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-G&lt;群组&gt; 　修改用户所属的附加群组。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-l&lt;帐号名称&gt; 　修改用户帐号名称。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-L 　锁定用户密码，使密码无效。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-s 　修改用户登入后所使用的shell。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-u 　修改用户ID。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-U 　解除密码锁定。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-------------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">userdel</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">功能说明：删除用户帐号。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">语　　法：userdel [-r][用户帐号]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">补充说明：userdel可删除用户帐号与相关的文件。若不加参数，则仅删除用户帐号，而不删除相关文件。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参　　数：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-f 　删除用户登入目录以及目录中所有文件。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">----------------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">userconf</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">功能说明：用户帐号设置程序。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">语　　法：userconf [--addgroup &lt;群组&gt;][--adduser &lt;用户ID&gt;&lt;群组&gt;&lt;用户名称&gt;][--delgroup &lt;群组&gt;][--deluser &lt;用户ID&gt;][--help]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">补充说明：userconf实际上为linuxconf的符号连接，提供图形界面的操作方式，供管理员建立与管理各类帐号。若不加任何参数，即进入图形界面。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参　　数：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--addgroup&lt;群组&gt; 　新增群组。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--adduser&lt;用户ID&gt;&lt;群组&gt;&lt;用户名称&gt; 　新增用户帐号。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--delgroup&lt;群组&gt; 　删除群组。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--deluser&lt;用户ID&gt; 　删除用户帐号。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">--help 　显示帮助。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">------------------------------------------------------</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">useradd</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">功能说明：建立用户帐号。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">语　　法：useradd [-mMnr][-c &lt;备注&gt;][-d &lt;登入目录&gt;][-e &lt;有效期限&gt;][-f &lt;缓冲天数&gt;][-g &lt;群组&gt;][-G &lt;群组&gt;][-s ][-u ][用户帐号] 或 useradd -D [-b][-e &lt;有效期限&gt;][-f &lt;缓冲天数&gt;][-g &lt;群组&gt;][-G &lt;群组&gt;][-s ]</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">补充说明：useradd可用来建立用户帐号。帐号建好之后，再用passwd设定帐号的密码．而可用userdel删除帐号。使用useradd指令所建立的帐号，实际上是保存在/etc/passwd文本文件中。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">参　　数：</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-c&lt;备注&gt; 　加上备注文字。备注文字会保存在passwd的备注栏位中。　</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-d&lt;登入目录&gt; 　指定用户登入时的启始目录。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-D 　变更预设值．</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-e&lt;有效期限&gt; 　指定帐号的有效期限。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-f&lt;缓冲天数&gt; 　指定在密码过期后多少天即关闭该帐号。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-g&lt;群组&gt; 　指定用户所属的群组。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-G&lt;群组&gt; 　指定用户所属的附加群组。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-m 　自动建立用户的登入目录。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-M 　不要自动建立用户的登入目录。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-n 　取消建立以用户名称为名的群组．</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-r 　建立系统帐号。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-s　 　指定用户登入后所使用的shell。</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">-u 　指定用户ID。</span></p>
<h1 style="" id="linux-%E7%94%A8%E6%88%B7%EF%BC%88user%EF%BC%89%E5%92%8C%E7%94%A8%E6%88%B7%E7%BB%84%EF%BC%88group%EF%BC%89%E6%A6%82%E8%BF%B0"><strong>Linux 用户（user）和用户组（group）概述</strong></h1>
<h3 style="" id="%E4%B8%80%E3%80%81%E7%90%86%E8%A7%A3linux%E7%9A%84%E5%8D%95%E7%94%A8%E6%88%B7%E5%A4%9A%E4%BB%BB%E5%8A%A1%EF%BC%8C%E5%A4%9A%E7%94%A8%E6%88%B7%E5%A4%9A%E4%BB%BB%E5%8A%A1%E6%A6%82%E5%BF%B5%EF%BC%9B">一、理解Linux的单用户多任务，多用户多任务概念；</h3>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">Linux 是一个多用户、多任务的操作系统；我们应该了解单用户多任务和多用户多任务的概念；</span></p>
<p style=""></p>
<h4 style="" id="1%E3%80%81linux-%E7%9A%84%E5%8D%95%E7%94%A8%E6%88%B7%E5%A4%9A%E4%BB%BB%E5%8A%A1%EF%BC%9B"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">1、Linux 的单用户多任务；</span></h4>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">单用户多任务；比如我们以beinan 登录系统，进入系统后，我要打开gedit 来写文档，但在写文档的过程中，我感觉少点音乐，所以又打开xmms 来点音乐；当然听点音乐还不行，MSN 还得打开，想知道几个弟兄现在正在做什么，这样一样，我在用beinan 用户登录时，执行了gedit 、xmms以及msn等，当然还有输入法fcitx ；这样说来就有点简单了，一个beinan用户，为了完成工作，执行了几个任务；当然beinan这个用户，其它的人还能以远程登录过来，也能做其它的工作。</span></p>
<p style=""></p>
<h4 style="" id="2%E3%80%81linux-%E7%9A%84%E5%A4%9A%E7%94%A8%E6%88%B7%E3%80%81%E5%A4%9A%E4%BB%BB%E5%8A%A1%EF%BC%9B"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">2、Linux 的多用户、多任务；</span></h4>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">有时可能是很多用户同时用同一个系统，但并不所有的用户都一定都要做同一件事，所以这就有多用户多任务之说；</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">举个例子，</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://比如LinuxSir.Org"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">比如LinuxSir.Org</span></a><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)"> 服务器，上面有FTP 用户、系统管理员、web 用户、常规普通用户等，在同一时刻，可能有的弟兄正在访问论坛；有的可能在上传软件包管理子站，比如luma 或Yuking 兄在管理他们的主页系统和FTP ；在与此同时，可能还会有系统管理员在维护系统；浏览主页的用的是nobody 用户，大家都用同一个，而上传软件包用的是FTP用户；管理员的对系统的维护或查看，可能用的是普通帐号或超级权限root帐号；不同用户所具有的权限也不同，要完成不同的任务得需要不同的用户，也可以说不同的用户，可能完成的工作也不一样；</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">值得注意的是：多用户多任务并不是大家同时挤到一接在一台机器的的键盘和显示器前来操作机器，多用户可能通过远程登录来进行，比如对服务器的远程控制，只要有用户权限任何人都是可以上去操作或访问的；</span></p>
<p style=""></p>
<h4 style="" id="3%E3%80%81%E7%94%A8%E6%88%B7%E7%9A%84%E8%A7%92%E8%89%B2%E5%8C%BA%E5%88%86%EF%BC%9B"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">3、用户的角色区分；</span></h4>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户在系统中是分角色的，在Linux 系统中，由于角色不同，权限和所完成的任务也不同；值得注意的是用户的角色是通过UID和识别的，特别是UID；在系统管理中，系统管理员一定要坚守UID 唯一的特性；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">root 用户：系统唯一，是真实的，可以登录系统，可以操作系统任何文件和命令，拥有最高权限；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">虚拟用户：这类用户也被称之为伪用户或假用户，与真实用户区分开来，这类用户不具有登录系统的能力，但却是系统运行不可缺少的用户，比如bin、daemon、adm、ftp、mail等；这类用户都系统自身拥有的，而非后来添加的，当然我们也可以添加虚拟用户；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">普通真实用户：这类用户能登录系统，但只能操作自己家目录的内容；权限有限；这类用户都是系统管理员自行添加的；</span></p>
<p style=""></p>
<h4 style="" id="4%E3%80%81%E5%A4%9A%E7%94%A8%E6%88%B7%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E5%AE%89%E5%85%A8%EF%BC%9B"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">4、多用户操作系统的安全；</span></h4>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">多用户系统从事实来说对系统管理更为方便。从安全角度来说，多用户管理的系统更为安全，比如beinan用户下的某个文件不想让其它用户看到，只是设置一下文件的权限，只有beinan一个用户可读可写可编辑就行了，这样一来只有beinan一个用户可以对其私有文件进行操作，Linux 在多用户下表现最佳，Linux能很好的保护每个用户的安全，但我们也得学会Linux 才是，再安全的系统，如果没有安全意识的管理员或管理技术，这样的系统也不是安全的。</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">从服务器角度来说，多用户的下的系统安全性也是最为重要的，我们常用的Windows 操作系统，它在系纺权限管理的能力只能说是一般般，根本没有没有办法和Linux或Unix 类系统相比；</span></p>
<p style=""></p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E7%94%A8%E6%88%B7(user%EF%BC%89%E5%92%8C%E7%94%A8%E6%88%B7%E7%BB%84%EF%BC%88group%EF%BC%89%E6%A6%82%E5%BF%B5%EF%BC%9B">二、用户(user）和用户组（group）概念；</h3>
<p style=""></p>
<h4 style="" id="1%E3%80%81%E7%94%A8%E6%88%B7%EF%BC%88user%EF%BC%89%E7%9A%84%E6%A6%82%E5%BF%B5%EF%BC%9B"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">1、用户（user）的概念；</span></h4>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">通过前面对Linux 多用户的理解，我们明白Linux 是真正意义上的多用户操作系统，所以我们能在Linux系统中建若干用户（user）。比如我们的同事想用我的计算机，但我不想让他用我的用户名登录，因为我的用户名下有不想让别人看到的资料和信息（也就是隐私内容）这时我就可以给他建一个新的用户名，让他用我所开的用户名去折腾，这从计算机安全角度来说是符合操作规则的；</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">当然用户（user）的概念理解还不仅仅于此，在Linux系统中还有一些用户是用来完成特定任务的，比如nobody和ftp 等，</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://我们访问LinuxSir.Org"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">我们访问LinuxSir.Org</span></a><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)"> 的网页程序，就是nobody用户；我们匿名访问ftp 时，会用到用户ftp或nobody ；如果您想了解Linux系统的一些帐号，请查看 /etc/passwd ；</span></p>
<p style=""></p>
<h4 style="" id="2%E3%80%81%E7%94%A8%E6%88%B7%E7%BB%84%EF%BC%88group%EF%BC%89%E7%9A%84%E6%A6%82%E5%BF%B5%EF%BC%9B"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">2、用户组（group）的概念；</span></h4>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户组（group）就是具有相同特征的用户（user）的集合体；比如有时我们要让多个用户具有相同的权限，比如查看、修改某一文件或执行某个命令，这时我们需要用户组，我们把用户都定义到同一用户组，我们通过修改文件或目录的权限，让用户组具有一定的操作权限，这样用户组下的用户对该文件或目录都具有相同的权限，这是我们通过定义组和修改文件的权限来实现的；</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">举例：我们为了让一些用户有权限查看某一文档，比如是一个时间表，而编写时间表的人要具有读写执行的权限，我们想让一些用户知道这个时间表的内容，而不让他们修改，所以我们可以把这些用户都划到一个组，然后来修改这个文件的权限，让用户组可读，这样用户组下面的每个用户都是可读的；</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">用户和用户组的对应关系是：一对一、多对一、一对多或多对多；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">一对一：某个用户可以是某个组的唯一成员；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">多对一：多个用户可以是某个唯一的组的成员，不归属其它用户组；比如beinan和linuxsir两个用户只归属于beinan用户组；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">一对多：某个用户可以是多个用户组的成员；比如beinan可以是root组成员，也可以是linuxsir用户组成员，还可以是adm用户组成员；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">多对多：多个用户对应多个用户组，并且几个用户可以是归属相同的组；其实多对多的关系是前面三条的扩展；理解了上面的三条，这条也能理解；</span></p>
<p style=""></p>
<h3 style="" id="%E4%B8%89%E3%80%81%E7%94%A8%E6%88%B7%EF%BC%88user%EF%BC%89%E5%92%8C%E7%94%A8%E6%88%B7%E7%BB%84%EF%BC%88group%EF%BC%89%E7%9B%B8%E5%85%B3%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E3%80%81%E5%91%BD%E4%BB%A4%E6%88%96%E7%9B%AE%E5%BD%95%EF%BC%9B">三、用户（user）和用户组（group）相关的配置文件、命令或目录；</h3>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">1、与用户（user）和用户组（group）相关的配置文件；</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">1）与用户（user）相关的配置文件；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">/etc/passwd 注：用户（user）的配置文件；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">/etc/shadow 注：用户（user）影子口令文件；</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">2）与用户组（group）相关的配置文件；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">/etc/group 注：用户组（group）配置文件；</span></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">/etc/gshadow 注：用户组（group）的影子文件；</span><br><br></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299321136</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:56:00 GMT</pubDate></item><item><title><![CDATA[Linux 查看磁盘空间]]></title><link>https://xiaoming728.com/archives/1702299289346</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%20%E6%9F%A5%E7%9C%8B%E7%A3%81%E7%9B%98%E7%A9%BA%E9%97%B4&amp;url=/archives/1702299289346" width="1" height="1" alt="" style="opacity:0;">
<p style="">Linux 查看磁盘空间可以使用 df 和 du 命令。</p>
<h3 style="" id="df%E5%91%BD%E4%BB%A4">df命令</h3>
<p style="">df 以磁盘分区为单位查看文件系统，可以获取硬盘被占用了多少空间，目前还剩下多少空间等信息。</p>
<p style="">例如，我们使用df -h命令来查看磁盘信息， -h 选项为根据大小适当显示：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ysmn.png&amp;size=m" style="display: inline-block"></p>
<p style="">显示参数内容说明：</p>
<ul>
 <li>
  <p style=""><strong>Filesystem</strong>：文件系统</p></li>
 <li>
  <p style=""><strong>Size</strong>： 分区大小</p></li>
 <li>
  <p style=""><strong>Used</strong>： 已使用容量</p></li>
 <li>
  <p style=""><strong>Avail</strong>： 还可以使用的容量</p></li>
 <li>
  <p style=""><strong>Use%</strong>： 已用百分比</p></li>
 <li>
  <p style=""><strong>Mounted on</strong>： 挂载点　</p></li>
</ul>
<p style=""><strong>相关命令</strong></p>
<ul>
 <li>
  <p style=""><code>df -hl</code>：查看磁盘剩余空间</p></li>
 <li>
  <p style=""><code>df -h</code>：查看每个根路径的分区大小</p></li>
 <li>
  <p style=""><code>du -sh [目录名]</code>：返回该目录的大小</p></li>
 <li>
  <p style=""><code>du -sm [文件夹]</code>：返回该文件夹总M数</p></li>
 <li>
  <p style=""><code>du -h [目录名]</code>：查看指定文件夹下的所有文件大小（包含子文件夹）</p></li>
</ul>
<h3 style="" id="du-%E5%91%BD%E4%BB%A4">du 命令</h3>
<p style="">du 的英文原义为 disk usage，含义为显示磁盘空间的使用情况，用于查看当前目录的总大小。</p>
<p style="">例如查看当前目录的大小：</p>
<pre><code># du -sh
605M    </code></pre>
<p style="">显示指定文件所占空间：</p>
<pre><code># du log2012.log 
300     log2012.log</code></pre>
<p style="">方便阅读的格式显示test目录所占空间情况：</p>
<pre><code># du -h test
608K    test/test6
308K    test/test4
4.0K    test/scf/lib
4.0K    test/scf/service/deploy/product
4.0K    test/scf/service/deploy/info
12K     test/scf/service/deploy
16K     test/scf/service
4.0K    test/scf/doc
4.0K    test/scf/bin
32K     test/scf
8.0K    test/test3
1.3M    test</code></pre>
<p style="">du 命令用于查看当前目录的总大小：</p>
<ul>
 <li>
  <p style="">-s：对每个Names参数只给出占用的数据块总数。</p></li>
 <li>
  <p style="">-a：递归地显示指定目录中各文件及子目录中各文件占用的数据块数。若既不指定-s，也不指定-a，则只显示Names中的每一个目录及其中的各子目录所占的磁盘块数。</p></li>
 <li>
  <p style="">-b：以字节为单位列出磁盘空间使用情况（系统默认以k字节为单位）。</p></li>
 <li>
  <p style="">-k：以1024字节为单位列出磁盘空间使用情况。</p></li>
 <li>
  <p style="">-c：最后再加上一个总计（系统默认设置）。</p></li>
 <li>
  <p style="">-l：计算所有的文件大小，对硬链接文件，则计算多次。</p></li>
 <li>
  <p style="">-x：跳过在不同文件系统上的目录不予统计。</p></li>
 <li>
  <p style="">-h：以K，M，G为单位，提高信息的可读性。</p></li>
</ul>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299289346</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:55:00 GMT</pubDate></item><item><title><![CDATA[Linux安装部署Redis(超级详细)]]></title><link>https://xiaoming728.com/archives/1702299066924</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%E5%AE%89%E8%A3%85%E9%83%A8%E7%BD%B2Redis%28%E8%B6%85%E7%BA%A7%E8%AF%A6%E7%BB%86%29&amp;url=/archives/1702299066924" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="%E5%89%8D%E8%A8%80"><strong>前言</strong></h2>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">网上搜索了一筐如何在Linux下安装部署Redis的文章，各种文章混搭在一起勉强安装成功了。自己也记录下，方便后续安装时候有个借鉴之处。</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">Redis版本 5.0.4</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">服务器版本 Linux CentOS 7.6 64位</span></p></li>
</ul>
<h3 style="" id="%E4%B8%8B%E8%BD%BDredis"><strong>下载Redis</strong></h3>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">进入官网找到下载地址&nbsp;</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://redis.io/download">https://redis.io/download</a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-uqbw.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">右键Download按钮，选择复制链接。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">进入到Xshell控制台(默认当前是root根目录)，输入wget 将上面复制的下载链接粘贴上，如下命令:</span></p>
<p style="">wget <a target="_blank" rel="noopener noreferrer nofollow" href="https://redis.io/download">http://download.redis.io/releases/redis-5.0.7.tar.gz</a></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">敲入回车键执行后如下图:</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-euyo.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">等待下载完成。</span></p>
<h3 style="" id="%E8%A7%A3%E5%8E%8B%E5%B9%B6%E5%AE%89%E8%A3%85redis"><strong>解压并安装Redis</strong></h3>
<p style=""><strong>&nbsp;解压</strong></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">下载完成后需要将压缩文件解压，输入以下命令解压到当前目录</span></p>
<pre><code>tar -zvxf redis-5.0.7.tar.gz</code></pre>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">解压后在根目录上输入ls 列出所有目录会发现与下载redis之前多了一个redis-5.0.7.tar.gz文件和 redis-5.0.7的目录。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-yvfj.png&amp;size=m" style="display: inline-block"></p>
<p style=""><strong>移动redis目录</strong></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">一般都会将redis目录放置到 /usr/local/redis目录，所以这里输入下面命令将目前在/root目录下的redis-5.0.7文件夹更改目录，同时更改文件夹名称为redis。</span></p>
<pre><code>mv /root/redis-5.0.7 /usr/local/redis</code></pre>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">cd 到/usr/local目录下输入ls命令可以查询到当前目录已经多了一个redis子目录，同时/root目录下已经没有redis-5.0.7文件夹</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ivqk.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span></p>
<p style=""><strong>编译</strong></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">cd到/usr/local/redis目录，输入命令make执行编译命令，接下来控制台会输出各种编译过程中输出的内容。</span></p>
<pre><code>make</code></pre>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">最终运行结果如下:</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1620366813527-4eb2ea85-a324-4951-bf80-7925bddd385a.png&amp;size=m" width="360" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span><strong>安装</strong></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">输入以下命令</span></p>
<pre><code>make PREFIX=/usr/local/redis install</code></pre>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">这里多了一个关键字</span> <strong>PREFIX=</strong> <span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">这个关键字的作用是编译的时候用于指定程序存放的路径。比如我们现在就是指定了redis必须存放在/usr/local/redis目录。假设不添加该关键字Linux会将可执行文件存放在/usr/local/bin目录，</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">库文件会存放在/usr/local/lib目录。配置文件会存放在/usr/local/etc目录。其他的资源文件会存放在usr/local/share目录。这里指定号目录也方便后续的卸载，后续直接rm -rf /usr/local/redis 即可删除redis。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">执行结果如下图:</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-fqlg.png&amp;size=m" style="display: inline-block"></p>
<h4 style="" id="%E5%90%AF%E5%8A%A8redis"><strong>启动redis</strong></h4>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">根据上面的操作已经将redis安装完成了。在目录/usr/local/redis 输入下面命令启动redis</span></p>
<pre><code>./bin/redis-server ./redis.conf</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-vcxo.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;上面是采取显示启动方式(如在配置文件设置了daemonize属性为yes则是后台进程方式启动)。</span></p>
<h4 style="" id="redis.conf%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6"><strong>redis.conf配置文件</strong></h4>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">在目录/usr/local/redis下有一个redis.conf的配置文件。我们上面启动方式就是执行了该配置文件的配置运行的。我么可以通过cat、vim、less等Linux内置的读取命令读取该文件。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">也可以通过redis-cli命令进入redis控制台后通过CONFIG GET * 的方式读取所有配置项。 如下：</span></p>
<pre><code>redis-cli
CONFIG GET *</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xiin.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">回车确认后会将所有配置项读取出来，如下图</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-kepv.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">这里列举下比较重要的配置项</span></p>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">配置项名称</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">配置项值范围</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">说明</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">daemonize</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">yes、no</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">yes表示启用守护进程，默认是no即不以守护进程方式运行。其中Windows系统下不支持启用守护进程方式运行</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">port</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">指定 Redis 监听端口，默认端口为 6379</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">bind</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">绑定的主机地址,如果需要设置远程访问则直接将这个属性备注下或者改为bind * 即可,这个属性和下面的protected-mode控制了是否可以远程访问 。</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">protected-mode</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">yes 、no</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">保护模式，该模式控制外部网是否可以连接redis服务，默认是yes,所以默认我们外网是无法访问的，如需外网连接rendis服务则需要将此属性改为no。</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">timeout</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">300</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">当客户端闲置多长时间后关闭连接，如果指定为 0，表示关闭该功能</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">loglevel</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">debug、verbose、notice、warning</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">日志级别，默认为 notice</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">databases</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">16</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">设置数据库的数量，默认的数据库是0。整个通过客户端工具可以看得到</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">rdbcompression</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">yes、no</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">指定存储至本地数据库时是否压缩数据，默认为 yes，Redis 采用 LZF 压缩，如果为了节省 CPU 时间，可以关闭该选项，但会导致数据库文件变的巨大。</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">dbfilename</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">dump.rdb</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">指定本地数据库文件名，默认值为 dump.rdb</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">dir</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">指定本地数据库存放目录</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">requirepass</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: #FFE8E6">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">设置 Redis 连接密码，如果配置了连接密码，客户端在连接 Redis 时需要通过 AUTH &lt;password&gt; 命令提供密码，默认关闭</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">maxclients</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">0</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">设置同一时间最大客户端连接数，默认无限制，Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数，如果设置 maxclients 0，表示不作限制。当客户端连接数到达限制时，Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息。</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">maxmemory</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">XXX &lt;bytes&gt;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">指定 Redis 最大内存限制，Redis 在启动时会把数据加载到内存中，达到最大内存后，Redis 会先尝试清除已到期或即将到期的 Key，当此方法处理 后，仍然到达最大内存设置，将无法再进行写入操作，但仍然可以进行读取操作。Redis 新的 vm 机制，会把 Key 存放内存，Value 会存放在 swap 区。配置项值范围列里XXX为数值。</span></p></td>
  </tr>
 </tbody>
</table>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">这里我要将daemonize改为yes，不然我每次启动都得在redis-server命令后面加符号&amp;，不这样操作则只要回到Linux控制台则redis服务会自动关闭，同时也将bind注释，将protected-mode设置为no。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">这样启动后我就可以在外网访问了。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">更改方式:&nbsp;</span></p>
<pre><code>vim /usr/local/redis/redis.conf</code></pre>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">通过 /daemonize&nbsp; 查找到属性，默认是no，更改为yes即可。 (通过/关键字查找出现多个结果则使用 n字符切换到下一个即可，查找到结果后输入:noh退回到正常模式)</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">如下图:</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-aiag.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;其他两个属性也是同样方式查找和编辑即可。</span></p>
<h3 style="" id="%E6%9F%A5%E7%9C%8Bredis%E6%98%AF%E5%90%A6%E6%AD%A3%E5%9C%A8%E8%BF%90%E8%A1%8C"><strong>&nbsp;查看Redis是否正在运行</strong></h3>
<p style=""><strong>1、采取查看进程方式</strong></p>
<pre><code>ps -aux | grep redis</code></pre>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">结果如下图：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-orkv.png&amp;size=m" style="display: inline-block"></p>
<p style=""><strong>2、采取端口监听查看方式</strong></p>
<pre><code>netstat -lanp | grep 6379</code></pre>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">结果如下图：</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-oqqi.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="redis-cli"><strong>redis-cli</strong></h3>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">redis-cli是连接本地redis服务的一个命令，通过该命令后可以既然怒redis的脚本控制台。如下图</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ikpr.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">输入exit可以退出redis脚本控制台</span></p>
<h3 style="" id="%E5%85%B3%E9%97%AD%E8%BF%90%E8%A1%8C%E4%B8%AD%E7%9A%84redis%E6%9C%8D%E5%8A%A1"><strong>关闭运行中的Redis服务</strong></h3>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">输入redis-cli</span> <span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">进入控制台后输入命令shutdown即可关闭运行中的Redis服务了。如下图:</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-hazt.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="%E8%BF%9C%E7%A8%8B%E8%BF%9E%E6%8E%A5%E4%B8%8D%E4%B8%8A%E9%97%AE%E9%A2%98"><strong>远程连接不上问题</strong></h3>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">如下图，已经开放了Redis服务的ip不为127.0.0.1,理论上远程客户端应该可以连接了，而且云服务器的端口号也在安全组里开放了。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qbpo.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">后面发现是启动命令的问题，因为我比较偷懒，启动redis我都是直接输入命令</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">redis-server</span> <span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">或</span> <span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">redis-server&amp;</span> <span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">这两种方式都是直接读取默认的配置文件启动，无非前者是显示启动后者是作为后台应用启动。我其实也很纳闷，因为我修改的就是默认的配置文件啊，我并没有重新生成新的配置文件，但是确实我输入命令</span> <span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">redis-server /usr/local/redis/etc/redis.conf</span> <span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">就是能成功，而且我输入命令redis-server&amp; /usr/local/redis/etc/redis.conf也是远程登录失败。</span></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">关于直接输入redis-server不行的问题我还怀疑是不是Linux缓存问题，我重启服务器尝试下。结果还是一样的。。。哎先不纠结了 后续再去找原因吧</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-wdfv.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">&nbsp;</span></p>
<p style=""></p>
<blockquote>
 <p style="">来源：博客园 -长沙大鹏</p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://redis.io/download">https://www.cnblogs.com/hunanzp/p/12304622.html</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299066924</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fredis.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Redis技术</category><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:54:00 GMT</pubDate></item><item><title><![CDATA[Linux的关机与重启命令]]></title><link>https://xiaoming728.com/archives/1702299270763</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%E7%9A%84%E5%85%B3%E6%9C%BA%E4%B8%8E%E9%87%8D%E5%90%AF%E5%91%BD%E4%BB%A4&amp;url=/archives/1702299270763" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px">重启命令：<br>
  1、reboot<br>
  2、shutdown -r now 立刻重启(root用户使用)<br>
  3、shutdown -r 10 过10分钟自动重启(root用户使用)<br>
  4、shutdown -r 20:35 在时间为20:35时候重启(root用户使用)<br>
  如果是通过shutdown命令设置重启的话，可以用shutdown -c命令取消重启</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">关机命令：<br>
  1、halt 立刻关机<br>
  2、poweroff 立刻关机<br>
  3、shutdown -h now 立刻关机(root用户使用)<br>
  4、shutdown -h 10 10分钟后自动关机<br>
  如果是通过shutdown命令设置关机的话，可以用shutdown -c命令取消重启</span></p>]]></description><guid isPermaLink="false">/archives/1702299270763</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:54:00 GMT</pubDate></item><item><title><![CDATA[Linux服务器优化]]></title><link>https://xiaoming728.com/archives/1702299035609</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%BC%98%E5%8C%96&amp;url=/archives/1702299035609" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1%E3%80%81%E6%9F%A5%E7%9C%8B%E7%94%A8%E6%88%B7%E8%BF%9B%E7%A8%8B%E9%99%90%E5%88%B6">1、查看<span fontsize="" color="rgb(77, 77, 77)" style="color: rgb(77, 77, 77)">用户进程限制</span></h3>
<pre><code> # ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 128595
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024			//最大文件打开数
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 31784		//用户最大的进程数
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited</code></pre>
<h3 style="" id="2%E3%80%81%E8%AE%BE%E7%BD%AE%E7%94%A8%E6%88%B7%E8%BF%9B%E7%A8%8B%E9%99%90%E5%88%B6">2、设置<span fontsize="" color="rgb(77, 77, 77)" style="color: rgb(77, 77, 77)">用户进程限制</span></h3>
<p style=""><span style="font-size: 16px">* 代表针对所有用户,noproc 是代表最大进程数,nofile 是代表最大文件打开数</span></p>
<pre><code># vi /etc/security/limits.conf

* soft nofile 655360
* hard nofile 655360
* soft nproc 655360
* hard nproc 65536</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(51, 51, 51)">改完重启电脑即可，或者直接</span><code>source /etc/profile</code><span style="font-size: 16px; color: rgb(51, 51, 51)"> 。</span></p>
<h3 style="" id="3%E3%80%81%E8%AE%BE%E7%BD%AE%E6%99%AE%E9%80%9A%E7%94%A8%E6%88%B7%E8%BF%9B%E7%A8%8B%E9%99%90%E5%88%B6"><span fontsize="" color="rgb(51, 51, 51)" style="color: rgb(51, 51, 51)">3、设置</span>普通用户进程限制</h3>
<p style=""><span style="font-size: 16px">修改20-nproc.conf文件，将普通用户的进程数改到他能达到的最大值</span></p>
<pre><code>vi /etc/security/limits.d/20-nproc.conf

# cat /etc/security/limits.d/20-nproc.conf
# Default limit for number of user's processes to prevent
# accidental fork bombs.
# See rhbz #432903 for reasoning.
 
*          soft    nproc     65536
root       soft    nproc     unlimited</code></pre>
<h3 style="" id="4%E3%80%81%E8%AE%BE%E7%BD%AElinux%E7%B3%BB%E7%BB%9F%E5%8F%82%E6%95%B0">4、设置Linux系统参数</h3>
<pre><code>vi /etc/sysctl.conf

#timewait 的数量，默认是 180000。
net.ipv4.tcp_max_tw_buckets = 12000

net.ipv4.ip_local_port_range = 1024 65000

#启用 timewait 快速回收。
net.ipv4.tcp_tw_recycle = 1

#开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接。
net.ipv4.tcp_tw_reuse = 1

# 生效命令
/sbin/sysctl -p</code></pre>
<h3 style="" id=""></h3>
<blockquote>
 <h3 style="" id="%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</h3>
 <p style="">Linux查看、修改SELinux的状态：<a target="_blank" rel="noopener noreferrer nofollow" href="https://linewell.yuque.com/oill7w/newbie/nl20ft">https://linewell.yuque.com/oill7w/newbie/nl20ft</a></p>
 <p style="">解决 ClientAbortException: <a target="_blank" rel="noopener noreferrer nofollow" href="https://linewell.yuque.com/oill7w/newbie/nl20ft">java.io</a>.IOException: Broken pipe：<a target="_blank" rel="noopener noreferrer nofollow" href="https://linewell.yuque.com/oill7w/newbie/nl20ft">https://linewell.yuque.com/oill7w/newbie/yiv0gh</a></p>
 <p style="">解决 java.lang.OutOfMemoryError： unable to create new native thread：<a target="_blank" rel="noopener noreferrer nofollow" href="https://linewell.yuque.com/oill7w/newbie/nl20ft">https://linewell.yuque.com/oill7w/newbie/ovxemv</a></p>
 <p style="">记一次性能优化，单台 4 核 8G 机器支撑 5 万 QPS：<a target="_blank" rel="noopener noreferrer nofollow" href="https://linewell.yuque.com/oill7w/newbie/nl20ft">https://linewell.yuque.com/oill7w/newbie/kolah5</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702299035609</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:50:41 GMT</pubDate></item><item><title><![CDATA[Docker环境搭建XXL-JOB自动任务]]></title><link>https://xiaoming728.com/archives/1702299014356</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BAXXL-JOB%E8%87%AA%E5%8A%A8%E4%BB%BB%E5%8A%A1&amp;url=/archives/1702299014356" width="1" height="1" alt="" style="opacity:0;">
<p style="">仓库地址 <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/xuxueli/xxl-job">https://github.com/xuxueli/xxl-job</a></p>
<p style="">官方文档 <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/xuxueli/xxl-job">https://www.xuxueli.com/xxl-job/</a></p>
<h3 style="" id="1%E3%80%81%E4%B8%8B%E8%BD%BD%E9%95%9C%E5%83%8F">1、下载镜像</h3>
<pre><code>docker pull xuxueli/xxl-job-admin 2.3.0</code></pre>
<p style="">需指定版本号,目前最新2.3.0</p>
<h3 style="" id="2%E3%80%81%E5%88%9B%E5%BB%BA%E5%AE%B9%E5%99%A8%E5%B9%B6%E8%BF%90%E8%A1%8C">2、创建容器并运行</h3>
<p style="">Docker地址：<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/xuxueli/xxl-job">https://hub.docker.com/r/xuxueli/xxl-job-admin/</a></p>
<pre><code>docker run -d -p 6080:8080 \
-v /home/docker/xxl-job/applogs:/data/applogs \
-e PARAMS="--spring.datasource.url=jdbc:mysql://127.0.0.1:3306/dev_archives_center?serverTimezone=GMT%2B8&amp;useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;allowMultiQueries=true --spring.datasource.username=root --spring.datasource.password=123" \
--name xxl-job-admin \
xuxueli/xxl-job-admin:2.3.0</code></pre>
<p style="">如需自定义 mysql 等配置，可通过 "-e PARAMS" 指定，参数格式 PARAMS="--key=value --key2=value2" ；</p>
<p style="">配置项参考文件：</p>
<pre><code>### web
server.port=8080
server.servlet.context-path=/xxl-job-admin

### actuator
management.server.servlet.context-path=/actuator
management.health.mail.enabled=false

### resources
spring.mvc.servlet.load-on-startup=0
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/

### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.##########

### mybatis
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
#mybatis.type-aliases-package=com.xxl.job.admin.core.model

### xxl-job, datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/dev_archives_center?serverTimezone=GMT%2B8&amp;useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

### datasource-pool
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.validation-timeout=1000

### xxl-job, email
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.from=xxx@qq.com
spring.mail.password=
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory

### xxl-job, access token
xxl.job.accessToken=

### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en")
xxl.job.i18n=zh_CN

## xxl-job, triggerpool max size
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100

### xxl-job, log retention days
xxl.job.logretentiondays=30</code></pre>
<ul>
 <li>
  <p style="">spring.mail可设置报警邮箱</p></li>
 <li>
  <p style="">如需自定义 JVM内存参数 等配置，可通过 "-e JAVA_OPTS" 指定，参数格式 JAVA_OPTS="-Xmx512m" ；</p></li>
 <li>
  <p style="">务必指定日志挂载路径 /data/applogs 到自定义的日志路径</p></li>
</ul>
<h3 style="" id="3%E3%80%81%E6%89%A7%E8%A1%8C%E5%99%A8%E9%85%8D%E7%BD%AE">3、执行器配置</h3>
<p style="">归档项目中center服务为执行器,负责接收调度中心发来的调度请求,执行对应任务</p>
<p style="">配置类:</p>
<pre><code>@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
    logger.info("&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; xxl-job config init.");
    XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
    xxlJobSpringExecutor.setAppname(appname);
    xxlJobSpringExecutor.setIp(ip);
    xxlJobSpringExecutor.setPort(port);
    xxlJobSpringExecutor.setAccessToken(accessToken);
    xxlJobSpringExecutor.setLogPath(logPath);
    xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
    return xxlJobSpringExecutor;
}</code></pre>
<p style="">配置文件:</p>
<pre><code>#任务调度
xxl:
  job:
    accessToken:                        # 执行器通讯TOKEN [选填]：非空时启用；
    admin:
      addresses: http://127.0.0.1:6080/xxl-job-admin
    executor:
      appname: xxl-job-executor-center  # 执行器AppName [选填]：执行器心跳注册分组依据；为空则关闭自动注册
      address:                          # 执行器注册 [选填]：优先使用该配置作为注册地址，为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
      ip:                               # 执行器IP [选填]：默认为空表示自动获取IP，多网卡时可手动设置指定IP，该IP不会绑定Host仅作为通讯实用；地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"；
      port: 6081                        # 执行器端口号 [选填]：小于等于0则自动获取；默认端口为9999，单机部署多个执行器时，注意要配置不同执行器端口；
      logpath:                          # 执行器运行日志文件存储磁盘路径 [选填] ：需要对该路径拥有读写权限；为空则使用默认路径；
      logretentiondays: 7               # 执行器日志文件保存天数 [选填] ： 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能；</code></pre>
<ul>
 <li>
  <p style="">防火墙需开放端口</p></li>
</ul>
<p style="">执行器支持集群部署，提升调度系统可用性，同时提升任务处理能力。</p>
<p style="">执行器集群部署时，几点要求和建议：</p>
<ul>
 <li>
  <p style="">执行器回调地址（xxl.job.admin.addresses）需要保持一致；执行器根据该配置进行执行器自动注册等操作。</p></li>
 <li>
  <p style="">同一个执行器集群内AppName（xxl.job.executor.appname）需要保持一致；调度中心根据该配置动态发现不同集群的在线执行器列表。</p></li>
</ul>
<h3 style="" id="4%E3%80%81%E7%99%BB%E9%99%86">4、登陆</h3>
<p style="">管理页面地址：<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/xuxueli/xxl-job">http://127.0.0.1:6080/xxl-job-admin/</a></p>
<p style="">账号：admin</p>
<p style="">密码：123456</p>]]></description><guid isPermaLink="false">/archives/1702299014356</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fxxl-job.png&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:50:00 GMT</pubDate></item><item><title><![CDATA[docker 环境搭建Maven私服]]></title><link>https://xiaoming728.com/archives/1702298967825</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=docker%20%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BAMaven%E7%A7%81%E6%9C%8D&amp;url=/archives/1702298967825" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">1.&nbsp;下载一个nexus3的镜像</span></p>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">docker pull sonatype/nexus3</span></p>
</blockquote>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">2.&nbsp;将容器内部/var/nexus-data挂载到主机/root/nexus-data目录。</span></p>
<pre><code>docker run -d -p 8081:8081 \
--name nexus \
-v /opt/soft/nexus-data:/var/nexus-data \
--restart=always \
sonatype/nexus3</code></pre>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">3.关闭防火墙&nbsp; &nbsp;&nbsp;</span></p>
<blockquote>
 <p style=""><strong><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">systemctl</span></strong><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">&nbsp;</span><strong><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">stop</span></strong><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">&nbsp;</span><strong><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">firewalld</span></strong><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">.service</span></p>
</blockquote>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">4.访问</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://ip:8081"><u><span style="font-size: 16px; color: rgb(0, 0, 0)">http://ip:8081</span></u></a><span style="font-size: 16px; color: rgb(0, 0, 0)">&nbsp;&nbsp;</span></p>
<p style=""></p>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">Maven私服启动容器稍微比较慢，等待1分钟即可。</span></p>
 <p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">默认登陆账号&nbsp;admin admin123</span></p>
</blockquote>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">5.登录maven私服时 出现该报错 ：&nbsp;</span><code>Incorrect username or password, or no permission to use the application.</code></p>
<p style=""></p>
<p style=""><code>解决办法</code><span style="font-size: 14px; color: rgb(0, 0, 0)">：</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://ip:8081"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">https://www.cnblogs.com/niceyoo/p/11204143.html</span></a></p>]]></description><guid isPermaLink="false">/archives/1702298967825</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmaven.png&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:50:00 GMT</pubDate></item><item><title><![CDATA[Linux连接数优化]]></title><link>https://xiaoming728.com/archives/1702299050218</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Linux%E8%BF%9E%E6%8E%A5%E6%95%B0%E4%BC%98%E5%8C%96&amp;url=/archives/1702299050218" width="1" height="1" alt="" style="opacity:0;">
<p style="">修改<code>/etc/sysctl.conf</code><span style="font-size: 15px; color: rgb(64, 64, 64)">文件</span></p>
<pre><code>#timewait 的数量，默认是 180000。
net.ipv4.tcp_max_tw_buckets = 6000

net.ipv4.ip_local_port_range = 1024 65000

#启用 timewait 快速回收。
net.ipv4.tcp_tw_recycle = 1

#开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接。
net.ipv4.tcp_tw_reuse = 1</code></pre>
<p style="">使命令生效<code>/sbin/sysctl -p</code></p>]]></description><guid isPermaLink="false">/archives/1702299050218</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FLinux-scaled.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Linux技术</category><pubDate>Mon, 11 Dec 2023 12:50:00 GMT</pubDate></item><item><title><![CDATA[dcoker 环境搭建Gitlab-ce]]></title><link>https://xiaoming728.com/archives/1702298929609</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=dcoker%20%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BAGitlab-ce&amp;url=/archives/1702298929609" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><strong>建议虚拟机内存2G以上</strong></p>
</blockquote>
<p style=""><strong>1.下载镜像文件</strong></p>
<blockquote>
 <p style=""><span style="font-size: 13px">docker pull beginor/gitlab-ce:11.0.1-ce.0</span></p>
</blockquote>
<p style=""><strong>注意：一定要配置阿里云的加速镜像</strong></p>
<p style=""><span style="font-size: 13px">1.&nbsp;</span><strong>创建GitLab 的配置 (etc) 、 日志 (log) 、数据 (data) 放到容器之外， 便于日后升级， 因此请先准备这三个目录。</strong></p>
<pre><code>mkdir -p /mnt/gitlab/etc
mkdir -p /mnt/gitlab/log
mkdir -p /mnt/gitlab/data</code></pre>
<p style=""><span style="font-size: 13px">2.&nbsp;</span><strong>运行GitLab容器</strong></p>
<p style=""><strong>&nbsp;</strong></p>
<pre><code>docker run \
    --detach \
    --publish 8443:443 \
    --publish 8090:80 \
    --name gitlab \
    --restart unless-stopped \
    -v /mnt/gitlab/etc:/etc/gitlab \
    -v /mnt/gitlab/log:/var/log/gitlab \
    -v /mnt/gitlab/data:/var/opt/gitlab \
    beginor/gitlab-ce:11.0.1-ce.0 </code></pre>
<p style=""><span style="font-size: 16px">&nbsp;</span></p>
<p style=""><span style="font-size: 16px">停止docker容器，并且删除</span></p>
<blockquote>
 <p style="">docker stop 容器id</p>
 <p style="">docker rm 容器id</p>
 <p style="">systemctl stop firewalld</p>
</blockquote>
<p style=""></p>
<p style=""><span style="font-size: 16px">4.修改/mnt/gitlab/etc/gitlab.rb</span></p>
<p style=""><strong>把external_url改成部署机器的域名或者IP地址</strong></p>
<blockquote>
 <p style=""><strong>vi /mnt/gitlab/etc/gitlab.rb</strong></p>
 <p style=""><strong>external_url '</strong><a target="_blank" rel="noopener noreferrer nofollow" href="http://192.168.212.227"><strong>http://192.168.212.227</strong></a><strong>'</strong></p>
</blockquote>
<p style=""></p>
<p style=""><span style="font-size: 13px">3.&nbsp;</span><strong>修改/mnt/gitlab/data/gitlab-rails/etc/gitlab.yml</strong></p>
<blockquote>
 <p style=""><strong>vi /mnt/gitlab/data/gitlab-rails/etc/gitlab.yml</strong></p>
 <p style=""><strong>找到关键字 <em> ## Web server settings </em></strong></p>
</blockquote>
<p style=""><strong>将host的值改成映射的外部主机ip地址和端口，这里会显示在gitlab克隆地址</strong></p>
<p style=""><strong>&nbsp;</strong><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-goxl.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style=""><span style="font-size: 14px; color: rgb(0, 0, 0)">到此为止，gitlab的web管理页面就可以正常访问</span></p>]]></description><guid isPermaLink="false">/archives/1702298929609</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fopen-graph-gitlab.png&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><category>Git技术</category><pubDate>Mon, 11 Dec 2023 12:49:00 GMT</pubDate></item><item><title><![CDATA[Docker搭建Kafka]]></title><link>https://xiaoming728.com/archives/1702298856189</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E6%90%AD%E5%BB%BAKafka&amp;url=/archives/1702298856189" width="1" height="1" alt="" style="opacity:0;">
<p style=""><strong><span style="font-size: 22px">一、kafka简单介绍</span></strong></p>
<p style=""></p>
<p style=""><strong><span style="font-size: 16px">1、kafka简介</span></strong></p>
<p style="">Kafka是最初由Linkedin公司开发，是一个分布式、支持分区的（partition）、多副本的（replica），基于zookeeper协调的分布式消息系统，它的最大的特性就是可以实时的处理大量数据以满足各种需求场景：比如基于hadoop的批处理系统、低延迟的实时系统、storm/Spark流式处理引擎，web/nginx日志、访问日志，消息服务等等，用scala语言编写，Linkedin于2010年贡献给了Apache基金会并成为顶级开源 项目。</p>
<p style=""></p>
<p style=""><strong><span style="font-size: 16px">2、kafka特性</span></strong></p>
<ul>
 <li>
  <p style="">高吞吐量、低延迟：kafka每秒可以处理几十万条消息，它的延迟最低只有几毫秒，每个topic可以分多个partition, consumer group 对partition进行consume操作。</p></li>
 <li>
  <p style="">可扩展性：kafka集群支持热扩展</p></li>
 <li>
  <p style="">持久性、可靠性：消息被持久化到本地磁盘，并且支持数据备份防止数据丢失</p></li>
 <li>
  <p style="">容错性：允许集群中节点失败（若副本数量为n,则允许n-1个节点失败）</p></li>
 <li>
  <p style="">高并发：支持数千个客户端同时读写</p></li>
</ul>
<p style=""></p>
<p style=""><strong><span style="font-size: 16px">3、kafka使用场景</span></strong></p>
<ul>
 <li>
  <p style="">日志收集：一个公司可以用Kafka可以收集各种服务的log，通过kafka以统一接口服务的方式开放给各种consumer，例如hadoop、Hbase、Solr等。</p></li>
 <li>
  <p style="">消息系统：解耦和生产者和消费者、缓存消息等。</p></li>
 <li>
  <p style="">用户活动跟踪：Kafka经常被用来记录web用户或者app用户的各种活动，如浏览网页、搜索、点击等活动，这些活动信息被各个服务器发布到kafka的topic中，然后订阅者通过订阅这些topic来做实时的监控分析，或者装载到hadoop、数据仓库中做离线分析和挖掘。</p></li>
 <li>
  <p style="">运营指标：Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据，生产各种操作的集中反馈，比如报警和报告。</p></li>
 <li>
  <p style="">流式处理：比如spark streaming和storm</p></li>
 <li>
  <p style="">事件源</p></li>
</ul>
<p style=""></p>
<p style=""><strong><span style="font-size: 22px">二、docker里kafka的搭建</span></strong></p>
<p style=""></p>
<p style=""><strong><span style="font-size: 16px">1、拉取镜像文件</span></strong></p>
<p style="">首先去docker镜像仓库拉取kafka镜像，仓库地址：<a target="_blank" rel="noopener noreferrer nofollow" href="https://hub.docker.com">https://hub.docker.com</a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-acfh.png&amp;size=m" style="display: inline-block"></p>
<p style="">拉取kafka镜像命令</p>
<p style=""></p>
<pre><code>docker pull wurstmeister/kafka</code></pre>
<p style=""></p>
<p style=""><strong><span style="font-size: 16px">2、挂载宿主机的配置文件</span></strong></p>
<p style="">kafka镜像内配置文件路径：/opt/kafka/config</p>
<p style="">宿主机配置文件路径：/docker/kafka/config</p>
<p style=""><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">注：配置文件，你可以选择拷贝容器里的配置文件出来到宿主机，后续定制化修改，也可以直接从官网下载kafka安装包，解压后有默认配置文件。</span></p>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">启动kafka命令</span></p>
<p style=""></p>
<pre><code>docker run -d --restart=always --name kafka -p 9092:9092 \
--link zookeeper \
--env KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
--env KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
--env KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.8.201:9092 \
-v /etc/localtime:/etc/localtime \
-v /home/docker/kafka/data:/kafka \
-v /home/docker/kafka/logs:/opt/kafka/logs \
wurstmeister/kafka</code></pre>
<p style="">KAFKA_ZOOKEEPER_CONNECT&nbsp; &nbsp;&nbsp;--配置zk注册中心</p>
<p style="">KAFKA_LISTENERS&nbsp; &nbsp;&nbsp;--需要开启外部访问，LISTENERS直接配置0.0.0.0:9092</p>
<p style="">KAFKA_ADVERTISED_LISTENERS&nbsp; --为宿主机器的IP地址，如果不这么设置，可能会导致在别的机器上访问不到</p>
<p style="">同时可以选择挂载宿主机的配置文件和数据存储目录</p>
<p style=""></p>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://hub.docker.com">查看配置文件server.properties</a>：</p>
<p style=""></p>
<pre><code># Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# see kafka.server.KafkaConfig for additional details and defaults

############################# Server Basics #############################
# The id of the broker. This must be set to a unique integer for each broker.
broker.id=-1

############################# Socket Server Settings #############################
# The address the socket server listens on. It will get the value returned from 
# java.net.InetAddress.getCanonicalHostName() if not configured.
#   FORMAT:
#     listeners = listener_name://host_name:port
#   EXAMPLE:
#     listeners = PLAINTEXT://your.host.name:9092
listeners=PLAINTEXT://0.0.0.0:9092

# Hostname and port the broker will advertise to producers and consumers. If not set, 
# it uses the value for "listeners" if configured.  Otherwise, it will use the value
# returned from java.net.InetAddress.getCanonicalHostName().
advertised.listeners=PLAINTEXT://192.168.8.201:9092

# Maps listener names to security protocols, the default is for them to be the same. See the config documentation for more details
#listener.security.protocol.map=PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL

# The number of threads that the server uses for receiving requests from the network and sending responses to the network
num.network.threads=3

# The number of threads that the server uses for processing requests, which may include disk I/O
num.io.threads=8

# The send buffer (SO_SNDBUF) used by the socket server
socket.send.buffer.bytes=102400

# The receive buffer (SO_RCVBUF) used by the socket server
socket.receive.buffer.bytes=102400

# The maximum size of a request that the socket server will accept (protection against OOM)
socket.request.max.bytes=104857600


############################# Log Basics #############################
# A comma separated list of directories under which to store log files
log.dirs=/kafka/kafka-logs-2a55893bad6f

# The default number of log partitions per topic. More partitions allow greater
# parallelism for consumption, but this will also result in more files across
# the brokers.
num.partitions=1

# The number of threads per data directory to be used for log recovery at startup and flushing at shutdown.
# This value is recommended to be increased for installations with data dirs located in RAID array.
num.recovery.threads.per.data.dir=1

############################# Internal Topic Settings  #############################
# The replication factor for the group metadata internal topics "__consumer_offsets" and "__transaction_state"
# For anything other than development testing, a value greater than 1 is recommended for to ensure availability such as 3.
offsets.topic.replication.factor=1
transaction.state.log.replication.factor=1
transaction.state.log.min.isr=1

############################# Log Flush Policy #############################
# Messages are immediately written to the filesystem but by default we only fsync() to sync
# the OS cache lazily. The following configurations control the flush of data to disk.
# There are a few important trade-offs here:
#    1. Durability: Unflushed data may be lost if you are not using replication.
#    2. Latency: Very large flush intervals may lead to latency spikes when the flush does occur as there will be a lot of data to flush.
#    3. Throughput: The flush is generally the most expensive operation, and a small flush interval may lead to excessive seeks.
# The settings below allow one to configure the flush policy to flush data after a period of time or
# every N messages (or both). This can be done globally and overridden on a per-topic basis.

# The number of messages to accept before forcing a flush of data to disk
#log.flush.interval.messages=10000

# The maximum amount of time a message can sit in a log before we force a flush
#log.flush.interval.ms=1000

############################# Log Retention Policy #############################
# The following configurations control the disposal of log segments. The policy can
# be set to delete segments after a period of time, or after a given size has accumulated.
# A segment will be deleted whenever *either* of these criteria are met. Deletion always happens
# from the end of the log.

# The minimum age of a log file to be eligible for deletion due to age
log.retention.hours=168

# A size-based retention policy for logs. Segments are pruned from the log unless the remaining
# segments drop below log.retention.bytes. Functions independently of log.retention.hours.
#log.retention.bytes=1073741824

# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824

# The interval at which log segments are checked to see if they can be deleted according
# to the retention policies
log.retention.check.interval.ms=300000

############################# Zookeeper #############################
# Zookeeper connection string (see zookeeper docs for details).
# This is a comma separated host:port pairs, each corresponding to a zk
# server. e.g. "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002".
# You can also append an optional chroot string to the urls to specify the
# root directory for all kafka znodes.
zookeeper.connect=zookeeper:2181

# Timeout in ms for connecting to zookeeper
zookeeper.connection.timeout.ms=6000

# The following configuration specifies the time, in milliseconds, that the GroupCoordinator will delay the initial consumer rebalance.
# The rebalance will be further delayed by the value of group.initial.rebalance.delay.ms as new members join the group, up to a maximum of max.poll.interval.ms.
# The default value for this is 3 seconds.
# We override this to 0 here as it makes for a better out-of-the-box experience for development and testing.
# However, in production environments the default value of 3 seconds is more suitable as this will help to avoid unnecessary, and potentially expensive, rebalances during application startup.
group.initial.rebalance.delay.ms=0
port=9092</code></pre>
<p style=""></p>
<p style="">可以用Kafka Tools来查看消息队列情况</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ogpu.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style=""><strong>查看所有topic列表</strong></p>
<pre><code>bin/kafka-topics.sh --zookeeper zookeeper:2181 --list</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298856189</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fkafka.png&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:48:00 GMT</pubDate></item><item><title><![CDATA[Docker搭建Mycat指南]]></title><link>https://xiaoming728.com/archives/1702298810003</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E6%90%AD%E5%BB%BAMycat%E6%8C%87%E5%8D%97&amp;url=/archives/1702298810003" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="%E4%B8%80%E3%80%81mycat%E7%AE%80%E4%BB%8B">一、Mycat简介</h1>
<h2 style="" id="1%E3%80%81%E4%BB%80%E4%B9%88%E6%98%AFmycat%EF%BC%9F">1、什么是Mycat？</h2>
<ul>
 <li>
  <p style="">一个彻底开源的，面向企业应用开发的大数据库集群</p></li>
 <li>
  <p style="">支持事务、ACID、可以替代MySQL的加强版数据库</p></li>
 <li>
  <p style="">一个可以视为MySQL集群的企业级数据库，用来替代昂贵的Oracle集群</p></li>
 <li>
  <p style="">一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server</p></li>
 <li>
  <p style="">结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品</p></li>
 <li>
  <p style="">一个新颖的数据库中间件产品</p></li>
</ul>
<p style=""></p>
<p style=""></p>
<h2 style="" id="2%E3%80%81mycat%E7%9A%84%E5%85%B3%E9%94%AE%E7%89%B9%E6%80%A7">2、Mycat的关键特性</h2>
<ul>
 <li>
  <p style="">支持MySQL、Oracle、DB2、SQL Server、PostgreSQL等DB的常见SQL语法</p></li>
 <li>
  <p style="">遵守Mysql原生协议，跨语言，跨平台，跨数据库的通用中间件代理。</p></li>
 <li>
  <p style="">基于心跳的自动故障切换，支持读写分离，支持MySQL主从，以及galera cluster集群。</p></li>
 <li>
  <p style="">支持Galera for MySQL集群，Percona Cluster或者MariaDB cluster</p></li>
 <li>
  <p style="">基于Nio实现，有效管理线程，解决高并发问题。</p></li>
 <li>
  <p style="">支持数据的多片自动路由与聚合，支持sum,count,max等常用的聚合函数,支持跨库分页。</p></li>
 <li>
  <p style="">支持单库内部任意join，支持跨库2表join，甚至基于caltlet的多表join。</p></li>
 <li>
  <p style="">支持通过全局表，ER关系的分片策略，实现了高效的多表join查询。</p></li>
 <li>
  <p style="">支持多租户方案。</p></li>
 <li>
  <p style="">支持分布式事务（弱xa）。</p></li>
 <li>
  <p style="">支持XA分布式事务（1.6.5）。</p></li>
 <li>
  <p style="">支持全局序列号，解决分布式下的主键生成问题。</p></li>
 <li>
  <p style="">分片规则丰富，插件化开发，易于扩展。</p></li>
 <li>
  <p style="">强大的web，命令行监控。</p></li>
 <li>
  <p style="">支持前端作为MySQL通用代理，后端JDBC方式支持Oracle、DB2、SQL Server 、 mongodb 、巨杉。</p></li>
 <li>
  <p style="">支持密码加密</p></li>
 <li>
  <p style="">支持服务降级</p></li>
 <li>
  <p style="">支持IP白名单</p></li>
 <li>
  <p style="">支持SQL黑名单、sql注入攻击拦截</p></li>
 <li>
  <p style="">支持prepare预编译指令（1.6）</p></li>
 <li>
  <p style="">支持非堆内存(Direct Memory)聚合计算（1.6）</p></li>
 <li>
  <p style="">支持PostgreSQL的native协议（1.6）</p></li>
 <li>
  <p style="">支持mysql和oracle存储过程，out参数、多结果集返回（1.6）</p></li>
 <li>
  <p style="">支持zookeeper协调主从切换、zk序列、配置zk化（1.6）</p></li>
 <li>
  <p style="">支持库内分表（1.6）</p></li>
 <li>
  <p style="">集群基于ZooKeeper管理，在线升级，扩容，智能优化，大数据处理（2.0开发版）。</p></li>
</ul>
<p style=""></p>
<h1 style="" id="%E4%BA%8C%E3%80%81%E4%BD%BF%E7%94%A8docker%E6%90%AD%E5%BB%BAmycat">二、使用Docker搭建Mycat</h1>
<p style=""></p>
<h2 style="" id="1%E3%80%81%E6%8B%89%E5%8F%96mycat%E9%95%9C%E5%83%8F">1、拉取Mycat镜像</h2>
<p style="">到 Docker Hub(<a target="_blank" rel="noopener noreferrer nofollow" href="https://link.hacpai.com/forward?goto=https%3A%2F%2Fhub.docker.com%2F">https://hub.docker.com/)镜像仓库中拉取别人已经制作好的</a> Mycat 镜像。如下图</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-jmnk.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<p style="">docker拉取镜像命令:</p>
<p style=""></p>
<pre><code>$ sudo docker pull fjy8018/mycat</code></pre>
<p style=""></p>
<h2 style="" id="2%E3%80%81%E6%8B%B7%E8%B4%9Dmycat%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">2、拷贝Mycat的配置文件</h2>
<p style="">为了方便对Mycat进行配置，需要将mycat中配置文件目录挂载到宿主机对应目录下。因为配置文件较多，可以先将容器中的文件拷贝出来进行修改。</p>
<ul>
 <li>
  <p style="">mycat容器中的配置目录：/usr/local/mycat/conf</p></li>
 <li>
  <p style="">宿主机mycat配置目录：/home/docker/mycat/conf</p></li>
</ul>
<p style=""></p>
<p style=""><strong>运行容器</strong>（运行容器时将<code>-d</code>和<code>--rm</code>两个选项一起使用，那么容器会在退出或者后台进程停止的的时候自动移除掉）</p>
<pre><code>$ docker run --rm -d --name=mycat fjy8018/mycat</code></pre>
<p style=""></p>
<p style=""><strong>拷贝配置文件</strong></p>
<pre><code>$ docker cp mycat:/usr/local/mycat/conf /home/docker/mycat/conf</code></pre>
<p style=""></p>
<p style="">停止临时容器</p>
<pre><code>$ docker stop mycat</code></pre>
<p style=""></p>
<h2 style="" id="3%E3%80%81%E4%BF%AE%E6%94%B9mycat%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">3、修改Mycat配置文件</h2>
<p style=""></p>
<h3 style="" id="1)-schema.xml-%3A-%E6%98%AF%E9%80%BB%E8%BE%91%E5%BA%93%E5%AE%9A%E4%B9%89%E5%92%8C%E8%A1%A8%E4%BB%A5%E5%8F%8A%E5%88%86%E7%89%87%E5%AE%9A%E4%B9%89%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">1) schema.xml : 是逻辑库定义和表以及分片定义的配置文件</h3>
<p style=""></p>
<pre><code>&lt;?xml version="1.0"?&gt;
&lt;!DOCTYPE mycat:schema SYSTEM "schema.dtd"&gt;
&lt;mycat:schema xmlns:mycat="http://io.mycat/"&gt;
	&lt;!-- name: 逻辑数据库名 --&gt; 
	&lt;schema name="uums" checkSQLschema="true" sqlMaxLimit="100"&gt;
		&lt;table name="uums_user" primaryKey="user_id" autoIncrement="false" dataNode="dn1" /&gt;
	&lt;/schema&gt;
	&lt;!-- database:物理数据库名 --&gt;
	&lt;dataNode name="dn1" dataHost="localhost1" database="uums" /&gt;

	&lt;dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100"&gt;
		&lt;heartbeat&gt;select user()&lt;/heartbeat&gt;
		&lt;writeHost host="hostM1" url="192.168.8.201:3306" user="xxx" password="xxx"&gt;
                  &lt;readHost host="hostS1" url="192.168.8.201:3306" user="xxx" password="xxx" /&gt;
        &lt;/writeHost&gt;
	&lt;/dataHost&gt;

&lt;/mycat:schema&gt;
</code></pre>
<p style=""></p>
<h3 style="" id="2)-server.xml-%3A-%E6%98%AFmycat%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8F%82%E6%95%B0%E8%B0%83%E6%95%B4%E5%92%8C%E7%94%A8%E6%88%B7%E6%8E%88%E6%9D%83%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">2) server.xml : 是Mycat服务器参数调整和用户授权的配置文件</h3>
<p style=""></p>
<pre><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;!-- - - Licensed under the Apache License, Version 2.0 (the "License"); 
	- you may not use this file except in compliance with the License. - You 
	may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 
	- - Unless required by applicable law or agreed to in writing, software - 
	distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 
	WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the 
	License for the specific language governing permissions and - limitations 
	under the License. --&gt;
&lt;!DOCTYPE mycat:server SYSTEM "server.dtd"&gt;
&lt;mycat:server xmlns:mycat="http://io.mycat/"&gt;
	&lt;system&gt;
	&lt;property name="nonePasswordLogin"&gt;0&lt;/property&gt; &lt;!-- 0为需要密码登陆、1为不需要密码登陆 ,默认为0，设置为1则需要指定默认账户--&gt;
	&lt;property name="useHandshakeV10"&gt;1&lt;/property&gt;
	&lt;property name="useSqlStat"&gt;0&lt;/property&gt;  &lt;!-- 1为开启实时统计、0为关闭 --&gt;
	&lt;property name="useGlobleTableCheck"&gt;0&lt;/property&gt;  &lt;!-- 1为开启全加班一致性检测、0为关闭 --&gt;

	&lt;property name="sequnceHandlerType"&gt;2&lt;/property&gt;
	&lt;property name="subqueryRelationshipCheck"&gt;false&lt;/property&gt; &lt;!-- 子查询中存在关联查询的情况下,检查关联字段中是否有分片字段 .默认 false --&gt;
      &lt;!--  &lt;property name="useCompression"&gt;1&lt;/property&gt;--&gt; &lt;!--1为开启mysql压缩协议--&gt;
        &lt;!--  &lt;property name="fakeMySQLVersion"&gt;5.6.20&lt;/property&gt;--&gt; &lt;!--设置模拟的MySQL版本号--&gt;
	&lt;!-- &lt;property name="processorBufferChunk"&gt;40960&lt;/property&gt; --&gt;
	&lt;!-- 
	&lt;property name="processors"&gt;1&lt;/property&gt; 
	&lt;property name="processorExecutor"&gt;32&lt;/property&gt; 
	 --&gt;
        &lt;!--默认为type 0: DirectByteBufferPool | type 1 ByteBufferArena | type 2 NettyBufferPool --&gt;
		&lt;property name="processorBufferPoolType"&gt;0&lt;/property&gt;
		&lt;!--默认是65535 64K 用于sql解析时最大文本长度 --&gt;
		&lt;!--&lt;property name="maxStringLiteralLength"&gt;65535&lt;/property&gt;--&gt;
		&lt;!--&lt;property name="sequnceHandlerType"&gt;0&lt;/property&gt;--&gt;
		&lt;!--&lt;property name="backSocketNoDelay"&gt;1&lt;/property&gt;--&gt;
		&lt;!--&lt;property name="frontSocketNoDelay"&gt;1&lt;/property&gt;--&gt;
		&lt;!--&lt;property name="processorExecutor"&gt;16&lt;/property&gt;--&gt;
		&lt;!--
			&lt;property name="serverPort"&gt;8066&lt;/property&gt; &lt;property name="managerPort"&gt;9066&lt;/property&gt; 
			&lt;property name="idleTimeout"&gt;300000&lt;/property&gt; &lt;property name="bindIp"&gt;0.0.0.0&lt;/property&gt; 
			&lt;property name="frontWriteQueueSize"&gt;4096&lt;/property&gt; &lt;property name="processors"&gt;32&lt;/property&gt; --&gt;
		&lt;!--分布式事务开关，0为不过滤分布式事务，1为过滤分布式事务（如果分布式事务内只涉及全局表，则不过滤），2为不过滤分布式事务,但是记录分布式事务日志--&gt;
		&lt;property name="handleDistributedTransactions"&gt;0&lt;/property&gt;
		
			&lt;!--
			off heap for merge/order/group/limit      1开启   0关闭
		--&gt;
		&lt;property name="useOffHeapForMerge"&gt;1&lt;/property&gt;

		&lt;!--
			单位为m
		--&gt;
        &lt;property name="memoryPageSize"&gt;64k&lt;/property&gt;

		&lt;!--
			单位为k
		--&gt;
		&lt;property name="spillsFileBufferSize"&gt;1k&lt;/property&gt;

		&lt;property name="useStreamOutput"&gt;0&lt;/property&gt;

		&lt;!--
			单位为m
		--&gt;
		&lt;property name="systemReserveMemorySize"&gt;384m&lt;/property&gt;


		&lt;!--是否采用zookeeper协调切换  --&gt;
		&lt;property name="useZKSwitch"&gt;true&lt;/property&gt;

		&lt;!-- XA Recovery Log日志路径 --&gt;
		&lt;!--&lt;property name="XARecoveryLogBaseDir"&gt;./&lt;/property&gt;--&gt;

		&lt;!-- XA Recovery Log日志名称 --&gt;
		&lt;!--&lt;property name="XARecoveryLogBaseName"&gt;tmlog&lt;/property&gt;--&gt;
		&lt;!--如果为 true的话 严格遵守隔离级别,不会在仅仅只有select语句的时候在事务中切换连接--&gt;
		&lt;property name="strictTxIsolation"&gt;false&lt;/property&gt;
				
	&lt;/system&gt;

	&lt;!-- mycat的8066 端口连接的用户名，密码，逻辑数据库名 --&gt;
	&lt;user name="root"&gt;
	    &lt;property name="password"&gt;123456&lt;/property&gt;
	    &lt;property name="schemas"&gt;uums&lt;/property&gt;
	&lt;/user&gt;

	

&lt;/mycat:server&gt;
</code></pre>
<p style=""></p>
<h3 style="" id="3)-rule.xml-%3A-%E6%98%AF%E5%88%86%E7%89%87%E8%A7%84%E5%88%99%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%2C%E5%88%86%E7%89%87%E8%A7%84%E5%88%99%E7%9A%84%E5%85%B7%E4%BD%93%E4%B8%80%E4%BA%9B%E5%8F%82%E6%95%B0%E4%BF%A1%E6%81%AF%E5%8D%95%E7%8B%AC%E5%AD%98%E6%94%BE%E4%B8%BA%E6%96%87%E4%BB%B6">3) rule.xml : 是分片规则的配置文件,分片规则的具体一些参数信息单独存放为文件</h3>
<p style=""></p>
<pre><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;!-- - - Licensed under the Apache License, Version 2.0 (the "License"); 
	- you may not use this file except in compliance with the License. - You 
	may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 
	- - Unless required by applicable law or agreed to in writing, software - 
	distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 
	WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the 
	License for the specific language governing permissions and - limitations 
	under the License. --&gt;
&lt;!DOCTYPE mycat:rule SYSTEM "rule.dtd"&gt;
&lt;mycat:rule xmlns:mycat="http://io.mycat/"&gt;

	&lt;tableRule name="rule1"&gt;
		&lt;rule&gt;
			&lt;columns&gt;id&lt;/columns&gt;
			&lt;algorithm&gt;func1&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;

	&lt;tableRule name="rule2"&gt;
		&lt;rule&gt;
			&lt;columns&gt;user_id&lt;/columns&gt;
			&lt;algorithm&gt;func1&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;

	&lt;tableRule name="sharding-by-intfile"&gt;
		&lt;rule&gt;
			&lt;columns&gt;sharding_id&lt;/columns&gt;
			&lt;algorithm&gt;hash-int&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;
	&lt;tableRule name="auto-sharding-long"&gt;
		&lt;rule&gt;
			&lt;columns&gt;id&lt;/columns&gt;
			&lt;algorithm&gt;rang-long&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;
	&lt;tableRule name="mod-long"&gt;
		&lt;rule&gt;
			&lt;columns&gt;id&lt;/columns&gt;
			&lt;algorithm&gt;mod-long&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;
	&lt;tableRule name="sharding-by-murmur"&gt;
		&lt;rule&gt;
			&lt;columns&gt;id&lt;/columns&gt;
			&lt;algorithm&gt;murmur&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;
	&lt;tableRule name="crc32slot"&gt;
		&lt;rule&gt;
			&lt;columns&gt;id&lt;/columns&gt;
			&lt;algorithm&gt;crc32slot&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;
	&lt;tableRule name="sharding-by-month"&gt;
		&lt;rule&gt;
			&lt;columns&gt;create_time&lt;/columns&gt;
			&lt;algorithm&gt;partbymonth&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;
	&lt;tableRule name="latest-month-calldate"&gt;
		&lt;rule&gt;
			&lt;columns&gt;calldate&lt;/columns&gt;
			&lt;algorithm&gt;latestMonth&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;
	
	&lt;tableRule name="auto-sharding-rang-mod"&gt;
		&lt;rule&gt;
			&lt;columns&gt;id&lt;/columns&gt;
			&lt;algorithm&gt;rang-mod&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;
	
	&lt;tableRule name="jch"&gt;
		&lt;rule&gt;
			&lt;columns&gt;id&lt;/columns&gt;
			&lt;algorithm&gt;jump-consistent-hash&lt;/algorithm&gt;
		&lt;/rule&gt;
	&lt;/tableRule&gt;
	
	&lt;function name="murmur"
		class="io.mycat.route.function.PartitionByMurmurHash"&gt;
		&lt;property name="seed"&gt;0&lt;/property&gt;&lt;!-- 默认是0 --&gt;
		&lt;property name="count"&gt;2&lt;/property&gt;&lt;!-- 要分片的数据库节点数量，必须指定，否则没法分片 --&gt;
		&lt;property name="virtualBucketTimes"&gt;160&lt;/property&gt;&lt;!-- 一个实际的数据库节点被映射为这么多虚拟节点，默认是160倍，也就是虚拟节点数是物理节点数的160倍 --&gt;
		&lt;!-- &lt;property name="weightMapFile"&gt;weightMapFile&lt;/property&gt; 节点的权重，没有指定权重的节点默认是1。以properties文件的格式填写，以从0开始到count-1的整数值也就是节点索引为key，以节点权重值为值。所有权重值必须是正整数，否则以1代替 --&gt;
		&lt;!-- &lt;property name="bucketMapPath"&gt;/etc/mycat/bucketMapPath&lt;/property&gt; 
			用于测试时观察各物理节点与虚拟节点的分布情况，如果指定了这个属性，会把虚拟节点的murmur hash值与物理节点的映射按行输出到这个文件，没有默认值，如果不指定，就不会输出任何东西 --&gt;
	&lt;/function&gt;

	&lt;function name="crc32slot"
			  class="io.mycat.route.function.PartitionByCRC32PreSlot"&gt;
	&lt;/function&gt;
	&lt;function name="hash-int"
		class="io.mycat.route.function.PartitionByFileMap"&gt;
		&lt;property name="mapFile"&gt;partition-hash-int.txt&lt;/property&gt;
	&lt;/function&gt;
	&lt;function name="rang-long"
		class="io.mycat.route.function.AutoPartitionByLong"&gt;
		&lt;property name="mapFile"&gt;autopartition-long.txt&lt;/property&gt;
	&lt;/function&gt;
	&lt;function name="mod-long" class="io.mycat.route.function.PartitionByMod"&gt;
		&lt;!-- how many data nodes --&gt;
		&lt;property name="count"&gt;3&lt;/property&gt;
	&lt;/function&gt;

	&lt;function name="func1" class="io.mycat.route.function.PartitionByLong"&gt;
		&lt;property name="partitionCount"&gt;8&lt;/property&gt;
		&lt;property name="partitionLength"&gt;128&lt;/property&gt;
	&lt;/function&gt;
	&lt;function name="latestMonth"
		class="io.mycat.route.function.LatestMonthPartion"&gt;
		&lt;property name="splitOneDay"&gt;24&lt;/property&gt;
	&lt;/function&gt;
	&lt;function name="partbymonth"
		class="io.mycat.route.function.PartitionByMonth"&gt;
		&lt;property name="dateFormat"&gt;yyyy-MM-dd&lt;/property&gt;
		&lt;property name="sBeginDate"&gt;2015-01-01&lt;/property&gt;
	&lt;/function&gt;
	
	&lt;function name="rang-mod" class="io.mycat.route.function.PartitionByRangeMod"&gt;
        	&lt;property name="mapFile"&gt;partition-range-mod.txt&lt;/property&gt;
	&lt;/function&gt;
	
	&lt;function name="jump-consistent-hash" class="io.mycat.route.function.PartitionByJumpConsistentHash"&gt;
		&lt;property name="totalBuckets"&gt;3&lt;/property&gt;
	&lt;/function&gt;
&lt;/mycat:rule&gt;
</code></pre>
<p style=""></p>
<h2 style="" id="4%E3%80%81%E5%88%9B%E5%BB%BAmycat%E5%AE%B9%E5%99%A8%EF%BC%8C%E6%8C%82%E8%BD%BD%E7%9B%AE%E5%BD%95">4、创建mycat容器，挂载目录</h2>
<p style=""></p>
<pre><code>sudo docker run --restart=always --name=mycat -p 8066:8066 -p 9066:9066 \
-v /home/docker/mycat/conf:/usr/local/mycat/conf \
-v /home/docker/mycat/logs:/usr/local/mycat/logs \
-d fjy8018/mycat</code></pre>
<p style=""></p>
<ul>
 <li>
  <p style="">端口映射：8066、9066（目前 mycat 有两个端口,8066 数据端口,9066 管理端口）</p></li>
 <li>
  <p style="">目录挂载：/conf 配置文件目录，方便对配置进行修改；/logs日志目录，方便查看运行日志信息</p></li>
</ul>
<p style=""></p>
<h2 style="" id="5%E3%80%81%E8%BF%9E%E6%8E%A5mycat">5、连接mycat</h2>
<p style=""></p>
<p style="">通过8066端口连接即可。</p>
<p style=""></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ifet.png&amp;size=m" style="display: inline-block"></p>]]></description><guid isPermaLink="false">/archives/1702298810003</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><category>Mysql技术</category><pubDate>Mon, 11 Dec 2023 12:47:00 GMT</pubDate></item><item><title><![CDATA[Docker搭建MinIO]]></title><link>https://xiaoming728.com/archives/1702298787231</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E6%90%AD%E5%BB%BAMinIO&amp;url=/archives/1702298787231" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="%E4%B8%80%E3%80%81minio%E7%AE%80%E4%BB%8B">一、MinIO简介</h1>
<p style="">Minio是GlusterFS创始人之一Anand Babu Periasamy发布新的开源项目。Minio兼容Amason的S3分布式对象存储项目，采用Golang实现，客户端支持Java,Python,Javacript, Golang语言。</p>
<p style="">Minio可以做为云存储的解决方案用来保存海量的图片，视频，文档。由于采用Golang实现，服务端可以工作在Windows,Linux, OS X和FreeBSD上。配置简单，基本是复制可执行程序，单行命令可以运行起来。</p>
<p style=""></p>
<h1 style="" id="%E4%BA%8C%E3%80%81docker%E9%87%8Cminio%E7%9A%84%E6%90%AD%E5%BB%BA">二、docker里MinIO的搭建</h1>
<p style="">搜索MinIO镜像命令</p>
<pre><code>docker search minio</code></pre>
<p style="">拉取MinIO镜像命令</p>
<pre><code>docker pull minio/minio </code></pre>
<p style="">dokcer安装MinIO命令</p>
<pre><code>sudo  docker run -p 20090:9000 -p 9001:9001  \
	-d --restart=always  --user=root --privileged=true \
	-e "MINIO_ROOT_USER=echola" \
	-e 'MINIO_ROOT_PASSWORD=Echola12345!' \
	-v /opt/soft/docker/minio/data:/data \
	-v /opt/soft/docker/minio/config:/root/.minio \
	--name=minio  minio/minio server  /data --console-address ":9001" --address ":9000"</code></pre>
<p style="">9000端口已被使用，使用9090端口映射MinIO9000端口</p>]]></description><guid isPermaLink="false">/archives/1702298787231</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fminio.png&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:46:00 GMT</pubDate></item><item><title><![CDATA[Docker环境安装Mysql]]></title><link>https://xiaoming728.com/archives/1702298774737</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E7%8E%AF%E5%A2%83%E5%AE%89%E8%A3%85Mysql&amp;url=/archives/1702298774737" width="1" height="1" alt="" style="opacity:0;">
<h4 style="" id="1%E3%80%81%E6%90%9C%E7%B4%A2%E9%95%9C%E5%83%8F"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">1、搜索镜像</span></h4>
<pre><code>docker search mysql</code></pre>
<h4 style="" id="2%E3%80%81%E9%95%9C%E5%83%8F%E4%B8%8B%E8%BD%BD"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">2、镜像下载</span></h4>
<p style=""><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">下载第一个官方提的镜像</span></p>
<pre><code>docker pull mysql:8.0.40</code></pre>
<h4 style="" id="3%E3%80%81%E5%AE%89%E8%A3%85%E9%95%9C%E5%83%8F">3、安装镜像</h4>
<p style="">因为要Mysql生成的配置文件通过挂载映射出来，所以要先运行不挂载配置文件的命令</p>
<pre><code>docker run -d -p 3306:3306 \
-v /opt/soft/docker/mysql/data/:/var/lib/mysql/ \
-e MYSQL_ROOT_PASSWORD='admin@mysql!' \
-e TZ=Asia/Shanghai \
--restart=always \
--name mysql \
mysql:8.0.40 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci</code></pre>
<p style="">第二步：拷贝配置文件到主机目录</p>
<pre><code>docker cp mysql:/etc/mysql /opt/soft/docker/mysql/conf</code></pre>
<p style="">第三步：关闭删除当前容器（ps：相关配置文件已经在主机保存）</p>
<pre><code># 查看所有容器
docker ps -a
# 关闭容器
docker stop mysql / 容器ID
# 删除容器
docker rm mysql / 容器ID</code></pre>
<p style="">第四步：重启运行容器，并挂载配置文件</p>
<pre><code>docker run -d -p 3306:3306 \
-v /opt/soft/docker/mysql/conf/:/etc/mysql/ \
-v /opt/soft/docker/mysql/data/:/var/lib/mysql/ \
-e MYSQL_ROOT_PASSWORD='admin@mysql!' \
-e TZ=Asia/Shanghai \
--restart=always \
--name mysql \
mysql:8.0.40 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci</code></pre>
<blockquote>
 <p style=""><mark>注意：两次密码注意保持一致，否则可能出现密码错误，如果已经出现了就删掉</mark><code>/home/docker/mysql</code><mark>内所有文件重头来</mark></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298774737</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:46:00 GMT</pubDate></item><item><title><![CDATA[Docker环境安装zookeeper]]></title><link>https://xiaoming728.com/archives/1702298753938</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E7%8E%AF%E5%A2%83%E5%AE%89%E8%A3%85zookeeper&amp;url=/archives/1702298753938" width="1" height="1" alt="" style="opacity:0;">
<h4 style="" id="1%E3%80%81%E6%90%9C%E7%B4%A2%E9%95%9C%E5%83%8F"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">1、搜索镜像</span></h4>
<p style=""><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">docker search zookeeper</span></p>
<pre><code>[admin@localhost ~]$ sudo docker search zookeeper
NAME                               DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
zookeeper                          Apache ZooKeeper is an open-source server wh…   817                 [OK]
jplock/zookeeper                   Builds a docker image for Zookeeper version …   164                                     [OK]
wurstmeister/zookeeper                                                             110                                     [OK]
mesoscloud/zookeeper               ZooKeeper                                       73                                      [OK]
bitnami/zookeeper                  ZooKeeper is a centralized service for distr…   27                                      [OK]</code></pre>
<p style=""></p>
<h4 style="" id="2%E3%80%81%E9%95%9C%E5%83%8F%E4%B8%8B%E8%BD%BD"><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">2、镜像下载</span></h4>
<p style=""><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">下载第一个官方提的镜像</span></p>
<p style=""><span fontsize="" color="rgb(73, 73, 73)" style="color: rgb(73, 73, 73)">docker pull zookeeper</span></p>
<pre><code>[admin@localhost ~]# sudo docker pull zookeeper
Using default tag: latest
Trying to pull repository docker.io/library/zookeeper ...
latest: Pulling from docker.io/library/zookeeper
1ab2bdfe9778: Already exists
7aaf9a088d61: Pull complete
80a55c9c9fe8: Pull complete
a0086b0e6eec: Pull complete
4165e7457cad: Pull complete
bcba13bcf3a1: Pull complete
41c03a109e47: Pull complete
4d5281c6b0d4: Pull complete
Digest: sha256:175d6bb1471e1e37a48bfa41a9da047c80fade60fd585eae3a0e08a4ce1d39ed
Status: Downloaded newer image for docker.io/zookeeper:latest</code></pre>
<h4 style="" id="3%E3%80%81%E5%AE%89%E8%A3%85%E9%95%9C%E5%83%8F">3、安装镜像</h4>
<p style="">执行以下命令即可安装成功</p>
<pre><code>docker run -d -p 2181:2181 \
   -v /home/docker/zookeeper/data:/data \
   -v /home/docker/zookeeper/logs:/logs \
   -v /home/docker/zookeeper/datalog:/datalog \
   --name zookeeper \
   --restart=always \
   zookeeper:latest</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298753938</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fzookeeper.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:46:00 GMT</pubDate></item><item><title><![CDATA[Docker容器日志查看与配置]]></title><link>https://xiaoming728.com/archives/1702298719228</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%9F%A5%E7%9C%8B%E4%B8%8E%E9%85%8D%E7%BD%AE&amp;url=/archives/1702298719228" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">来源：CSDN-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/yjk13703623757">Locutus</a></p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/yjk13703623757/article/details/80283729">https://blog.csdn.net/yjk13703623757/article/details/80283729</a></p>
 <p style="">来源：CSDN-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/huangliuyu00">写代码的蓝胖子</a></p>
 <p style="">链接：https://blog.csdn.net/huangliuyu00/article/details/90384345</p>
</blockquote>
<p style=""></p>
<h2 style="" id="1%E3%80%81%E9%97%AE%E9%A2%98">1、问题</h2>
<p style="">docker容器日志导致主机磁盘空间满了。<code>docker logs -f container_name</code>噼里啪啦一大堆，很占用空间，不用的日志如何清理？</p>
<p style=""></p>
<h2 style="" id="2%E3%80%81%E6%9F%A5%E7%9C%8Bdocker%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97">2、查看Docker容器日志</h2>
<p style="">在linux上，容器日志一般存放在<code>/var/lib/docker/containers/container_id/</code>下面， 以json.log结尾的文件（业务日志）很大。</p>
<p style="">查看各个日志文件大小的脚本docker_log_size.sh，内容如下：</p>
<pre><code>#!/bin/sh

echo "======== docker containers logs file size ========"  

logs=$(find /var/lib/docker/containers/ -name *-json.log)  

for log in $logs  
        do  
             ls -lh $log   
        done  </code></pre>
<pre><code># 设置脚本可执行权限
chmod +x docker_log_size.sh
# 启动脚本
./docker_log_size.sh</code></pre>
<p style="">查询结果显示：</p>
<pre><code>======== docker containers logs file size ========
-rw-r-----. 1 root root 33K 4月  14 10:34 /var/lib/docker/containers/d57a8ec2ef7a0b7ffffe316bb00041404308358fa5244b95dd0d599a13f65245/d57a8ec2ef7a0b7ffffe316bb00041404308358fa5244b95dd0d599a13f65245-json.log
-rw-r-----. 1 root root 28K 1月  28 20:08 /var/lib/docker/containers/ad2e10a57aba2c1c9ff3cec6581db03b4b70e196648a4e8ecba66d75927fa44f/ad2e10a57aba2c1c9ff3cec6581db03b4b70e196648a4e8ecba66d75927fa44f-json.log
-rw-r-----. 1 root root 645 1月  18 16:31 /var/lib/docker/containers/212ef1629aeac225f7de56e1989912a352a3f92cd50d08c5fba388adb57ffde5/212ef1629aeac225f7de56e1989912a352a3f92cd50d08c5fba388adb57ffde5-json.log
-rw-r-----. 1 root root 374M 4月  14 11:03 /var/lib/docker/containers/a8831197437bddc86b807753e146d55070dc036125aa3d34fc67822140f8304d/a8831197437bddc86b807753e146d55070dc036125aa3d34fc67822140f8304d-json.log
-rw-r-----. 1 root root 15G 4月  14 10:41 /var/lib/docker/containers/c9721b7cf6e956e6250d43a93cdc9a7b421bb9338d35a5a5d021855004abb364/c9721b7cf6e956e6250d43a93cdc9a7b421bb9338d35a5a5d021855004abb364-json.log
-rw-r-----. 1 root root 3.2K 2月  25 11:41 /var/lib/docker/containers/d27972bdc595f1b87f2f49e5ee10fff2b3419b76e6d5103a0c08e11433079a71/d27972bdc595f1b87f2f49e5ee10fff2b3419b76e6d5103a0c08e11433079a71-json.log
-rw-r--r--. 1 root root 1.5G 4月  14 10:44 /var/lib/docker/containers/4e23e075d62ff9217d7bddd3dba04c9cb4477728860e73906d3b2af871a77372/4e23e075d62ff9217d7bddd3dba04c9cb4477728860e73906d3b2af871a77372-json.log
-rw-r-----. 1 root root 3.1G 4月  14 11:02 /var/lib/docker/containers/0bc74214f5eacf999f10caa43405701b8381497e01197e9d4f27e1a021d8aa79/0bc74214f5eacf999f10caa43405701b8381497e01197e9d4f27e1a021d8aa79-json.log
-rw-r-----. 1 root root 12G 4月  14 11:03 /var/lib/docker/containers/e1691d4090e7a526f0f792d0b91f5814145393954dada013c76f8b5d77401a89/e1691d4090e7a526f0f792d0b91f5814145393954dada013c76f8b5d77401a89-json.log
-rw-r-----. 1 root root 54K 3月  10 09:41 /var/lib/docker/containers/61fd9b363b71fd0113769f292e5f393d325a230e6a030775a5ce88c670897f1c/61fd9b363b71fd0113769f292e5f393d325a230e6a030775a5ce88c670897f1c-json.log
-rw-r-----. 1 root root 22M 4月  14 11:03 /var/lib/docker/containers/b85667dfa132e108c5c21f35c130e8bce60faedcec5377cabcd49a34e82fe180/b85667dfa132e108c5c21f35c130e8bce60faedcec5377cabcd49a34e82fe180-json.log
-rw-r-----. 1 root root 18M 4月  14 11:02 /var/lib/docker/containers/9185d37555557b1213dc57e83e491985772785f1b8b4dbaa93837bffb7f4d2f3/9185d37555557b1213dc57e83e491985772785f1b8b4dbaa93837bffb7f4d2f3-json.log</code></pre>
<h2 style="" id="3%E3%80%81%E6%B8%85%E7%90%86docker%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%EF%BC%88%E6%B2%BB%E6%A0%87%EF%BC%89">3、清理Docker容器日志（治标）</h2>
<p style="">如果docker容器正在运行，那么使用<code>rm -rf</code>方式删除日志后，通过<code>df -h</code>会发现磁盘空间并没有释放。原因是在Linux或者Unix系统中，通过<code>rm -rf</code>或者文件管理器删除文件，将会从文件系统的目录结构上解除链接（unlink）。如果文件是被打开的（有一个进程正在使用），那么进程将仍然可以读取该文件，磁盘空间也一直被占用。</p>
<p style="">正确姿势是<code>cat /dev/null &gt; *-json.log</code>，当然你也可以通过<code>rm -rf</code>删除后重启docker。</p>
<p style="">提供一个日志清理脚本<code>clean_docker_log.sh</code>，内容如下：</p>
<pre><code>#!/bin/sh 
  
echo "======== start clean docker containers logs ========"  
  
logs=$(find /var/lib/docker/containers/ -name *-json.log)  
  
for log in $logs  
        do  
                echo "clean logs : $log"  
                cat /dev/null &gt; $log  
        done  

echo "======== end clean docker containers logs ========"  
</code></pre>
<pre><code># 设置脚本可执行权限
chmod +x clean_docker_log.sh
# 启动脚本
./clean_docker_log.sh</code></pre>
<p style="">但是，这样清理之后，随着时间的推移，容器日志会像杂草一样，卷土重来。</p>
<h2 style="" id="4%E3%80%81%E8%AE%BE%E7%BD%AEdocker%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E5%A4%A7%E5%B0%8F%EF%BC%88%E6%B2%BB%E6%9C%AC%EF%BC%89"><br>
 4、设置Docker容器日志大小（治本）</h2>
<h3 style="" id="%E5%85%A8%E5%B1%80%E8%AE%BE%E7%BD%AE">全局设置</h3>
<p style="">新建/etc/docker/daemon.json，若有就不用新建了。添加log-dirver和log-opts参数，样例如下：</p>
<pre><code>{
    "registry-mirrors": [
        "https://reg-mirror.qiniu.com/",
        "https://hub-mirror.c.163.com/",
        "https://mu8wy6wu.mirror.aliyuncs.com/"
    ],
    "log-driver":"json-file",
    "log-opts": {"max-size":"500m", "max-file":"3"}
}</code></pre>
<p style="">max-size=500m，意味着一个容器日志大小上限是500M，</p>
<p style="">max-file=3，意味着一个容器有三个日志，分别是id+.json、id+1.json、id+2.json。</p>
<pre><code>// 重启docker守护进程
systemctl daemon-reload
systemctl restart docker
# 注意：设置的日志大小，只对新建的容器有效。</code></pre>
<h3 style="" id="%E5%AE%B9%E5%99%A8%E8%AE%BE%E7%BD%AE">容器设置</h3>
<p style="">另外在创建容器时也可以指定log文件的限制，添加log-opts参数，样例如下：</p>
<pre><code>docker run \
--log-opt max-size=500m \
--log-opt max-file=3 \
......</code></pre>
<p style="">max-size=500m，意味着一个容器日志大小上限是500M，</p>
<p style="">max-file=3，意味着一个容器有三个日志，分别是id+.json、id+1.json、id+2.json。</p>
<p style=""></p>
<h2 style="" id="5%E3%80%81%E5%8F%82%E8%80%83%E6%96%87%E7%AB%A0">5、参考文章</h2>
<blockquote>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/xunzhaoyao/article/details/72959917">https://blog.csdn.net/xunzhaoyao/article/details/72959917</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.cnblogs.com/testzcy/p/7904829.html">https://www.cnblogs.com/testzcy/p/7904829.html</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://docs.docker.com/config/containers/logging/configure/">https://docs.docker.com/config/containers/logging/configure/</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298719228</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:45:29 GMT</pubDate></item><item><title><![CDATA[docker容器内部安装vi命令]]></title><link>https://xiaoming728.com/archives/1702298739249</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=docker%E5%AE%B9%E5%99%A8%E5%86%85%E9%83%A8%E5%AE%89%E8%A3%85vi%E5%91%BD%E4%BB%A4&amp;url=/archives/1702298739249" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 13px; color: rgb(0, 0, 0)">有时会需要在docker容器内使用vi命令，但是新启动的docker容器内并没有vi命令，那就需要自己安装一个</span></p>
<p style=""></p>
<p style=""><span style="font-size: 13px; color: rgb(0, 0, 0)">1.使用命令</span></p>
<pre><code>apt-get update</code></pre>
<p style=""></p>
<p style=""><span style="font-size: 13px; color: rgb(0, 0, 0)">2.进行安装</span></p>
<pre><code>apt-get install vim</code></pre>
<p style=""></p>
<p style=""><span style="font-size: 13px; color: rgb(0, 0, 0)">安装完成后，就可以用了。</span></p>]]></description><guid isPermaLink="false">/archives/1702298739249</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:45:00 GMT</pubDate></item><item><title><![CDATA[Docker 构建镜像（docker build）]]></title><link>https://xiaoming728.com/archives/1702298676500</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%20%E6%9E%84%E5%BB%BA%E9%95%9C%E5%83%8F%EF%BC%88docker%20build%EF%BC%89&amp;url=/archives/1702298676500" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><span style="font-size: 15px">来源：博客-</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.qikegu.com/"><span style="font-size: 15px">奇客谷</span></a></p>
 <p style=""><span style="font-size: 15px">日期：2019年5月21日</span></p>
 <p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.qikegu.com/"><span style="font-size: 15px">https://www.qikegu.com/docs/3011</span></a></p>
</blockquote>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(199, 37, 78)">docker build</span><span style="font-size: 15px; color: rgb(77, 77, 77)">命令用于从Dockerfile构建镜像。</span></p>
<h3 style="" id="%E4%B8%80%E3%80%81%E7%94%A8%E6%B3%95">一、用法</h3>
<pre><code>docker build  -t ImageName:TagName dir</code></pre>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(199, 37, 78)">-t</span><span style="font-size: 15px; color: rgba(0, 0, 0, 0.75)"> − 给镜像加一个Tag</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(199, 37, 78)">ImageName</span><span style="font-size: 15px; color: rgba(0, 0, 0, 0.75)"> − 给镜像起的名称</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(199, 37, 78)">TagName</span><span style="font-size: 15px; color: rgba(0, 0, 0, 0.75)"> − 给镜像的Tag名</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(199, 37, 78)">Dir</span><span style="font-size: 15px; color: rgba(0, 0, 0, 0.75)"> − Dockerfile所在目录</span></p></li>
</ul>
<h3 style="" id="%E4%BA%8C%E3%80%81%E4%BE%8B%E5%AD%90">二、例子</h3>
<pre><code>docker build -t myimg:0.1 .</code></pre>
<ul>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(255, 56, 96)">myimg</span><span style="font-size: 15px; color: rgb(34, 34, 34)"> 是镜像名</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(255, 56, 96)">0.1</span><span style="font-size: 15px; color: rgb(34, 34, 34)"> 是tag</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(255, 56, 96)">.</span><span style="font-size: 15px; color: rgb(34, 34, 34)"> 表示当前目录，即Dockerfile所在目录</span></p></li>
</ul>
<p style=""><span style="font-size: 15px; color: rgb(34, 34, 34)">执行时输出如下：</span></p>
<pre><code>[root@qikegu myImg]# docker build -t myimg:0.1 .
Sending build context to Docker daemon  2.048kB
Step 1/5 : FROM ubuntu

...

Successfully built 4d62577685b4
Successfully tagged myimg:0.1
[root@qikegu myImg]#</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(34, 34, 34)">使用</span><span style="font-size: 15px; color: rgb(255, 56, 96)">docker images</span><span style="font-size: 15px; color: rgb(34, 34, 34)">查看刚构建的镜像：</span></p>
<pre><code>[root@qikegu myImg]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
myimg               0.1                 4d62577685b4        3 minutes ago       155MB
ubuntu              latest              7698f282e524        5 days ago          69.9MB
mysql               latest              990386cbd5c0        11 days ago         443MB
busybox             latest              64f5d945efcc        11 days ago         1.2MB
nginx               latest              53f3fd8007f7        13 days ago         109MB
php                 7-fpm               d330e525cad6        6 weeks ago         367MB
nginx               latest              2bcb04bdb83f        7 weeks ago         109MB
hello-world         latest              fce289e99eb9        4 months ago        1.84kB
jenkins             latest              cd14cecfdb3a        10 months ago       696MB</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(34, 34, 34)">可以看到</span><span style="font-size: 15px; color: rgb(255, 56, 96)">myimg</span><span style="font-size: 15px; color: rgb(34, 34, 34)">已经构建成功了。</span></p>]]></description><guid isPermaLink="false">/archives/1702298676500</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:44:43 GMT</pubDate></item><item><title><![CDATA[Docker 镜像加速]]></title><link>https://xiaoming728.com/archives/1702298659310</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%20%E9%95%9C%E5%83%8F%E5%8A%A0%E9%80%9F&amp;url=/archives/1702298659310" width="1" height="1" alt="" style="opacity:0;">
<p style=""><strong>网易：</strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://hub-mirror.c.163.com/"><strong>https://hub-mirror.c.163.com/</strong></a></p>
<p style=""><strong>七牛云加速器：</strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://hub-mirror.c.163.com/"><strong>https://reg-mirror.qiniu.com/</strong></a></p>
<p style=""><strong>阿里云：</strong><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://%3C%E4%BD%A0%E7%9A%84ID%3E.mirror.aliyuncs.com/"><strong>https://&lt;你的ID&gt;.</strong></a><a target="_blank" rel="noopener noreferrer nofollow" href="https://hub-mirror.c.163.com/"><strong>mirror.aliyuncs.com/</strong></a></p>
<blockquote>
 <p style=""><span style="font-size: 13px; color: rgb(51, 51, 51)">阿里云镜像获取地址：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://hub-mirror.c.163.com/">https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors<span style="font-size: 13px; color: rgb(51, 51, 51)">，登陆后，左侧菜单选中镜像加速器就可以看到你的专属地址了</span></a></p>
</blockquote>
<p style=""></p>
<p style=""><strong>配置步骤：</strong></p>
<p style="">1、<span style="font-size: 13px; color: rgb(51, 51, 51)">在 /etc/docker/daemon.json 中写入如下内容（如果文件不存在请新建该文件）：</span></p>
<pre><code>{"registry-mirrors":["https://reg-mirror.qiniu.com/","https://hub-mirror.c.163.com/"]}</code></pre>
<p style="">2、<span style="font-size: 13px; color: rgb(51, 51, 51)">重新启动服务</span></p>
<pre><code>$ sudo systemctl daemon-reload
$ sudo systemctl restart docker</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298659310</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:44:24 GMT</pubDate></item><item><title><![CDATA[Docker 常用软件安装命令]]></title><link>https://xiaoming728.com/archives/1702298695118</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%20%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E5%AE%89%E8%A3%85%E5%91%BD%E4%BB%A4&amp;url=/archives/1702298695118" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="tomcat">tomcat</h1>
<pre><code>docker run -d -p 8080:8080 \
-v /opt/soft/docker/tomcat/webapps:/usr/local/tomcat/webapps \
-v /opt/soft/docker/tomcat/logs:/usr/local/tomcat/logs \
-v /opt/soft/docker/tomcat/conf:/usr/local/tomcat/conf \
--name tomcat \
--restart=always \
tomcat:6.0.41</code></pre>
<h1 style="" id="portainer">portainer</h1>
<pre><code>docker run -d -p 8000:8000 -p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/soft/docker/portainer:/data \
--name=portainer \
--restart=always \
portainer/portainer-ce</code></pre>
<h1 style="" id="redis">redis</h1>
<pre><code>docker run -d -p 6379:6379 \
    -v /opt/soft/docker/redis/data:/data \
    --name redis \
    --restart=always \
     redis redis-server \
    --appendonly yes \
    --requirepass 'admin@redis!'</code></pre>
<h1 style="" id="jenkins">jenkins</h1>
<pre><code>docker run -d -p 8080:8080 -p 50000:50000 -u root \
  -v /opt/soft/docker/jenkins:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/soft/docker/jenkins/.m2:/root/.m2 \
  -v /etc/localtime:/etc/localtime \
  --name jenkins \
  --restart=always \
  jenkinsci/blueocean</code></pre>
<h1 style="" id="nginx">nginx</h1>
<pre><code>docker run -d -p 80:80 \
    -v /opt/soft/docker/nginx/html:/usr/share/nginx/html \
    -v /opt/soft/docker/nginx/conf.d:/etc/nginx/conf.d \
    -v /opt/soft/docker/nginx/logs:/var/log/nginx \
    --name nginx \
    --restart=always \
    nginx</code></pre>
<h1 style="" id="zookeeper">zookeeper</h1>
<pre><code>docker run -d -p 2181:2181 \
   -v /opt/soft/docker/zookeeper/data:/data \
   -v /opt/soft/docker/zookeeper/logs:/logs \
   -v /opt/soft/docker/zookeeper/datalog:/datalog \
   --name zookeeper \
   --restart=always \
   zookeeper:latest</code></pre>
<h1 style="" id="kafka">kafka</h1>
<pre><code>docker run -d --restart=always --name kafka -p 9092:9092 \
--link zookeeper \
--env KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
--env KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
--env KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.206.183:9092 \
-v /etc/localtime:/etc/localtime \
-v /opt/soft/docker/kafka/data:/kafka \
-v /opt/soft/docker/kafka/logs:/opt/kafka/logs \
wurstmeister/kafka

docker cp kafka:/opt/kafka/config /opt/soft/docker/kafka/config

docker stop kafka

docker run -d --restart=always --name kafka -p 9092:9092 \
--link zookeeper \
--env KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
--env KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
--env KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.206.180:9092 \
-v /etc/localtime:/etc/localtime \
-v /opt/soft/docker/kafka/data:/kafka \
-v /opt/soft/docker/kafka/config:/opt/kafka/config \
-v /opt/soft/docker/kafka/logs:/opt/kafka/logs \
wurstmeister/kafka</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298695118</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:44:00 GMT</pubDate></item><item><title><![CDATA[安装 docker-compose]]></title><link>https://xiaoming728.com/archives/1702298641857</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%AE%89%E8%A3%85%20docker-compose&amp;url=/archives/1702298641857" width="1" height="1" alt="" style="opacity:0;">
<p style="">1、下载docker-compose</p>
<pre><code>sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose </code></pre>
<p style="">2、设置docker-compose权限</p>
<pre><code>sudo chmod +x /usr/local/bin/docker-compose</code></pre>
<p style="">3、测试docker-compose是否安装成功</p>
<pre><code>$ docker-compose --version
docker-compose version 1.26.0, build 1110ad01</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298641857</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker-compose.jpeg&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:44:00 GMT</pubDate></item><item><title><![CDATA[Docker安装Nacos]]></title><link>https://xiaoming728.com/archives/1702298575043</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E5%AE%89%E8%A3%85Nacos&amp;url=/archives/1702298575043" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="docker%E5%AE%89%E8%A3%85nacos"><strong>Docker安装Nacos</strong></h1>
<p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">官方提供有打包好的镜像直接拉取即可；同时也可以自己制作镜像</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fnacos-group%2Fnacos-docker%2Fblob%2Fmaster%2FREADME_ZH.md">官方说明文档</a> <span style="font-size: 16px; color: rgb(64, 64, 64)">；这里我们直接使用镜像仓库中制作好的镜像。</span></p>
<pre><code># 拉取镜像
docker pull nacos/nacos-server</code></pre>
<h2 style="" id="%E5%8D%95%E6%9C%BA%E7%89%88%E9%83%A8%E7%BD%B2"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">单机版部署</span></h2>
<p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">单机版部署很多简单，直接一条命令即可完成，通过</span> <span style="font-size: 12px; color: rgb(199, 37, 78)">MODE</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">来设置使用单机模式；注意如果是多网卡的话需要配置NACOS_SERVER_IP参数来指定IP，否则可能会导致外网无法访问；</span></p>
<pre><code>docker run -d  --name nacos -p 8848:8848 \
--env MODE=standalone \
--env NACOS_SERVER_IP=192.168.56.102 \
nacos/nacos-server</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">启动成功后访问</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fnacos-group%2Fnacos-docker%2Fblob%2Fmaster%2FREADME_ZH.md"><span style="font-size: 12px; color: rgb(199, 37, 78)">http://192.168.56.102:8848/nacos/index.html</span></a> <span style="font-size: 16px; color: rgb(64, 64, 64)">即可，默认是账号和密码都是nacos。</span></p>
<h2 style="" id="%E9%9B%86%E7%BE%A4%E7%89%88%E9%83%A8%E7%BD%B2"><span fontsize="" color="rgb(64, 64, 64)" style="color: rgb(64, 64, 64)">集群版部署</span></h2>
<p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">同样准备3台服务器；分别执行如下命令；注意</span><span style="font-size: 12px; color: rgb(199, 37, 78)">NACOS_SERVER_IP</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">参数需要修改为各个服务器自己的IP地址</span></p>
<pre><code>docker run -d --name nacos-cluster -p 8848:8848 \
  --env NACOS_SERVERS=192.168.56.102,192.168.56.104,192.168.56.105 \
  --env NACOS_SERVER_IP=192.168.56.102 \
  --env SPRING_DATASOURCE_PLATFORM=mysql \
  --env MYSQL_SERVICE_HOST=192.168.56.103 \
  --env MYSQL_SERVICE_DB_NAME=nacos \
  --env MYSQL_SERVICE_USER=root \
  --env MYSQL_SERVICE_PASSWORD=123456 \
  --env MYSQL_DATABASE_NUM=1 \
  nacos/nacos-server</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">参数说明：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 12px; color: rgb(199, 37, 78)">NACOS_SERVERS</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">: 集群节点信息</span></p></li>
 <li>
  <p style=""><span style="font-size: 12px; color: rgb(199, 37, 78)">NACOS_SERVER_IP</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">: 服务IP，多网卡模式下建议指定</span></p></li>
 <li>
  <p style=""><span style="font-size: 12px; color: rgb(199, 37, 78)">SPRING_DATASOURCE_PLATFORM</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">: 使用数据库类型</span></p></li>
 <li>
  <p style=""><span style="font-size: 12px; color: rgb(199, 37, 78)">MYSQL_SERVICE_HOST</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">: MySQL数据库地址</span></p></li>
 <li>
  <p style=""><span style="font-size: 12px; color: rgb(199, 37, 78)">MYSQL_SERVICE_DB_NAME</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">: 数据库名称</span></p></li>
 <li>
  <p style=""><span style="font-size: 12px; color: rgb(199, 37, 78)">MYSQL_SERVICE_DB_NAME</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">: 数据库用户名</span></p></li>
 <li>
  <p style=""><span style="font-size: 12px; color: rgb(199, 37, 78)">MYSQL_SERVICE_PASSWORD</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">: 数据库密码</span></p></li>
 <li>
  <p style=""><span style="font-size: 12px; color: rgb(199, 37, 78)">MYSQL_DATABASE_NUM</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">: 数据库数量，默认就是1，可以不填写</span></p></li>
</ul>
<p style=""></p>
<p style=""><span style="font-size: 16px; color: rgb(64, 64, 64)">至此集群就搭建完成了。可以通过docker日志命令查询nacos的日志信息(</span><span style="font-size: 12px; color: rgb(199, 37, 78)">docker logs -ft --tail 200 nacos-cluster</span> <span style="font-size: 16px; color: rgb(64, 64, 64)">)。</span></p>]]></description><guid isPermaLink="false">/archives/1702298575043</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fnacos.png&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:43:00 GMT</pubDate></item><item><title><![CDATA[Docker环境搭建单机版RocketMQ]]></title><link>https://xiaoming728.com/archives/1702298592645</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA%E5%8D%95%E6%9C%BA%E7%89%88RocketMQ&amp;url=/archives/1702298592645" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1%E3%80%81%E4%B8%8B%E8%BD%BDrocketmq-docker%E6%BA%90%E7%A0%81">1、下载rocketmq-docker源码</h3>
<pre><code>git clone https://github.com/apache/rocketmq-docker.git</code></pre>
<h3 style="" id="2%E3%80%81%E7%BC%96%E8%AF%91rocketmq%E9%95%9C%E5%83%8F">2、编译RocketMQ镜像</h3>
<pre><code>cd image-build
sh build-image.sh RMQ-VERSION BASE-IMAGE</code></pre>
<p style=""><span fontsize="" color="rgb(106, 115, 125)" style="color: rgb(106, 115, 125)">注意: RMQ-VERSION：表示编译的版本点击&nbsp;</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://archive.apache.org/dist/rocketmq/">这里</a>获取<span fontsize="" color="rgb(106, 115, 125)" style="color: rgb(106, 115, 125)">。 BASE-IMAGE：取值 [centos, alpine].</span></p>
<p style=""><span fontsize="" color="rgb(106, 115, 125)" style="color: rgb(106, 115, 125)">举个栗子:</span><span style="font-size: 16px; color: rgb(106, 115, 125)">&nbsp;</span><code>sh build-image.sh 4.8.0 alpine</code></p>
<h3 style="" id="3%E3%80%81%E7%94%9F%E6%88%90rocketmq%E8%BF%90%E8%A1%8C%E7%9B%AE%E5%BD%95">3、生成RocketMQ运行目录</h3>
<pre><code>sh stage.sh RMQ-VERSION</code></pre>
<p style="">注意：RMQ-VERSION是RocketMQ镜像的标签。执行完上述shell脚本（例如sh stage.sh 4.8.0）后，它将生成一个stage目录（./stages/4.8.0）。例如RMQ版本是4.8.0，则用户可以在目录下启动RMQ容器。</p>
<h3 style="" id="4%E3%80%81%E5%AE%89%E8%A3%85rocketmq%E5%AE%B9%E5%99%A8">4、安装RocketMQ容器</h3>
<h4 style="" id="%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC%E5%AE%89%E8%A3%85">使用脚本安装</h4>
<pre><code># 1、创建目录
mkdir -p /home/docker/rocketmq
# 2、copy初始化配置
cp -r ./stages/4.8.0/data play-docker.sh /home/docker/rocketmq
# 3、安装容器
cd /home/docker/rocketmq 
sh play-docker.sh alpine</code></pre>
<h4 style="" id="%E4%BD%BF%E7%94%A8docker%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85">使用docker命令安装</h4>
<h4 style="" id="1%E3%80%81centos%E7%8E%AF%E5%A2%83%E5%AE%89%E8%A3%85">1、centos环境安装</h4>
<p style="">如果镜像编译的是centos环境，使用一下安装命令</p>
<pre><code># Start nameserver
docker run -d --name rmqnamesrv \
 -v /home/docker/rocketmq/data/namesrv/logs:/home/rocketmq/logs \
 -p 9876:9876 \
 apacherocketmq/rocketmq:4.8.0 sh mqnamesrv
 
 # Start Broker
docker run -d --name rmqbroker \
 -v /home/docker/rocketmq/broker/logs:/home/rocketmq/logs \
 -v /home/docker/rocketmq/data/broker/conf/broker.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf \
 --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" \
 -p 10909:10909 -p 10911:10911 -p 10912:10912 \
 apacherocketmq/rocketmq:4.8.0 \
 sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf</code></pre>
<p style=""></p>
<h4 style="" id="2%E3%80%81alpine%E7%8E%AF%E5%A2%83%E5%AE%89%E8%A3%85">2、alpine环境安装</h4>
<p style="">如果镜像编译的是alpine环境，使用一下安装命令</p>
<pre><code># Start nameserver
docker run -d --name rmqnamesrv \
 -v /home/docker/rocketmq/data/namesrv/logs:/home/rocketmq/logs \
 -p 9876:9876 \
 apacherocketmq/rocketmq:4.8.0-alpine sh mqnamesrv

# Start Broker
docker run -d --name rmqbroker \
 -v /home/docker/rocketmq/data/broker/logs:/home/rocketmq/logs \
 -v /home/docker/rocketmq/data/broker/conf/broker.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf \
 --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" \
 -p 10909:10909 -p 10911:10911 -p 10912:10912 \
 apacherocketmq/rocketmq:4.8.0-alpine \
 sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf</code></pre>
<h3 style="" id="5%E3%80%81%E5%AE%89%E8%A3%85rocketmq-console-ng%E5%9B%BE%E5%BD%A2%E6%8E%A7%E5%88%B6%E5%8F%B0">5、安装RocketMQ-Console-NG图形控制台</h3>
<p style="">1、下载镜像</p>
<pre><code>docker pull styletang/rocketmq-console-ng</code></pre>
<p style="">2、安装镜像</p>
<pre><code>docker run -d -p 8080:8080 --name rocketmq-console-ng \
-v /home/docker/rocketmq/tmp:/tmp \
--link rmqnamesrv:namesrv \
-e "JAVA_OPTS=-Drocketmq.namesrv.addr=namesrv:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" \
styletang/rocketmq-console-ng</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298592645</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fapache_rocketmq-ar21.png&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:43:00 GMT</pubDate></item><item><title><![CDATA[CentOS 7 安装 Docker]]></title><link>https://xiaoming728.com/archives/1702298619414</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=CentOS%207%20%E5%AE%89%E8%A3%85%20Docker&amp;url=/archives/1702298619414" width="1" height="1" alt="" style="opacity:0;">
<p style="">1、卸载旧版本Docker</p>
<pre><code>sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine</code></pre>
<p style="">2、安装Yum工具</p>
<pre><code>sudo yum install -y yum-utils</code></pre>
<p style="">3、Yum添加Docker应用库地址</p>
<pre><code>sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo</code></pre>
<p style="">4、安装Docker</p>
<pre><code>sudo yum install docker-ce docker-ce-cli containerd.io</code></pre>
<p style="">5、启动Docker</p>
<pre><code>sudo systemctl start docker</code></pre>
<p style="">6、查看Docker启动状态</p>
<pre><code>sudo systemctl status docker</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298619414</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:43:00 GMT</pubDate></item><item><title><![CDATA[解决 ClientAbortException: java.io.IOException: Broken pipe]]></title><link>https://xiaoming728.com/archives/1702298548987</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E8%A7%A3%E5%86%B3%20ClientAbortException%3A%20java.io.IOException%3A%20Broken%20pipe&amp;url=/archives/1702298548987" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><span style="font-size: 15px">来源：简书- 汤问嗔</span></p>
 <p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.jianshu.com/p/d7f5de27fec5"><span style="font-size: 15px">https://www.jianshu.com/p/d7f5de27fec5</span></a></p>
 <p style=""><span style="font-size: 15px">日期：2020.10.26 21:09:37</span></p>
</blockquote>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(64, 64, 64)">做压力测试的时候遇到这个问题，自己并发的经验不够。在并发数量低的情况下没有遇到这个问题。经过网上搜索，参考了别人的思路</span></p>
<p style=""><span style="font-size: 15px; color: rgb(64, 64, 64)">我们的服务器前端通过nginx做后端Java的负载均衡。当并发上来之后，nginx不断的和后端的Java服务器建立连接。虽然做了keep-alive配置，却依然挡不住连接的销毁和创建。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(64, 64, 64)">通过如下命令对连接状态做了统计</span></p>
<pre><code>netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(64, 64, 64)">发下</span><span style="font-size: 15px; color: rgb(199, 37, 78)">TIME_WAIT</span><span style="font-size: 15px; color: rgb(64, 64, 64)">数量非常之高，也就是nginx这台服务器主动断开连接，导致Java后端往断开的套接字上写数据，于是报错。按照参考链接里的说法做了如下优化，下面的内容摘自别人文章。</span></p>
<pre><code>解决方案很简单，通过修改/etc/sysctl.conf文件，服务器能够快速回收和重用那些TIME_WAIT的资源

#表示开启SYN Cookies。当出现SYN等待队列溢出时，启用cookies来处理，可防范少量SYN攻击，默认为0，表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接，默认为0，表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收，默认为0，表示关闭
net.ipv4.tcp_tw_recycle = 1
#表示如果套接字由本端要求关闭，这个参数决定了它保持在FIN-WAIT-2状态的时间
net.ipv4.tcp_fin_timeout=30
生效，如下命令

/sbin/sysctl -p</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(199, 37, 78)">TIME_WAIT</span><span style="font-size: 15px; color: rgb(64, 64, 64)">降下来了，也没有报Java异常了，顺利解决。</span></p>
<p style=""><span style="font-size: 15px; color: rgb(64, 64, 64)">这边文章算是记录了如何把不同人的解决思路综合，拿来解决自己的问题。参考了别人两篇文章。</span></p>
<p style=""></p>
<blockquote>
 <p style=""><span style="font-size: 15px; color: rgb(64, 64, 64)">参考链接</span></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.jianshu.com/p/d7f5de27fec5"><span style="font-size: 15px; color: rgb(199, 37, 78)">https://blog.csdn.net/zqz_zqz/article/details/52235479</span></a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.jianshu.com/p/d7f5de27fec5"><span style="font-size: 15px; color: rgb(199, 37, 78)">https://www.cnblogs.com/shengs/p/4495998.html</span></a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298548987</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fbug.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Mon, 11 Dec 2023 12:42:00 GMT</pubDate></item><item><title><![CDATA[SpringBoot整合WebService]]></title><link>https://xiaoming728.com/archives/1702298499968</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=SpringBoot%E6%95%B4%E5%90%88WebService&amp;url=/archives/1702298499968" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">来源：简书-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.jianshu.com/u/d3708504dbd5">程序员小杰</a></p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.jianshu.com/u/d3708504dbd5">https://www.jianshu.com/p/8cf6ccb765a2</a></p>
 <p style="">日期：2020.11.10 21:26:33</p>
</blockquote>
<h2 style="" id="%E4%B8%80%E3%80%81%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%88%9B%E5%BB%BA%E6%96%B9%E5%BC%8F">一、服务端创建方式</h2>
<p style="">由于SpringBoot提供了WebService的starter组件，所以集成WebService相当简单</p>
<h3 style="" id="%E5%8A%A0%E5%85%A5%E4%BE%9D%E8%B5%96">加入依赖</h3>
<pre><code>        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.cxf&lt;/groupId&gt;
            &lt;artifactId&gt;cxf-spring-boot-starter-jaxws&lt;/artifactId&gt;
            &lt;version&gt;3.4.5&lt;/version&gt;
        &lt;/dependency&gt;</code></pre>
<h3 style="" id="%E5%88%9B%E5%BB%BAwebservice%E6%8E%A5%E5%8F%A3">创建WebService接口</h3>
<pre><code>package com.gongj.webservice_server.service;

import com.gongj.webservice_server.DTO.UserDTO;
import javax.jws.WebService;

@WebService(name = "IUserServer", // 暴露服务名称
        targetNamespace = "http://service.webservice_server.gongj.com"// 命名空间,一般是接口的包名倒序
)
public interface IUserServer {

    UserDTO getUser(Long str);
}</code></pre>
<h3 style="" id="%E5%88%9B%E5%BB%BA%E5%AE%9E%E4%BD%93%E7%B1%BB">创建实体类</h3>
<pre><code>@Data
public class UserDTO {
    private Long id;
    private String name;
    private Integer age;
    private String address;
}</code></pre>
<h3 style="" id="%E5%88%9B%E5%BB%BAwebservice%E6%8E%A5%E5%8F%A3%E7%9A%84%E5%AE%9E%E7%8E%B0%E7%B1%BB">创建WebService接口的实现类</h3>
<pre><code>package com.gongj.webservice_server.service.impl;

import com.gongj.webservice_server.DTO.UserDTO;
import com.gongj.webservice_server.service.IUserServer;
import org.springframework.stereotype.Service;

import javax.jws.WebService;

@Service
@WebService(serviceName = "IUserServer", // 与接口中指定的name一致
        targetNamespace = "http://service.webservice_server.gongj.com", // 与接口中的命名空间一致
        endpointInterface = "com.gongj.webservice_server.service.IUserServer" // 接口地址
)
public class UserServerImpl implements IUserServer {
    @Override
    public UserDTO getUser(Long id) {
        UserDTO user = new UserDTO();
        user.setId(id);
        user.setAddress("上海市浦东新区");
        user.setAge(25);
        user.setName("gongj");
        return user;
    }
}</code></pre>
<h3 style="" id="%E5%88%9B%E5%BB%BAwebservice%E9%85%8D%E7%BD%AE%E7%B1%BB">创建WebService配置类</h3>
<pre><code>package com.gongj.webservice_server.config;

import com.gongj.webservice_server.service.IUserServer;
import org.apache.cxf.Bus;
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.transport.servlet.CXFServlet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.xml.ws.Endpoint;

@Configuration
public class CxfConfig {

    @Autowired
    private IUserServer userServer;
    /**
     * 注入Servlet 注意beanName不能为dispatcherServlet  
     (不推荐用这个，官方的demo直接在配置文件配置即可 cxf.path: /webservice)
     * @return
     */
 //   @Bean
 //   public ServletRegistrationBean cxfServlet() {
 //       return new ServletRegistrationBean(new CXFServlet(),"/webservice/*");
 //   }

    @Bean(name = Bus.DEFAULT_BUS_ID)
    public SpringBus springBus() {
        return new SpringBus();
    }


    @Bean
    public Endpoint endpoint() {
        EndpointImpl endpoint = new EndpointImpl(springBus(), userServer);
        endpoint.publish("/api");
        return endpoint;
    }
}</code></pre>
<p style="">application.yml配置文件配置webservice统一路径前缀</p>
<p style=""><br><span fontsize="" color="rgb(204, 120, 50)" style="color: rgb(204, 120, 50)">cxf.path</span>: /webservice</p>
<p style=""></p>
<p style=""></p>
<p style="">启动服务，进行访问：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.jianshu.com/u/d3708504dbd5">http://localhost:8080/webservice</a></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-cyxs.png&amp;size=m" style="display: inline-block"></p>
<p style="">点击链接跳转，我们会看到一个页面，这是wsdl服务描述文档。</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ssdz.png&amp;size=m" style="display: inline-block"></p>
<p style=""></p>
<h2 style="" id="%E4%BA%8C%E3%80%81%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%B0%83%E7%94%A8%E6%96%B9%E5%BC%8F">二、客户端调用方式</h2>
<h3 style="" id="%E5%8A%A0%E5%85%A5%E4%BE%9D%E8%B5%96">加入依赖</h3>
<pre><code>&lt;dependency&gt;
  &lt;groupId&gt;org.apache.cxf&lt;/groupId&gt;
  &lt;artifactId&gt;cxf-spring-boot-starter-jaxws&lt;/artifactId&gt;
  &lt;version&gt;3.3.4&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<h3 style="" id="%E8%B0%83%E7%94%A8">调用</h3>
<pre><code>public static void main(String[] args) {
        JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
        Client client = dcf.createClient("http://localhost:8080/webservice/api?wsdl");
        Object[] objects = new Object[0];
        ObjectMapper mapper = new ObjectMapper();
        try {
            // invoke("方法名",参数1,参数2,参数3....);
            objects = client.invoke("getUser", 99L);
            System.out.println(mapper.writeValueAsString(objects[0]));
        } catch (java.lang.Exception e) {
            e.printStackTrace();
        }
    }

输出结果：
{"address":"上海市浦东新区","age":25,"id":99,"name":"gongj"}
</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298499968</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fspringboot-1.webp&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Mon, 11 Dec 2023 12:42:00 GMT</pubDate></item><item><title><![CDATA[SpringBoot与SpringCloud的版本对应]]></title><link>https://xiaoming728.com/archives/1702298454392</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=SpringBoot%E4%B8%8ESpringCloud%E7%9A%84%E7%89%88%E6%9C%AC%E5%AF%B9%E5%BA%94&amp;url=/archives/1702298454392" width="1" height="1" alt="" style="opacity:0;">
<p style=""><strong>spring官方对应查看网址：</strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://start.spring.io/actuator/info"><strong>https://start.spring.io/actuator/info</strong></a></p>
<pre><code>{
    "git": {
        "branch": "bc9bfc1e7a872f628baa142780fa999f308fef71",
        "commit": {
            "id": "bc9bfc1",
            "time": "2021-07-08T06:31:35Z"
        }
    },
    "build": {
        "version": "0.0.1-SNAPSHOT",
        "artifact": "start-site",
        "versions": {
            "spring-boot": "2.5.2",
            "initializr": "0.11.0-SNAPSHOT"
        },
        "name": "start.spring.io website",
        "time": "2021-07-08T06:47:57.255Z",
        "group": "io.spring.start"
    },
    "bom-ranges": {
        "azure": {
            "2.2.4": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.3.0.M1",
            "3.2.0": "Spring Boot &gt;=2.3.0.M1 and &lt;2.4.0-M1",
            "3.5.0": "Spring Boot &gt;=2.4.0.M1 and &lt;2.5.0-M1",
            "3.6.0": "Spring Boot &gt;=2.5.0.M1 and &lt;2.6.0-M1"
        },
        "codecentric-spring-boot-admin": {
            "2.2.4": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.3.0.M1",
            "2.3.1": "Spring Boot &gt;=2.3.0.M1 and &lt;2.5.0-M1"
        },
        "solace-spring-boot": {
            "1.0.0": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.3.0.M1",
            "1.1.0": "Spring Boot &gt;=2.3.0.M1 and &lt;2.6.0-M1"
        },
        "solace-spring-cloud": {
            "1.0.0": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.3.0.M1",
            "1.1.1": "Spring Boot &gt;=2.3.0.M1 and &lt;2.4.0-M1",
            "2.1.0": "Spring Boot &gt;=2.4.0.M1 and &lt;2.6.0-M1"
        },
        "spring-cloud": {
            "Hoxton.SR12": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.4.0.M1",
            "2020.0.0-M3": "Spring Boot &gt;=2.4.0.M1 and &lt;=2.4.0.M1",
            "2020.0.0-M4": "Spring Boot &gt;=2.4.0.M2 and &lt;=2.4.0-M3",
            "2020.0.0": "Spring Boot &gt;=2.4.0.M4 and &lt;=2.4.0",
            "2020.0.3": "Spring Boot &gt;=2.4.1 and &lt;2.5.3-SNAPSHOT",
            "2020.0.4-SNAPSHOT": "Spring Boot &gt;=2.5.3-SNAPSHOT"
        },
        "spring-cloud-gcp": {
            "2.0.3": "Spring Boot &gt;=2.4.0-M1 and &lt;2.5.0-M1"
        },
        "spring-cloud-services": {
            "2.2.6.RELEASE": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.3.0.RELEASE",
            "2.3.0.RELEASE": "Spring Boot &gt;=2.3.0.RELEASE and &lt;2.4.0-M1",
            "2.4.1": "Spring Boot &gt;=2.4.0-M1 and &lt;2.5.0-M1"
        },
        "spring-geode": {
            "1.2.12.RELEASE": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.3.0.M1",
            "1.3.12.RELEASE": "Spring Boot &gt;=2.3.0.M1 and &lt;2.4.0-M1",
            "1.4.8": "Spring Boot &gt;=2.4.0-M1 and &lt;2.5.0-M1",
            "1.5.2": "Spring Boot &gt;=2.5.0-M1"
        },
        "vaadin": {
            "14.6.5": "Spring Boot &gt;=2.1.0.RELEASE and &lt;2.6.0-M1"
        },
        "wavefront": {
            "2.0.2": "Spring Boot &gt;=2.1.0.RELEASE and &lt;2.4.0-M1",
            "2.1.1": "Spring Boot &gt;=2.4.0-M1 and &lt;2.5.0-M1",
            "2.2.0": "Spring Boot &gt;=2.5.0-M1"
        }
    },
    "dependency-ranges": {
        "native": {
            "0.9.0": "Spring Boot &gt;=2.4.3 and &lt;2.4.4",
            "0.9.1": "Spring Boot &gt;=2.4.4 and &lt;2.4.5",
            "0.9.2": "Spring Boot &gt;=2.4.5 and &lt;2.5.0-M1",
            "0.10.0": "Spring Boot &gt;=2.5.0-M1 and &lt;2.5.2-M1",
            "0.10.1": "Spring Boot &gt;=2.5.2-M1 and &lt;2.5.3-M1",
            "0.10.2-SNAPSHOT": "Spring Boot &gt;=2.5.3-M1 and &lt;2.6.0-M1"
        },
        "okta": {
            "1.4.0": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.4.0-M1",
            "1.5.1": "Spring Boot &gt;=2.4.0-M1 and &lt;2.4.1",
            "2.0.1": "Spring Boot &gt;=2.4.1 and &lt;2.5.0-M1",
            "2.1.0": "Spring Boot &gt;=2.5.0-M1 and &lt;2.6.0-M1"
        },
        "mybatis": {
            "2.1.4": "Spring Boot &gt;=2.1.0.RELEASE and &lt;2.5.0-M1",
            "2.2.0": "Spring Boot &gt;=2.5.0-M1"
        },
        "camel": {
            "3.3.0": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.3.0.M1",
            "3.5.0": "Spring Boot &gt;=2.3.0.M1 and &lt;2.4.0-M1",
            "3.10.0": "Spring Boot &gt;=2.4.0.M1 and &lt;2.5.0-M1",
            "3.11.0": "Spring Boot &gt;=2.5.0.M1 and &lt;2.6.0-M1"
        },
        "open-service-broker": {
            "3.1.1.RELEASE": "Spring Boot &gt;=2.2.0.RELEASE and &lt;2.3.0.M1",
            "3.2.0": "Spring Boot &gt;=2.3.0.M1 and &lt;2.4.0-M1",
            "3.3.0": "Spring Boot &gt;=2.4.0-M1 and &lt;2.5.0-M1"
        }
    }
}</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298454392</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fspringboot-1.webp&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Mon, 11 Dec 2023 12:41:00 GMT</pubDate></item><item><title><![CDATA[解决 java.lang.OutOfMemoryError： unable to create new native thread]]></title><link>https://xiaoming728.com/archives/1702298475036</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E8%A7%A3%E5%86%B3%20java.lang.OutOfMemoryError%EF%BC%9A%20unable%20to%20create%20new%20native%20thread&amp;url=/archives/1702298475036" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><span style="font-size: 15px">来源： CSDN-</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/pansanday"><span style="font-size: 15px">Pansanday</span></a></p>
 <p style=""><span style="font-size: 15px">日期：2019-01-14 17:29:17</span></p>
 <p style=""><span style="font-size: 15px">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/pansanday"><span style="font-size: 15px">https://blog.csdn.net/pansanday/article/details/79697761</span></a></p>
</blockquote>
<h2 style="" id="%E5%87%BA%E7%8E%B0%E5%9C%BA%E6%99%AF">出现场景</h2>
<p style=""><span style="font-size: 15px">最近在部署一个省的项目时，遇到这个问题，该环境提供的服务器配置偏低，而项目本身为了性能，大量的使用的线程，导出出现 java.lang.OutOfMemoryError： unable to create new native thread异常</span></p>
<p style=""></p>
<h2 style="" id="%E5%BC%82%E5%B8%B8%E5%88%86%E6%9E%90">异常分析</h2>
<p style=""><span style="font-size: 15px">这个异常问题本质原因是我们创建了太多的线程，而能创建的线程数是有限制的，导致了异常的发生。能创建的线程数的具体计算公式如下：</span></p>
<pre><code>(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads</code></pre>
<p style=""><span style="font-size: 15px">MaxProcessMemory 指的是一个进程的最大内存</span></p>
<p style=""><span style="font-size: 15px">JVMMemory JVM内存</span></p>
<p style=""><span style="font-size: 15px">ReservedOsMemory 保留的操作系统内存</span></p>
<p style=""><span style="font-size: 15px">ThreadStackSize 线程栈的大小</span></p>
<p style=""><span style="font-size: 15px">在java语言里， 当你创建一个线程的时候，虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程，而这个系统线程的内存用的不是JVMMemory，而是系统中剩下的内存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。由公式得出结论：你给JVM内存越多，那么你能创建的线程越少，越容易发生 java.lang.OutOfMemoryError: unable to create new native thread<br><br></span></p>
<h2 style="" id="%E9%97%AE%E9%A2%98%E5%8E%9F%E5%9B%A0">问题原因</h2>
<p style=""><span style="font-size: 15px">linux下每个用户都是有进程限制的</span></p>
<p style=""><span style="font-size: 15px">通过命令</span><code>cat /etc/security/limits.d/20-nproc.conf</code><span style="font-size: 15px">可以看出，普通用户进程限制是1024，root用户没有进程限制</span></p>
<pre><code># cat /etc/security/limits.d/20-nproc.conf 
# Default limit for number of user's processes to prevent
# accidental fork bombs.
# See rhbz #432903 for reasoning.
 
*          soft    nproc     1024
root       soft    nproc     unlimited</code></pre>
<p style=""><span style="font-size: 15px">每个用户最大进程数不止这么多（ulimit -u 或 ulimit -a命令查看）</span></p>
<pre><code># ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 31402
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 278528
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 10240
cpu time               (seconds, -t) unlimited
max user processes              (-u) 31402
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited</code></pre>
<p style=""><span style="font-size: 15px">可看出，max user processes为31402，也就是用户最大的进程数为31402</span></p>
<p style=""><span style="font-size: 15px">由于测试服务器运行了很多项目，每个项目基本上都用到了sofaRPC，所以造成用户创建的进程数被用完，无法创建的错误</span></p>
<p style=""></p>
<h2 style="" id="%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88">解决方案</h2>
<p style=""><span style="font-size: 15px">修改20-nproc.conf文件，将普通用户的进程数改到他能达到的最大值</span></p>
<pre><code>vi /etc/security/limits.d/90-nproc.conf

# cat /etc/security/limits.d/90-nproc.conf
# Default limit for number of user's processes to prevent
# accidental fork bombs.
# See rhbz #432903 for reasoning.
 
*          soft    nproc     31402
root       soft    nproc     unlimited</code></pre>
<p style=""><span style="font-size: 15px">重启应用程序服务器（Tomcat）</span></p>
<p style=""><span style="font-size: 15px">至此问题解决。</span></p>]]></description><guid isPermaLink="false">/archives/1702298475036</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fbug.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Java技术</category><pubDate>Mon, 11 Dec 2023 12:41:00 GMT</pubDate></item><item><title><![CDATA[Docker - 实现本地镜像的导出、导入]]></title><link>https://xiaoming728.com/archives/1702298401501</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Docker%20-%20%E5%AE%9E%E7%8E%B0%E6%9C%AC%E5%9C%B0%E9%95%9C%E5%83%8F%E7%9A%84%E5%AF%BC%E5%87%BA%E3%80%81%E5%AF%BC%E5%85%A5&amp;url=/archives/1702298401501" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><span style="font-size: 15px">来源：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.hangge.com/"><span style="font-size: 15px">航歌-www.hangge.com</span></a></p>
 <p style=""><span style="font-size: 15px">日期：2019-07-03<br>
   链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.hangge.com/"><span style="font-size: 15px">https://www.hangge.com/blog/cache/detail_2411.html</span></a></p>
</blockquote>
<p style=""></p>
<p style=""><span style="font-size: 15px">有时我们需要将一台电脑上的镜像复制到另一台电脑上使用，除了可以借助仓库外，还可以直接将镜像保存成一个文件，再拷贝到另一台电脑上导入使用。</span></p>
<p style=""><span style="font-size: 15px">对于镜像的导出和导入，Docker 提供了两种方案，下面分别进行介绍。</span></p>
<h2 style="" id="%E4%B8%80%E3%80%81%E4%BD%BF%E7%94%A8-export-%E5%92%8C-import">一、使用 export 和 import</h2>
<h3 style="" id="1%EF%BC%8C%E6%9F%A5%E7%9C%8B%E6%9C%AC%E6%9C%BA%E7%9A%84%E5%AE%B9%E5%99%A8">1，查看本机的容器</h3>
<p style=""><span style="font-size: 15px">这两个命令是通过容器来导入、导出镜像。首先我们使用 </span><code>docker ps -a </code><span style="font-size: 15px">命令查看本机所有的容器。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-izpy.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="2%EF%BC%8C%E5%AF%BC%E5%87%BA%E9%95%9C%E5%83%8F">2，导出镜像</h3>
<p style=""><span style="font-size: 15px">（1）使用 docker export 命令根据容器 ID 将镜像导出成一个文件。</span></p>
<pre><code>docker export f299f501774c &gt; hangger_server.tar</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">（2）上面命令执行后，就可以看到文件已经保存到当前的 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 终端目录下。</span></p>
<h3 style="" id="3%EF%BC%8C%E5%AF%BC%E5%85%A5%E9%95%9C%E5%83%8F">3，导入镜像</h3>
<p style=""><span style="font-size: 15px">（1）使用 docker import 命令则可将这个镜像文件导入进来。</span></p>
<pre><code>docker import - new_hangger_server &lt; hangger_server.tar</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">（2）执行 </span><code>docker images </code><span style="font-size: 15px; color: rgb(51, 51, 51)">命令就可以看到镜像已经导入进来了。</span></p>
<h2 style="" id="%E4%BA%8C%E3%80%81%E4%BD%BF%E7%94%A8-save-%E5%92%8C-load">二、使用 save 和 load</h2>
<h3 style="" id="1%EF%BC%8C%E6%9F%A5%E7%9C%8B%E6%9C%AC%E6%9C%BA%E7%9A%84%E5%AE%B9%E5%99%A8">1，查看本机的容器</h3>
<p style=""><span style="font-size: 15px">这两个命令是通过镜像来保存、加载镜像文件的。首先我们使用 </span><code>docker images</code><span style="font-size: 15px">命令查看本机所有的镜像。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-auqq.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="2%EF%BC%8C%E4%BF%9D%E5%AD%98%E9%95%9C%E5%83%8F">2，保存镜像</h3>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">（1）下面使用 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker save </span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">命令根据 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">ID</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 将镜像保存成一个文件。</span></p>
<pre><code>docker save -o portainer_latest.tar portainer/portainer:latest</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">（2）我们还可以同时将多个 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">image</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 打包成一个文件，比如下面将镜像库中的 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">postgres</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 和 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">mongo</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 打包：</span></p>
<pre><code>docker save -o images.tar postgres:9.6 mongo:3.4</code></pre>
<h3 style="" id="3%EF%BC%8C%E8%BD%BD%E5%85%A5%E9%95%9C%E5%83%8F">3，载入镜像</h3>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">使用 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker load</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 命令则可将这个镜像文件载入进来。</span></p>
<pre><code>docker load &lt; portainer_latest.tar</code></pre>
<h2 style="" id="%E9%99%84%EF%BC%9A%E4%B8%A4%E7%A7%8D%E6%96%B9%E6%A1%88%E7%9A%84%E5%B7%AE%E5%88%AB">附：两种方案的差别</h2>
<p style=""><span style="font-size: 15px">特别注意：两种方法不可混用。<br>
  如果使用 import 导入 save 产生的文件，虽然导入不提示错误，但是启动容器时会提示失败，会出现类似"docker: Error response from daemon: Container command not found or does not exist"的错误。</span></p>
<h3 style="" id="1%EF%BC%8C%E6%96%87%E4%BB%B6%E5%A4%A7%E5%B0%8F%E4%B8%8D%E5%90%8C">1，文件大小不同</h3>
<ul>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">export</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 导出的镜像文件体积小于 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">save</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 保存的镜像</span></p></li>
</ul>
<h3 style="" id="2%EF%BC%8C%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%AF%B9%E9%95%9C%E5%83%8F%E9%87%8D%E5%91%BD%E5%90%8D">2，是否可以对镜像重命名</h3>
<ul>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker import </span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">可以为镜像指定新名称</span></p></li>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker load </span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">不能对载入的镜像重命名</span></p></li>
</ul>
<h3 style="" id="3%EF%BC%8C%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%90%8C%E6%97%B6%E5%B0%86%E5%A4%9A%E4%B8%AA%E9%95%9C%E5%83%8F%E6%89%93%E5%8C%85%E5%88%B0%E4%B8%80%E4%B8%AA%E6%96%87%E4%BB%B6%E4%B8%AD">3，是否可以同时将多个镜像打包到一个文件中</h3>
<ul>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker export </span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">不支持</span></p></li>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker save</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 支持</span></p></li>
</ul>
<h3 style="" id="4%EF%BC%8C%E6%98%AF%E5%90%A6%E5%8C%85%E5%90%AB%E9%95%9C%E5%83%8F%E5%8E%86%E5%8F%B2">4，是否包含镜像历史</h3>
<ul>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">export</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 导出（</span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">import</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 导入）是根据容器拿到的镜像，再导入时会丢失镜像所有的历史记录和元数据信息（即仅保存容器当时的快照状态），所以无法进行回滚操作。</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">而 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">save</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 保存（</span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">load</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 加载）的镜像，没有丢失镜像的历史，可以回滚到之前的层（</span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">layer</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">）。</span></p></li>
</ul>
<h3 style="" id="5%EF%BC%8C%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF%E4%B8%8D%E5%90%8C">5，应用场景不同</h3>
<ul>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: rgb(51, 51, 51)">docker export 的应用场景</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">：主要用来制作基础镜像，比如我们从一个 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">ubuntu</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 镜像启动一个容器，然后安装一些软件和进行一些设置后，使用 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker export </span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">保存为一个基础镜像。然后，把这个镜像分发给其他人使用，比如作为基础的开发环境。</span></p></li>
 <li>
  <p style=""><strong><span style="font-size: 15px; color: rgb(51, 51, 51)">docker save 的应用场景</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">：如果我们的应用是使用 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker-compose.yml </span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">编排的多个镜像组合，但我们要部署的客户服务器并不能连外网。这时就可以使用 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker save</span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)"> 将用到的镜像打个包，然后拷贝到客户服务器上使用 </span><strong><span style="font-size: 15px; color: rgb(0, 128, 128)">docker load </span></strong><span style="font-size: 15px; color: rgb(51, 51, 51)">载入。</span></p></li>
</ul>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298401501</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fdocker.webp&amp;size=m" type="image/jpeg" length="0"/><category>Docker技术</category><pubDate>Mon, 11 Dec 2023 12:40:00 GMT</pubDate></item><item><title><![CDATA[MyBatis 批量插入的 3 种方式]]></title><link>https://xiaoming728.com/archives/1702298367693</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=MyBatis%20%E6%89%B9%E9%87%8F%E6%8F%92%E5%85%A5%E7%9A%84%203%20%E7%A7%8D%E6%96%B9%E5%BC%8F&amp;url=/archives/1702298367693" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">来源：简书-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.jianshu.com/u/f1da868a4ec5">楼主楼主</a></p>
 <p style="">日期：2018.03.29 11:41:40</p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://www.jianshu.com/p/cce617be9f9e">https://www.jianshu.com/p/cce617be9f9e</a></p>
</blockquote>
<p style=""></p>
<p style=""><span style="font-size: 15px">数据库使用的是sqlserver，JDK版本1.8，运行在SpringBoot环境下，对比3种可用的方式：</span></p>
<ol>
 <li>
  <p style=""><span style="font-size: 15px">反复执行单条插入语句</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">xml拼接sql</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">批处理执行</span></p></li>
</ol>
<p style=""></p>
<p style=""><span style="font-size: 15px">先说结论：少量插入请使用反复插入单条数据，方便。数量较多请使用批处理方式。（可以考虑以有需求的插入数据量20条左右为界吧，在我的测试和数据库环境下耗时都是百毫秒级的，方便最重要）。</span><strong><span style="font-size: 15px">无论何时都不用xml拼接sql的方式</span></strong><span style="font-size: 15px">。</span></p>
<p style=""></p>
<h3 style="" id="%E4%BB%A3%E7%A0%81">代码</h3>
<h4 style="" id="%E6%8B%BC%E6%8E%A5sql%E7%9A%84xml">拼接SQL的xml</h4>
<p style=""><span style="font-size: 15px">newId()是sqlserver生成UUID的函数</span></p>
<pre><code>&lt;insert id="insertByBatch" parameterType="java.util.List"&gt;
    INSERT INTO tb_item VALUES
    &lt;foreach collection="list" item="item" index="index" separator=","&gt;
        (newId(),#{item.uniqueCode},#{item.projectId},#{item.name},#{item.type},#{item.packageUnique},
        #{item.isPackage},#{item.factoryId},#{item.projectName},#{item.spec},#{item.length},#{item.weight},
        #{item.material},#{item.setupPosition},#{item.areaPosition},#{item.bottomHeight},#{item.topHeight},
        #{item.serialNumber},#{item.createTime}&lt;/foreach&gt;
&lt;/insert&gt;</code></pre>
<h4 style="" id="mapper%E6%8E%A5%E5%8F%A3">Mapper接口</h4>
<p style=""><span style="font-size: 15px">Mapper是 mybatis插件tk.Mapper 的接口</span></p>
<pre><code>public interface ItemMapper extends Mapper&lt;Item&gt; {
    int insertByBatch(List&lt;Item&gt; itemList);
}</code></pre>
<h4 style="" id="service%E7%B1%BB">Service类</h4>
<pre><code>@Service
public class ItemService {
    @Autowired
    private ItemMapper itemMapper;
    @Autowired
    private SqlSessionFactory sqlSessionFactory;
    //批处理
    @Transactional
    public void add(List&lt;Item&gt; itemList) {
        SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH,false);
        ItemMapper mapper = session.getMapper(ItemMapper.class);
        for (int i = 0; i &lt; itemList.size(); i++) {
            mapper.insertSelective(itemList.get(i));
            if(i%1000==999){//每1000条提交一次防止内存溢出
                session.commit();
                session.clearCache();
            }
        }
        session.commit();
        session.clearCache();
    }
    //拼接sql
    @Transactional
    public void add1(List&lt;Item&gt; itemList) {
        itemList.insertByBatch(itemMapper::insertSelective);
    }
    //循环插入
    @Transactional
    public void add2(List&lt;Item&gt; itemList) {
        itemList.forEach(itemMapper::insertSelective);
    }
}</code></pre>
<h4 style="" id="%E6%B5%8B%E8%AF%95%E7%B1%BB">测试类</h4>
<pre><code>@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ApplicationBoot.class)
public class ItemServiceTest {
    @Autowired
    ItemService itemService;

    private List&lt;Item&gt; itemList = new ArrayList&lt;&gt;();
    //生成测试List
    @Before 
    public void createList(){
        String json ="{\n" +
                "        \"areaPosition\": \"TEST\",\n" +
                "        \"bottomHeight\": 5,\n" +
                "        \"factoryId\": \"0\",\n" +
                "        \"length\": 233.233,\n" +
                "        \"material\": \"Q345B\",\n" +
                "        \"name\": \"TEST\",\n" +
                "        \"package\": false,\n" +
                "        \"packageUnique\": \"45f8a0ba0bf048839df85f32ebe5bb81\",\n" +
                "        \"projectId\": \"094b5eb5e0384bb1aaa822880a428b6d\",\n" +
                "        \"projectName\": \"项目_TEST1\",\n" +
                "        \"serialNumber\": \"1/2\",\n" +
                "        \"setupPosition\": \"1B柱\",\n" +
                "        \"spec\": \"200X200X200\",\n" +
                "        \"topHeight\": 10,\n" +
                "        \"type\": \"Steel\",\n" +
                "        \"uniqueCode\": \"12344312\",\n" +
                "        \"weight\": 100\n" +
                "    }";
        Item test1 = JSON.parseObject(json,Item.class);
        test1.setCreateTime(new Date());
        for (int i = 0; i &lt; 1000; i++) {//测试会修改此数量
            itemList.add(test1);
        }
    }
     //批处理
    @Test
    @Transactional
    public void tesInsert() {
        itemService.add(itemList);
    }
    //拼接字符串
    @Test
    @Transactional
    public void testInsert1(){
        itemService.add1(itemList);
    }
    //循环插入
    @Test
    @Transactional
    public void testInsert2(){
        itemService.add2(itemList);
    }
}</code></pre>
<p style=""></p>
<h3 style="" id="%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C%EF%BC%9A">测试结果：</h3>
<p style=""><span style="font-size: 15px">10条 25条数据插入经多次测试，波动性较大，但基本都在百毫秒级别</span></p>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(240, 240, 240)">
    <p style=""><span style="font-size: 15px">方式</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(240, 240, 240)">
    <p style=""><span style="font-size: 15px">50条</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(240, 240, 240)">
    <p style=""><span style="font-size: 15px">100条</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(240, 240, 240)">
    <p style=""><span style="font-size: 15px">500条</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(240, 240, 240)">
    <p style=""><span style="font-size: 15px">1000条</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">批处理</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">159ms</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">208ms</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">305ms</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">432ms</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(248, 248, 248)">
    <p style=""><span style="font-size: 15px">xml拼接sql</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(248, 248, 248)">
    <p style=""><span style="font-size: 15px">208ms</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(248, 248, 248)">
    <p style=""><span style="font-size: 15px">232ms</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(248, 248, 248)">
    <p style=""><span style="font-size: 15px">报错</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9; background-color: rgb(248, 248, 248)">
    <p style=""><span style="font-size: 15px">报错</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">反复单条插入</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">1013ms</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">2266ms</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">8141ms</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 15px">18861ms</span></p></td>
  </tr>
 </tbody>
</table>
<p style=""><span style="font-size: 15px">其中 拼接sql方式在插入500条和1000条时报错（似乎是因为sql语句过长，此条跟数据库类型有关，未做其他数据库的测试）：</span><code>com.microsoft.sqlserver.jdbc.SQLServerException: 传入的表格格式数据流(TDS)远程过程调用(RPC)协议流不正确。此 RPC 请求中提供了过多的参数。最多应为 2100</code></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">可以发现</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">循环插入的时间复杂度是 O(n),并且常数C很大</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">拼接SQL插入的时间复杂度（应该）是 O(logn),但是成功完成次数不多，不确定</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">批处理的效率的时间复杂度是 O(logn),并且常数C也比较小</span></p></li>
</ul>
<p style=""></p>
<h3 style="" id="%E7%BB%93%E8%AE%BA">结论</h3>
<p style=""><strong><span style="font-size: 15px">循环插入单条数据</span></strong><span style="font-size: 15px">虽然效率极低，但是代码量极少，在使用tk.Mapper的插件情况下，仅需代码：</span></p>
<pre><code>@Transactional
public void add1(List&lt;Item&gt; itemList) {
    itemList.forEach(itemMapper::insertSelective);
}</code></pre>
<p style=""><span style="font-size: 15px">因此，在需求插入数据数量不多的情况下肯定用它了。</span></p>
<p style=""></p>
<p style=""><strong><span style="font-size: 15px">xml拼接sql</span></strong><span style="font-size: 15px">是最不推荐的方式，使用时有大段的xml和sql语句要写，很容易出错，工作效率很低。更关键点是，虽然效率尚可，但是真正需要效率的时候你挂了，要你何用？</span></p>
<p style=""></p>
<p style=""><strong><span style="font-size: 15px">批处理执行</span></strong><span style="font-size: 15px">是有大数据量插入时推荐的做法，使用起来也比较方便。</span></p>]]></description><guid isPermaLink="false">/archives/1702298367693</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmybatis.jpg&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Mon, 11 Dec 2023 12:39:00 GMT</pubDate></item><item><title><![CDATA[MySQL占用CPU超过100%解决过程]]></title><link>https://xiaoming728.com/archives/1702298242009</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=MySQL%E5%8D%A0%E7%94%A8CPU%E8%B6%85%E8%BF%87100%25%E8%A7%A3%E5%86%B3%E8%BF%87%E7%A8%8B&amp;url=/archives/1702298242009" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">来源：CSDN-</span><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/jimshen"><span style="font-size: 15px">jimshen</span></a></p>
 <p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">日期：</span><span style="font-size: 15px; color: rgb(153, 154, 170)">2017-12-04 09:58:38</span></p>
 <p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/jimshen"><span style="font-size: 15px">https://blog.csdn.net/jimshen/article/details/78706538</span></a></p>
</blockquote>
<p style=""></p>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">2017年12月2日上午，将学校新闻网2015年之前的45000多条记录迁移到了新网站的mysql数据库，新网站上有2015年1月1日之后的9000多条记录，数据量一下子增加了5倍。</span><span style="font-size: 15px"><br></span><span style="font-size: 15px; color: rgb(51, 51, 51)">2017年12月3日晚上9点多，有领导和老师反映新闻网无法访问，立即登录服务器进行排查。</span></p>
<h3 style="" id="%E4%B8%80%E3%80%81%E4%BD%BF%E7%94%A8top%E5%91%BD%E4%BB%A4%E7%9C%8B%E5%88%B0%E7%9A%84%E6%83%85%E5%86%B5%E5%A6%82%E4%B8%8B">一、使用top命令看到的情况如下</h3>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-piaf.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">可以看到服务器负载很高，mysql CPU使用已达到接近400%（因为是四核，所以会有超过100%的情况）。</span></p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E6%9F%A5%E7%9C%8B%E6%AD%A3%E5%9C%A8%E6%89%A7%E8%A1%8C%E7%9A%84%E8%AF%AD%E5%8F%A5">二、查看正在执行的语句</h3>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">在服务器上执行mysql -u root -p之后，输入</span><code>show full processlist;</code><span style="font-size: 15px; color: rgb(51, 51, 51)"> 可以看到正在执行的语句。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-hrvv.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">可以看到是下面的SQL语句执行耗费了较长时间。</span></p>
<pre><code>SELECT id,title,most_top,view_count,posttime FROM article 
where status=3 AND catalog_id in (select catalog_id from catalog where catalog_id=17 or parent_id=17)  
order by most_top desc,posttime desc limit 0,8</code></pre>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">直接执行此条SQL，发现速度很慢，需要1-6秒的时间（跟mysql正在并发执行的查询有关，如果没有并发的，需要1秒多）。</span></p>
<h3 style="" id="%E4%B8%89%E3%80%81%E9%80%9A%E8%BF%87explain%E5%88%86%E6%9E%90sql%E8%AF%AD%E5%8F%A5">三、通过EXPLAIN分析SQL语句</h3>
<pre><code>EXPLAIN SELECT id,title,most_top,view_count,posttime FROM article 
where status=3 AND catalog_id in (select catalog_id from catalog where catalog_id=17 or parent_id=17)  
order by most_top desc,posttime desc limit 0,8</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-iuvt.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">可以看到，主select对27928条记录使用filesort进行了排序，这是造成查询速度慢的原因。然后8个并发的查询使CPU专用很高。至此对SQL进行优化，缩减查询范围，修改排序规则。</span></p>
<h3 style="" id="%E5%9B%9B%E3%80%81%E4%BC%98%E5%8C%96%E6%95%88%E6%9E%9C">四、优化效果</h3>
<p style=""><span style="font-size: 15px; color: rgb(51, 51, 51)">查询时间大幅度缩短，CPU负载很轻</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-rcmx.png&amp;size=m" style="display: inline-block"></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xrlw.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="%E4%BA%94%E3%80%81%E6%80%9D%E8%80%83%E6%80%BB%E7%BB%93">五、思考总结</h3>
<p style=""><span style="font-size: 15px">通过这个问题，数据库在大数量下对查询、更新等耗时成比例增加，造成整体服务访问变慢，针对这种情况，可以通过此方法排查问题，找到访问变慢的根源。</span></p>]]></description><guid isPermaLink="false">/archives/1702298242009</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Mon, 11 Dec 2023 12:39:00 GMT</pubDate></item><item><title><![CDATA[MySQL主从复制(Master-Slave)实践]]></title><link>https://xiaoming728.com/archives/1702298141339</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=MySQL%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%28Master-Slave%29%E5%AE%9E%E8%B7%B5&amp;url=/archives/1702298141339" width="1" height="1" alt="" style="opacity:0;">
<p style="">MySQL数据库自身提供的主从复制功能可以方便的实现数据的多处自动备份，实现数据库的拓展。多个数据备份不仅可以加强数据的安全性，通过实现读写分离还能进一步提升数据库的负载性能。</p>
<p style="">下图就描述了一个多个数据库间主从复制与读写分离的模型(来源网络)：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-crlg.png&amp;size=m" style="display: inline-block"></p>
<p style="">在一主多从的数据库体系中，多个从服务器采用异步的方式更新主数据库的变化，业务服务器在执行写或者相关修改数据库的操作是在主服务器上进行的，读操作则是在各从服务器上进行。如果配置了多个从服务器或者多个主服务器又涉及到相应的负载均衡问题，关于负载均衡具体的技术细节还没有研究过，今天就先简单的实现一主一从的主从复制功能。</p>
<p style="">Mysql主从复制的实现原理图大致如下(来源网络)：</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-caik.png&amp;size=m" style="display: inline-block"></p>
<p style="">MySQL之间数据复制的基础是二进制日志文件（binary log file）。一台MySQL数据库一旦启用二进制日志后，其作为master，它的数据库中所有操作都会以“事件”的方式记录在二进制日志中，其他数据库作为slave通过一个I/O线程与主服务器保持通信，并监控master的二进制日志文件的变化，如果发现master二进制日志文件发生变化，则会把变化复制到自己的中继日志中，然后slave的一个SQL线程会把相关的“事件”执行到自己的数据库中，以此实现从数据库和主数据库的一致性，也就实现了主从复制。</p>
<p style=""></p>
<h2 style="" id="%E5%AE%9E%E7%8E%B0mysql%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E9%9C%80%E8%A6%81%E8%BF%9B%E8%A1%8C%E7%9A%84%E9%85%8D%E7%BD%AE">实现MySQL主从复制需要进行的配置</h2>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">主服务器：</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">开启二进制日志</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">配置唯一的server-id</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">获得master二进制日志文件名及位置</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">创建一个用于slave和master通信的用户账号</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">从服务器：</span></p></li>
</ul>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">配置唯一的server-id</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">使用master分配的用户账号读取master二进制日志</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">启用slave服务</span></p></li>
</ul>
<p style=""></p>
<h2 style="" id="%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0%E8%BF%87%E7%A8%8B%E5%A6%82%E4%B8%8B">具体实现过程如下</h2>
<h3 style="" id="%E4%B8%80%E3%80%81%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C"><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">一、准备工作</span></h3>
<p style="">1.主从数据库版本最好一致</p>
<p style="">2.主从数据库内数据保持一致</p>
<p style="">主数据库：182.92.172.80 /linux</p>
<p style="">从数据库：123.57.44.85 /linux</p>
<p style=""></p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E4%B8%BB%E6%95%B0%E6%8D%AE%E5%BA%93master%E4%BF%AE%E6%94%B9"><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">二、主数据库master修改</span></h3>
<h4 style="" id="1.%E4%BF%AE%E6%94%B9mysql%E9%85%8D%E7%BD%AE">1.修改mysql配置</h4>
<p style="">找到主数据库的配置文件my.cnf(或者my.ini)，我的在/etc/mysql/my.cnf,在[mysqld]部分插入如下两行：</p>
<pre><code>[mysqld]
server-id=1 #设置server-id
log-bin=mysql-bin #开启二进制日志
binlog-format=ROW #binlog格式</code></pre>
<h4 style="" id="2.%E9%87%8D%E5%90%AFmysql%EF%BC%8C%E5%88%9B%E5%BB%BA%E7%94%A8%E4%BA%8E%E5%90%8C%E6%AD%A5%E7%9A%84%E7%94%A8%E6%88%B7%E8%B4%A6%E5%8F%B7">2.重启mysql，创建用于同步的用户账号</h4>
<p style="">打开mysql会话shell&gt;mysql -hlocalhost -uname -ppassword</p>
<p style="">创建用户并授权：用户：rel1密码：slavepass</p>
<pre><code>mysql&gt; CREATE USER 'repl'@'123.57.44.85' IDENTIFIED BY 'slavepass';#创建用户
mysql&gt; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'123.57.44.85';#分配权限
mysql&gt;flush privileges; #刷新权限</code></pre>
<h4 style="" id="3.%E6%9F%A5%E7%9C%8Bmaster%E7%8A%B6%E6%80%81">3.查看master状态</h4>
<p style="">记录二进制文件名(mysql-bin.000003)和位置(73)：</p>
<pre><code>mysql &gt; SHOW MASTER STATUS;
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000003 | 73 | test | manual,mysql |
+------------------+----------+--------------+------------------+</code></pre>
<h3 style="" id="%E4%B8%89%E3%80%81%E4%BB%8E%E6%9C%8D%E5%8A%A1%E5%99%A8slave%E4%BF%AE%E6%94%B9"><span fontsize="" color="rgb(245, 34, 45)" style="color: rgb(245, 34, 45)">三、从服务器slave修改</span></h3>
<h4 style="" id="1.%E4%BF%AE%E6%94%B9mysql%E9%85%8D%E7%BD%AE">1.修改mysql配置</h4>
<p style="">同样找到my.cnf配置文件，添加server-id</p>
<pre><code>[mysqld]
server-id=2 #设置server-id，必须唯一</code></pre>
<h4 style="" id="2.%E9%87%8D%E5%90%AFmysql">2.重启mysql</h4>
<p style="">打开mysql会话，执行同步SQL语句(需要主服务器主机名，登陆凭据，二进制文件的名称和位置)：</p>
<pre><code>mysql&gt; 
CHANGE MASTER TO
MASTER_HOST='192.168.206.180',
MASTER_USER='repl',
MASTER_PASSWORD='slavepass',
MASTER_LOG_FILE='mysql-bin.000044',
MASTER_LOG_POS=54661781;</code></pre>
<h4 style="" id="3.%E5%90%AF%E5%8A%A8slave%E5%90%8C%E6%AD%A5%E8%BF%9B%E7%A8%8B">3.启动slave同步进程</h4>
<pre><code>mysql&gt; start slave;</code></pre>
<h4 style="" id="4.%E6%9F%A5%E7%9C%8Bslave%E7%8A%B6%E6%80%81">4.查看slave状态</h4>
<pre><code>mysql&gt; show slave status\G;
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 182.92.172.80
Master_User: rep1
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000013
Read_Master_Log_Pos: 11662
Relay_Log_File: mysqld-relay-bin.000022
Relay_Log_Pos: 11765
Relay_Master_Log_File: mysql-bin.000013
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
...</code></pre>
<p style="">当Slave_IO_Running和Slave_SQL_Running都为YES的时候就表示主从同步设置成功了。接下来就可以进行一些验证了，比如在主master数据库的test数据库的一张表中插入一条数据，在slave的test库的相同数据表中查看是否有新增的数据即可验证主从复制功能是否有效，还可以关闭slave（mysql&gt;stop slave;）,然后再修改master，看slave是否也相应修改（停止slave后，master的修改不会同步到slave），就可以完成主从复制功能的验证了。</p>
<p style=""></p>
<h2 style="" id="%E8%BF%98%E5%8F%AF%E4%BB%A5%E7%94%A8%E5%88%B0%E7%9A%84%E5%85%B6%E4%BB%96%E7%9B%B8%E5%85%B3%E5%8F%82%E6%95%B0">还可以用到的其他相关参数</h2>
<p style="">master开启二进制日志后默认记录所有库所有表的操作，可以通过配置来指定只记录指定的数据库甚至指定的表的操作，具体在mysql配置文件的[mysqld]可添加修改如下选项：</p>
<pre><code># 不同步哪些数据库
binlog-ignore-db = mysql
binlog-ignore-db = test
binlog-ignore-db = information_schema
# 只同步哪些数据库，除此之外，其他不同步
binlog-do-db = game</code></pre>
<p style="">如之前查看master状态时就可以看到只记录了test库，忽略了manual和mysql库。</p>
<p style=""></p>
<blockquote>
 <h2 style="" id="%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99%EF%BC%9A">&nbsp;参考资料：</h2>
</blockquote>
<blockquote>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://dev.mysql.com/doc/refman/5.5/en/replication.html">MySQL官方手册</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://blog.csdn.net/mycwq/article/details/17136001">MySQL数据库设置主从同步</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://heylinux.com/archives/1004.html">MySQL主从复制（Master-Slave）与读写分离（MySQL-Proxy）实践</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://www.cnblogs.com/agileai/p/6126823.html">CentOS系统MySQL双机热备配置</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298141339</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Mon, 11 Dec 2023 12:36:22 GMT</pubDate></item><item><title><![CDATA[MySQL锁库锁表]]></title><link>https://xiaoming728.com/archives/1702298196724</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=MySQL%E9%94%81%E5%BA%93%E9%94%81%E8%A1%A8&amp;url=/archives/1702298196724" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="1%E3%80%81mysql%E9%94%81%E5%BA%93">1、MySQL锁库</h3>
<p style="">这个命令是全局读锁定，执行了命令之后所有库所有表都被锁定只读。一般都是用在数据库联机备份，这个时候数据库的写操作将被阻塞，读操作顺利进行。</p>
<pre><code>FLUSH TABLES WITH READ LOCK;</code></pre>
<p style="">解锁的语句：</p>
<pre><code>UNLOCK TABLES;</code></pre>
<p style="">&nbsp;</p>
<h3 style="" id="2%E3%80%81mysql%E9%94%81%E8%A1%A8">2、MySQL锁表</h3>
<p style="">这个命令是表级别的锁定，可以定制锁定某一个表，不影响其他表的写操作。</p>
<pre><code>LOCK TABLES tbl_name [AS alias] {READ [LOCAL] | [LOW_PRIORITY] WRITE}</code></pre>
<p style="">例如：</p>
<pre><code>LOCK TABLES test READ;</code></pre>
<p style="">解锁语句也是：</p>
<pre><code>UNLOCK TABLES;</code></pre>
<h3 style="" id=""></h3>
<h3 style="" id="3%E3%80%81%E6%B3%A8%E6%84%8F">&nbsp;3、注意</h3>
<p style="">这两个语句在执行的时候都需要注意个特点，就是 隐式提交的语句。在退出mysql终端的时候都会隐式的执行unlock tables。也就是如果要让表锁定生效就必须一直保持对话。</p>
<p style="">&nbsp;</p>
<p style="">P.S.&nbsp; MYSQL的read lock和wirte lock</p>
<p style="">read-lock:&nbsp; 允许其他并发的读请求，但阻塞写请求，即可以同时读，但不允许任何写。也叫共享锁</p>
<p style="">write-lock: 不允许其他并发的读和写请求，是排他的(exclusive)。也叫独占锁</p>]]></description><guid isPermaLink="false">/archives/1702298196724</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Mon, 11 Dec 2023 12:36:00 GMT</pubDate></item><item><title><![CDATA[MySQL主从同步异常解决办法]]></title><link>https://xiaoming728.com/archives/1702298076060</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=MySQL%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5%E5%BC%82%E5%B8%B8%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95&amp;url=/archives/1702298076060" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">来源：CSDN-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/a2589293499">a2589293499</a></p>
 <p style="">日期：2019-07-04 14:52:44</p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/a2589293499">https://blog.csdn.net/a2589293499/article/details/94617907</a></p>
</blockquote>
<p style=""></p>
<blockquote>
 <p style="">主从同步共有两个进程，I/O线程（stop slave IO_THREAD;）和SQL线程（stop slave SQL_THREAD;）</p>
 <p style="">I/<a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/a2589293499">O线程会维护master.info</a>信息的更新</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.csdn.net/a2589293499">SQL线程会维护relay-log.info</a>信息的更新</p>
</blockquote>
<h3 style="" id="%E4%B8%80%E3%80%81%E6%9F%A5%E7%9C%8B%E5%90%8C%E6%AD%A5%E5%BC%82%E5%B8%B8%E9%97%AE%E9%A2%98">一、查看同步异常问题</h3>
<p style="">登录mysql从库，查看同步状态，发现Slave_SQL_Running状态不正常，Last_Error提示主存同步失败的问题。</p>
<pre><code>mysql -u root -p
mysql&gt; show slave status\G
Slave_IO_Running: Yes
Slave_SQL_Running: No</code></pre>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-zcku.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="%E4%BA%8C%E3%80%81%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88-%E8%B7%B3%E8%BF%87%E9%94%99%E8%AF%AF">二、解决方案-跳过错误</h3>
<p style="">跳过一步错误，继续同步</p>
<pre><code>mysql&gt; stop slave;
mysql&gt; set global sql_slave_skip_counter =1;
mysql&gt; start slave;</code></pre>
<p style="">查看同步状态</p>
<pre><code>mysql&gt; show slave status\G
Slave_IO_Running: Yes
Slave_SQL_Running: Yes</code></pre>
<p style="">恢复正常</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-iklo.png&amp;size=m" style="display: inline-block"></p>
<h3 style="" id="%E4%B8%89%E3%80%81%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88-%E9%87%8D%E6%96%B0%E5%90%8C%E6%AD%A5">三、解决方案-重新同步</h3>
<p style="">登录主库mysql，加只读锁</p>
<pre><code>mysql -uroot -ppassword
mysql&gt; flush tables with read lock;
mysql&gt; exit</code></pre>
<p style="">备份数据（数据量大的话，时间会很长）</p>
<pre><code>mysqldump -uroot -p -hpassword &gt; /tmp/mysql.bak.sql</code></pre>
<p style="">传递到从库</p>
<pre><code>scp /tmp/mysql.bak.sql root@192.168.0.200:/tmp/</code></pre>
<p style="">登录到从库mysql</p>
<pre><code>mysql -u root -p</code></pre>
<p style="">停止同步</p>
<pre><code>mysql&gt; stop slave;</code></pre>
<p style="">导入数据</p>
<pre><code>mysql&gt; source /tmp/mysql.bak.sql</code></pre>
<p style="">重新设置从库同步，同步点是主库show master status信息里的File、Position两项</p>
<pre><code>CHANGE MASTER TO 
MASTER_HOST='192.168.0.100', 
MASTER_USER='slave1', 
MASTER_PASSWORD='123456', 
MASTER_LOG_FILE='mysql-bin.000005', 
MASTER_LOG_POS=154;</code></pre>
<p style="">开启同步</p>
<pre><code>mysql&gt; start slave;</code></pre>
<p style="">查看同步状态是否回复正常，Slave_IO_Running和Slave_SQL_Running都为Yes表示正常</p>
<pre><code>mysql&gt; show slave status\G
Slave_IO_Running: Yes
Slave_SQL_Running: Yes</code></pre>
<p style="">切回主库，关闭主库只读锁</p>
<pre><code>mysql&gt; unlock tables;</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298076060</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Mon, 11 Dec 2023 12:35:00 GMT</pubDate></item><item><title><![CDATA[MySQL命令创建数据库与创建用户以及授权]]></title><link>https://xiaoming728.com/archives/1702298025277</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=MySQL%E5%91%BD%E4%BB%A4%E5%88%9B%E5%BB%BA%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%8E%E5%88%9B%E5%BB%BA%E7%94%A8%E6%88%B7%E4%BB%A5%E5%8F%8A%E6%8E%88%E6%9D%83&amp;url=/archives/1702298025277" width="1" height="1" alt="" style="opacity:0;">
<p style="">先登录mysql：</p>
<pre><code>本地登录命令：
mysql -u root -p
远程登录命令：
mysql  -h 10.0.42.180 -u root -p
输入密码后登录，接下来操作如下：</code></pre>
<h4 style="" id="1%E3%80%81%E5%88%9B%E5%BB%BA%E6%95%B0%E6%8D%AE%E5%BA%93">1、创建数据库</h4>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">语法：create schema [数据库名称] default character set</span><span style="font-size: 16px; color: rgb(0, 102, 0)">utf8mb4</span><span style="font-size: 16px; color: rgb(79, 79, 79)">collate</span><span style="font-size: 16px; color: rgb(0, 102, 0)">utf8mb4</span><span style="font-size: 16px; color: rgb(79, 79, 79)">_general_ci;</span></p>
 <p style="margin-left: 2px!important;"><span style="font-size: 16px; color: rgb(79, 79, 79)">采用create schema和create database创建数据库的效果一样。</span></p>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">示例：</span><span style="font-size: 16px; color: rgb(0, 102, 0)">create schema db_name default character set utf8mb4 collate utf8mb4_general_ci;</span></p>
</blockquote>
<p style=""></p>
<h4 style="" id="2%E3%80%81%E5%88%9B%E5%BB%BA%E7%94%A8%E6%88%B7">2、创建用户</h4>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">语法：create user '[用户名称]'@'%' identified by '[用户密码]';</span></p>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">　　密码8位以上，包括：大写字母、小写字母、数字、特殊字符</span></p>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">　　%：匹配所有主机，该地方还可以设置成‘</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://localhost"><span style="font-size: 16px; color: rgb(79, 79, 79)">localhost</span></a><span style="font-size: 16px; color: rgb(79, 79, 79)">’，代表只能本地访问，例如root账户默认为‘</span><a target="_blank" rel="noopener noreferrer nofollow" href="http://localhost"><span style="font-size: 16px; color: rgb(79, 79, 79)">localhost</span></a><span style="font-size: 16px; color: rgb(79, 79, 79)">‘</span></p>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">示例：</span><span style="font-size: 16px; color: rgb(0, 102, 0)">create user &nbsp;'name'@'%' identified by 'Pwd_123456';</span></p>
</blockquote>
<p style=""></p>
<h4 style="" id="3%E3%80%81%E7%94%A8%E6%88%B7%E6%8E%88%E6%9D%83%E6%95%B0%E6%8D%AE%E5%BA%93">3、用户授权数据库</h4>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">语法：grant select,insert,update,delete,create on [数据库名称].* to [用户名称]@'%';</span></p>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">　　*代表整个数据库</span></p>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">&nbsp; &nbsp; &nbsp; &nbsp;all&nbsp;privileges&nbsp;数据库的所有操作权限</span></p>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">示例：</span><span style="font-size: 16px; color: rgb(0, 102, 0)">grant select,insert,update,delete,create on</span> <span style="font-size: 16px; color: rgb(0, 102, 0)">data_name.* to</span> <span style="font-size: 16px; color: rgb(0, 102, 0)">name@'%';</span></p>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">示例：</span><span style="font-size: 16px; color: rgb(0, 102, 0)">grant all privileges&nbsp;on</span> <span style="font-size: 16px; color: rgb(0, 102, 0)">db_name.* to</span> <span style="font-size: 16px; color: rgb(0, 102, 0)">name@'%';</span></p>
</blockquote>
<p style=""></p>
<h4 style="" id="4%E3%80%81%E7%AB%8B%E5%8D%B3%E5%90%AF%E7%94%A8%E4%BF%AE%E6%94%B9">4、立即启用修改</h4>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(0, 102, 0)">flush &nbsp;privileges ;</span></p>
</blockquote>
<p style=""></p>
<h4 style="" id="5%E3%80%81%E5%8F%96%E6%B6%88%E7%94%A8%E6%88%B7%E6%89%80%E6%9C%89%E6%95%B0%E6%8D%AE%E5%BA%93%EF%BC%88%E8%A1%A8%EF%BC%89%E7%9A%84%E6%89%80%E6%9C%89%E6%9D%83%E9%99%90">5、取消用户所有数据库（表）的所有权限</h4>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">revoke all on </span><em><span style="font-size: 16px; color: rgb(79, 79, 79)">.</span></em><span style="font-size: 16px; color: rgb(79, 79, 79)"> from</span> <span style="font-size: 16px; color: rgb(79, 79, 79)">name;</span></p>
</blockquote>
<p style=""></p>
<h4 style="" id="6%E3%80%81%E5%88%A0%E9%99%A4%E7%94%A8%E6%88%B7">6、删除用户</h4>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">delete from mysql.user where user='name';</span></p>
</blockquote>
<p style=""></p>
<h4 style="" id="7%E3%80%81%E5%88%A0%E9%99%A4%E6%95%B0%E6%8D%AE%E5%BA%93">7、删除数据库</h4>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">drop database [schema名称|数据库名称];</span></p>
</blockquote>
<p style=""></p>
<h4 style="" id="ps-%3A-%E5%9C%A8%E6%93%8D%E4%BD%9C%E8%BF%87%E7%A8%8B%E4%B8%AD%E5%A6%82%E6%9E%9C%E9%81%87%E5%88%B0%E9%94%99%E8%AF%AF">PS : 在操作过程中如果遇到错误</h4>
<p style=""><span style="font-size: 16px; color: rgb(204, 0, 0)">"The MySQL server is running with the --skip-grant-tables option so it cannot execute this statement"</span></p>
<p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">则先进行一下刷新操作：</span></p>
<blockquote>
 <p style=""><span style="font-size: 16px; color: rgb(79, 79, 79)">mysql&gt;</span> <span style="font-size: 16px; color: rgb(0, 102, 0)">flush privileges;</span></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702298025277</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Mon, 11 Dec 2023 12:34:00 GMT</pubDate></item><item><title><![CDATA[Mysql分库分表]]></title><link>https://xiaoming728.com/archives/1702297993629</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Mysql%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8&amp;url=/archives/1702297993629" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="%E5%BC%95%E5%85%A5%E4%BE%9D%E8%B5%96">引入依赖</h1>
<pre><code>    &lt;!--shardingsphere--&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.shardingsphere&lt;/groupId&gt;
            &lt;artifactId&gt;shardingsphere-jdbc-core-spring-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;5.0.0-alpha&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;!--druid--&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba&lt;/groupId&gt;
            &lt;artifactId&gt;druid-spring-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;1.2.11&lt;/version&gt;
        &lt;/dependency&gt;</code></pre>
<h1 style="" id="%E9%85%8D%E7%BD%AE%E6%95%B0%E6%8D%AE%E6%BA%90">配置数据源</h1>
<p style="">shardingsphere结合druid配置数据源，因为shardingsphere 5.X有common属性，所以一些公共的配置就写在了一块（最后会提供完整的配置文件）</p>
<pre><code>## 数据源配置
spring:
    autoconfigure:
        exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
            stat-view-servlet:
                enabled: true
                # 设置白名单，不填则允许所有访问
                allow:
                url-pattern: /druid/*
                loginUsername: admin
                loginPassword: 123456
            web-stat-filter:
                enabled: true
            filter:
                stat:
                    enabled: true
                    # 慢SQL记录
                    log-slow-sql: true
                    slow-sql-millis: 1000
                    merge-sql: true
                wall:
                    config:
                        multi-statement-allow: true
    shardingsphere:
        datasource:
            common:
                type: com.alibaba.druid.pool.DruidDataSource
                driver-class-name: com.mysql.cj.jdbc.Driver
                # 初始连接数
                initial-size: 6
                # 最小连接池数量
                min-idle: 3
                # 最大连接池数量
                maxActive: 20
                # 配置获取连接等待超时的时间
                maxWait: 60000
                # 配置间隔多久才进行一次检测，检测需要关闭的空闲连接，单位是毫秒
                timeBetweenEvictionRunsMillis: 60000
                # 配置一个连接在池中最小生存的时间，单位是毫秒
                minEvictableIdleTimeMillis: 300000
                # 配置一个连接在池中最大生存的时间，单位是毫秒
                maxEvictableIdleTimeMillis: 900000
                #Oracle需要打开注释
                validationQuery: SELECT 1 FROM DUAL
                testWhileIdle: true
                testOnBorrow: false
                testOnReturn: false
                # 打开PSCache，并且指定每个连接上PSCache的大小
                poolPreparedStatements: true
                maxPoolPreparedStatementPerConnectionSize: 20
                # 配置监控统计拦截的filters，去掉后监控界面sql无法统计，'wall'用于防火墙
                filters: stat,wall,slf4j
                # 通过connectProperties属性来打开mergeSql功能；慢SQL记录
                connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
                wall:
                    multi-statement-allow: true
            #配置分库分表的数据源
            names: db0,db1,db2
            #配置数据库连接地址
            db0:
                url: jdbc:mysql://127.0.0.1:3306/ry_vue?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT%2B8
                username: root
                password: 123456
            db1:
                url: jdbc:mysql://127.0.0.1:3306/ry?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT%2B8
                username: root
                password: 123456
            db2:
                url: jdbc:mysql://127.0.0.1:3306/ry1?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT%2B8
                username: root
                password: 123456</code></pre>
<h1 style="" id="druid%E9%85%8D%E7%BD%AE%E7%B1%BB">druid配置类</h1>
<h2 style="" id="%E5%8E%BB%E9%99%A4druid%E5%BA%95%E9%83%A8%E5%B9%BF%E5%91%8A">去除druid底部广告</h2>
<pre><code>package com.ruoyi.framework.config;
 
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.aspectj.lang.enums.DataSourceType;
import com.ruoyi.framework.datasource.DynamicDataSource;
 
/**
 * druid 配置多数据源
 *
 * @author ruoyi
 */
@Configuration
public class DruidConfig{
 
 
    /**
     * 去除监控页面底部的广告
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true")
    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
    {
        // 获取web监控页面的参数
        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
        // 提取common.js的配置路径
        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
        final String filePath = "support/http/resources/js/common.js";
        // 创建filter进行过滤
        Filter filter = new Filter()
        {
            @Override
            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
            {
            }
            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                    throws IOException, ServletException
            {
                chain.doFilter(request, response);
                // 重置缓冲区，响应头不会被重置
                response.resetBuffer();
                // 获取common.js
                String text = Utils.readFromResource(filePath);
                // 正则替换banner, 除去底部的广告信息
                text = text.replaceAll("&lt;a.*?banner\"&gt;&lt;/a&gt;&lt;br/&gt;", "");
                text = text.replaceAll("powered.*?shrek.wang&lt;/a&gt;", "");
                response.getWriter().write(text);
            }
            @Override
            public void destroy()
            {
            }
        };
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(filter);
        registrationBean.addUrlPatterns(commonJsPattern);
        return registrationBean;
    }
}</code></pre>
<h1 style="" id="%E5%85%BC%E5%AE%B9druid%E5%92%8Cshardingsphere">兼容druid和shardingsphere</h1>
<pre><code>package com.ruoyi.framework.config;
 
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.spring.boot.autoconfigure.stat.DruidFilterConfiguration;
import com.alibaba.druid.spring.boot.autoconfigure.stat.DruidSpringAopConfiguration;
import com.alibaba.druid.spring.boot.autoconfigure.stat.DruidStatViewServletConfiguration;
import com.alibaba.druid.spring.boot.autoconfigure.stat.DruidWebStatFilterConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
/**
 * @authoer:majinzhong
 * @Date: 2022/11/11
 * @description:
 */
@Configuration
@ConditionalOnClass(DruidDataSourceAutoConfigure.class)
@EnableConfigurationProperties({DruidStatProperties.class})
@Import({
        DruidSpringAopConfiguration.class,
        DruidStatViewServletConfiguration.class,
        DruidWebStatFilterConfiguration.class,
        DruidFilterConfiguration.class})
public class DruidShardingJdbcDataSourceConfiguration {
}</code></pre>
<h1 style="" id="%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB">读写分离</h1>
<p style=""></p>
<pre><code>        rules:
            #配置读写分离
            replica-query:
                data-sources:
                    db:
                        # 读写分离类型，比如：Static，Dynamic，动态方式需要配合高可用功能，具体参考下方链接
                        # https://blog.csdn.net/ShardingSphere/article/details/123243843
                        type: Static
                        #主数据源名称
                        primary-data-source-name: master
                        #从数据源名称，多个用逗号分隔
                        replica-data-source-names: slave0,slave1
                        #负载均衡算法名称
                        load-balancer-name: round-robin
                        props:
                            # 注意，如果接口有事务，读写分离不生效，默认全部使用主库，为了保证数据一致性
                            write-data-source-name: master
                            read-data-source-names: slave0,slave1
                load-balancers:
                    round-robin:
                        #负载均衡算法配置，一共三种一种是 RANDOM（随机），一种是 ROUND_ROBIN（轮询），一种是 WEIGHT（权重）
                        type: ROUND_ROBIN
                        props:
                            #负载均衡算法属性配置
                            workId: 1
        props:
            #打印sql
            sql-show: true</code></pre>
<h1 style="" id="%E9%85%8D%E7%BD%AE%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8">配置分库分表</h1>
<p style=""></p>
<pre><code>rules:
            sharding:
                #                default-data-source-name: db0
                # 分布式序列算法配置
                key-generators:
                    #　分布式序列算法（雪花算法：SNOWFLAKE, UUID:UUID(UUID没有props配置)）
                    snowflake:
                        type: SNOWFLAKE
                        props:
                            worker-id: 123
                # 配置user_info表
                tables:
                    foundation_place:
                        # 主键ＩＤ生成策略
                        key-generate-strategy:
                            #　分布式序列算法（雪花算法：SNOWFLAKE, UUID:UUID(UUID没有props配置)）
                            key-generator-name: snowflake
                            column: id
                        # 配置分表策略
                        actual-data-nodes: db$-&gt;{0..2}.foundation_place
#                        # 分库策略
                        database-strategy:
                            standard:
                                sharding-column: id
                                sharding-algorithm-name: database-inline
 
                #                        #　单分片键的标准分片
                #                        table-strategy:
                #                            standard:
                #                                sharding-column: id
                #                                sharding-algorithm-name: table-inline
                sharding-algorithms:
                    # 通过id取模的方式确定数据落在哪个库
                    database-inline:
                        type: INLINE
                        props:
                            algorithm-expression: db$-&gt;{id % 3}
        #                    # 通过id取模的方式确定数据落在哪个表
        #                    table-inline:
        #                        type: INLINE
        #                        props:
        #                            algorithm-expression: foundation_place
        #                enabled: true
        props:
            #打印sql
            sql-show: true</code></pre>
<h1 style="" id="%E5%AE%8C%E6%95%B4%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">完整的配置文件</h1>
<pre><code>## 数据源配置
spring:
    autoconfigure:
        exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
            stat-view-servlet:
                enabled: true
                # 设置白名单，不填则允许所有访问
                allow:
                url-pattern: /druid/*
                loginUsername: admin
                loginPassword: 123456
            web-stat-filter:
                enabled: true
            filter:
                stat:
                    enabled: true
                    # 慢SQL记录
                    log-slow-sql: true
                    slow-sql-millis: 1000
                    merge-sql: true
                wall:
                    config:
                        multi-statement-allow: true
    shardingsphere:
        datasource:
            common:
                type: com.alibaba.druid.pool.DruidDataSource
                driver-class-name: com.mysql.cj.jdbc.Driver
                # 初始连接数
                initial-size: 6
                # 最小连接池数量
                min-idle: 3
                # 最大连接池数量
                maxActive: 20
                # 配置获取连接等待超时的时间
                maxWait: 60000
                # 配置间隔多久才进行一次检测，检测需要关闭的空闲连接，单位是毫秒
                timeBetweenEvictionRunsMillis: 60000
                # 配置一个连接在池中最小生存的时间，单位是毫秒
                minEvictableIdleTimeMillis: 300000
                # 配置一个连接在池中最大生存的时间，单位是毫秒
                maxEvictableIdleTimeMillis: 900000
                #Oracle需要打开注释
                validationQuery: SELECT 1 FROM DUAL
                testWhileIdle: true
                testOnBorrow: false
                testOnReturn: false
                # 打开PSCache，并且指定每个连接上PSCache的大小
                poolPreparedStatements: true
                maxPoolPreparedStatementPerConnectionSize: 20
                # 配置监控统计拦截的filters，去掉后监控界面sql无法统计，'wall'用于防火墙
                filters: stat,wall,slf4j
                # 通过connectProperties属性来打开mergeSql功能；慢SQL记录
                connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
                wall:
                    multi-statement-allow: true
            #配置分库分表的数据源
            names: db0,db1,db2
            #配置数据库连接地址
            db0:
                url: jdbc:mysql://127.0.0.1:3306/ry_vue?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT%2B8
                username: root
                password: 123456
            db1:
                url: jdbc:mysql://127.0.0.1:3306/ry?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT%2B8
                username: root
                password: 123456
            db2:
                url: jdbc:mysql://127.0.0.1:3306/ry1?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT%2B8
                username: root
                password: 123456
        rules:
            #配置读写分离
            replica-query:
                data-sources:
                    db:
                        # 读写分离类型，比如：Static，Dynamic，动态方式需要配合高可用功能，具体参考下方链接
                        # https://blog.csdn.net/ShardingSphere/article/details/123243843
                        type: Static
                        #主数据源名称
                        primary-data-source-name: master
                        #从数据源名称，多个用逗号分隔
                        replica-data-source-names: slave0,slave1
                        #负载均衡算法名称
                        load-balancer-name: round-robin
                        props:
                            # 注意，如果接口有事务，读写分离不生效，默认全部使用主库，为了保证数据一致性
                            write-data-source-name: master
                            read-data-source-names: slave0,slave1
                load-balancers:
                    round-robin:
                        #负载均衡算法配置，一共三种一种是 RANDOM（随机），一种是 ROUND_ROBIN（轮询），一种是 WEIGHT（权重）
                        type: ROUND_ROBIN
                        props:
                            #负载均衡算法属性配置
                            workId: 1
            sharding:
                #                default-data-source-name: db0
                # 分布式序列算法配置
                key-generators:
                    #　分布式序列算法（雪花算法：SNOWFLAKE, UUID:UUID(UUID没有props配置)）
                    snowflake:
                        type: SNOWFLAKE
                        props:
                            worker-id: 123
                # 配置user_info表
                tables:
                    foundation_place:
                        # 主键ＩＤ生成策略
                        key-generate-strategy:
                            #　分布式序列算法（雪花算法：SNOWFLAKE, UUID:UUID(UUID没有props配置)）
                            key-generator-name: snowflake
                            column: id
                        # 配置分表策略
                        actual-data-nodes: db$-&gt;{0..2}.foundation_place
#                        # 分库策略
                        database-strategy:
                            standard:
                                sharding-column: id
                                sharding-algorithm-name: database-inline
                #                        #　单分片键的标准分片
                #                        table-strategy:
                #                            standard:
                #                                sharding-column: id
                #                                sharding-algorithm-name: table-inline
                sharding-algorithms:
                    # 通过id取模的方式确定数据落在哪个库
                    database-inline:
                        type: INLINE
                        props:
                            algorithm-expression: db$-&gt;{id % 3}
        #                    # 通过id取模的方式确定数据落在哪个表
        #                    table-inline:
        #                        type: INLINE
        #                        props:
        #                            algorithm-expression: foundation_place
        #                enabled: true
        props:
            #打印sql
            sql-show: true</code></pre>
<blockquote>
 <p style="">参考资料</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/m0_71817461/article/details/127967118">shardingsphere 5.X结合druid实现读写分离，分库分表</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://blog.csdn.net/qq_45752401/article/details/122105085">Springboot整合shardingsphere</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702297993629</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmysql.gif&amp;size=m" type="image/jpeg" length="0"/><category>Mysql技术</category><pubDate>Mon, 11 Dec 2023 12:33:00 GMT</pubDate></item><item><title><![CDATA[Nginx 的五大应用场景]]></title><link>https://xiaoming728.com/archives/1702297930752</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Nginx%20%E7%9A%84%E4%BA%94%E5%A4%A7%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF&amp;url=/archives/1702297930752" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">来源：CSDN-<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="https://monday.blog.csdn.net/">vbirdbest</a></p>
 <p style="">日期：<span style="font-size: 14px; color: rgb(153, 154, 170)">2018-07-04 15:18:23</span></p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">https://blog.csdn.net/vbirdbest/article/details/80913319</a></p>
</blockquote>
<h2 style="" id="%E4%B8%80%E3%80%81http%E6%9C%8D%E5%8A%A1%E5%99%A8">一、HTTP服务器</h2>
<p style="">Nginx本身也是一个静态资源的服务器，当只有静态资源的时候，就可以使用Nginx来做服务器，如果一个网站只是静态页面的话，那么就可以通过这种方式来实现部署。</p>
<p style=""></p>
<p style="">1、 首先在文档根目录Docroot(/usr/local/var/www)下创建html目录, 然后在html中放一个test.html;</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-gyrj.png&amp;size=m" style="display: inline-block"></p>
<p style="">2、 配置nginx.conf中的server</p>
<pre><code>user mengday staff;

http {
    server {
        listen       80;
        server_name  localhost;
        client_max_body_size 1024M;

        # 默认location
        location / {
            root   /usr/local/var/www/html;
            index  index.html index.htm;
        }
    }
}</code></pre>
<p style="">3、访问测试</p>
<ul>
 <li>
  <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">http://localhost/</a> 指向/usr/local/var/www/index.html, index.html是安装nginx自带的html</p></li>
 <li>
  <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">http://localhost/test.html</a> 指向/usr/local/var/www/html/test.html</p></li>
</ul>
<p style="">注意：如果访问图片出现403 Forbidden错误，可能是因为nginx.conf 的第一行user配置不对，默认是#user nobody;是注释的，linux下改成user root; macos下改成user 用户名 所在组; 然后重新加载配置文件或者重启，再试一下就可以了， 用户名可以通过who am i 命令来查看。</p>
<p style=""></p>
<p style="">4、指令简介</p>
<ul>
 <li>
  <p style="">server : 用于定义服务，http中可以有多个server块</p></li>
 <li>
  <p style="">listen : 指定服务器侦听请求的IP地址和端口，如果省略地址，服务器将侦听所有地址，如果省略端口，则使用标准端口</p></li>
 <li>
  <p style="">server_name : 服务名称，用于配置域名</p></li>
 <li>
  <p style="">location : 用于配置映射路径uri对应的配置，一个server中可以有多个location, location后面跟一个uri,可以是一个正则表达式, / 表示匹配任意路径, 当客户端访问的路径满足这个uri时就会执行location块里面的代码</p></li>
 <li>
  <p style="">root : 根路径，当访问<a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">http://localhost/test.html，“/test.html”会匹配到”/”uri</a>, 找到root为/usr/local/var/www/html，用户访问的资源物理地址=root + uri = /usr/local/var/www/html + /test.html=/usr/local/var/www/html/test.html</p></li>
 <li>
  <p style="">index : 设置首页，当只访问server_name时后面不跟任何路径是不走root直接走index指令的；如果访问路径中没有指定具体的文件，则返回index设置的资源，如果访问<a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">http://localhost/html/</a> 则默认返回index.html</p></li>
</ul>
<p style=""></p>
<p style="">5、location uri正则表达式</p>
<pre><code>. ：匹配除换行符以外的任意字符
? ：重复0次或1次
+ ：重复1次或更多次
* ：重复0次或更多次
\d ：匹配数字
^ ：匹配字符串的开始
$ ：匹配字符串的结束
{n} ：重复n次
{n,} ：重复n次或更多次
[c] ：匹配单个字符c
[a-z] ：匹配a-z小写字母的任意一个
(a|b|c) : 属线表示匹配任意一种情况，每种情况使用竖线分隔，一般使用小括号括括住，匹配符合a字符 或是b字符 或是c字符的字符串
\ 反斜杠：用于转义特殊字符
小括号()之间匹配的内容，可以在后面通过$1来引用，$2表示的是前面第二个()里的内容。正则里面容易让人困惑的是\转义特殊字符。</code></pre>
<h2 style="" id="%E4%BA%8C%E3%80%81%E9%9D%99%E6%80%81%E6%9C%8D%E5%8A%A1%E5%99%A8">二、静态服务器</h2>
<p style="">在公司中经常会遇到静态服务器，通常会提供一个上传的功能，其他应用如果需要静态资源就从该静态服务器中获取。</p>
<p style=""></p>
<p style="">1、在/usr/local/var/www 下分别创建images和img目录，分别在每个目录下放一张test.jpg</p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1627629282505-4b52da6b-0ee2-473c-8d20-7001ed74a0d4.png&amp;size=m" width="822" style="display: inline-block"></p>
<pre><code>http {
    server {
        listen       80;
        server_name  localhost;
        
        set $doc_root /usr/local/var/www;

        # 默认location
        location / {
            root   /usr/local/var/www/html;
            index  index.html index.htm;
        }

        location ^~ /images/ {
            root $doc_root;
       }

       location ~* \.(gif|jpg|jpeg|png|bmp|ico|swf|css|js)$ {
           root $doc_root/img;
       }
    }
}</code></pre>
<p style="">自定义变量使用set指令，语法 set 变量名值;引用使用变量名值;引用使用变量名; 这里自定义了doc_root变量。</p>
<p style=""></p>
<p style="">静态服务器location的映射一般有两种方式：</p>
<ul>
 <li>
  <p style="">使用路径，如 /images/ 一般图片都会放在某个图片目录下，</p></li>
 <li>
  <p style="">使用后缀，如 .jpg、.png 等后缀匹配模式</p></li>
</ul>
<p style=""></p>
<p style="">访问<a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">http://localhost/test.jpg</a> 会映射到 $doc_root/img</p>
<p style="">访问<a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">http://localhost/images/test.jpg</a> 当同一个路径满足多个location时，优先匹配优先级高的location，由于^~ 的优先级大于 ~, 所以会走/images/对应的location</p>
<p style=""></p>
<p style="">常见的location路径映射路径有以下几种：</p>
<pre><code>=       进行普通字符精确匹配。也就是完全匹配。
^~         前缀匹配。如果匹配成功，则不再匹配其他location。
~      表示执行一个正则匹配，区分大小写
~*         表示执行一个正则匹配，不区分大小写
/xxx/   常规字符串路径匹配
/       通用匹配，任何请求都会匹配到</code></pre>
<h4 style="" id="location%E4%BC%98%E5%85%88%E7%BA%A7">location优先级</h4>
<p style="">当一个路径匹配多个location时究竟哪个location能匹配到时有优先级顺序的，而优先级的顺序于location值的表达式类型有关，和在配置文件中的先后顺序无关。相同类型的表达式，字符串长的会优先匹配。</p>
<p style=""></p>
<p style="">以下是按优先级排列说明：</p>
<ol>
 <li>
  <p style="">等号类型（=）的优先级最高。一旦匹配成功，则不再查找其他匹配项，停止搜索。</p></li>
 <li>
  <p style="">^~类型表达式，不属于正则表达式。一旦匹配成功，则不再查找其他匹配项，停止搜索。</p></li>
 <li>
  <p style="">正则表达式类型（~ ~*）的优先级次之。如果有多个location的正则能匹配的话，则使用正则表达式最长的那个。</p></li>
 <li>
  <p style="">常规字符串匹配类型。按前缀匹配。</p></li>
 <li>
  <p style="">/ 通用匹配，如果没有匹配到，就匹配通用的</p></li>
</ol>
<p style=""></p>
<p style="">优先级搜索问题：不同类型的location映射决定是否继续向下搜索</p>
<ul>
 <li>
  <p style="">等号类型、^~类型：一旦匹配上就停止搜索了，不会再匹配其他location了</p></li>
 <li>
  <p style="">正则表达式类型(~ ~*）,常规字符串匹配类型/xxx/ : 匹配到之后，还会继续搜索其他其它location，直到找到优先级最高的，或者找到第一种情况而停止搜索</p></li>
</ul>
<pre><code>location优先级从高到底：
(location =) &gt; (location 完整路径) &gt; (location ^~ 路径) &gt; (location ~,~* 正则顺序) &gt; (location 部分起始路径) &gt; (/)</code></pre>
<pre><code>location = / {
    # 精确匹配/，主机名后面不能带任何字符串 /
    [ configuration A ]
}
location / {
    # 匹配所有以 / 开头的请求。
    # 但是如果有更长的同类型的表达式，则选择更长的表达式。
    # 如果有正则表达式可以匹配，则优先匹配正则表达式。
    [ configuration B ]
}
location /documents/ {
    # 匹配所有以 /documents/ 开头的请求，匹配符合以后，还要继续往下搜索。
    # 但是如果有更长的同类型的表达式，则选择更长的表达式。
    # 如果有正则表达式可以匹配，则优先匹配正则表达式。
    [ configuration C ]
}
location ^~ /images/ {
    # 匹配所有以 /images/ 开头的表达式，如果匹配成功，则停止匹配查找，停止搜索。
    # 所以，即便有符合的正则表达式location，也不会被使用
    [ configuration D ]
}
location ~* \.(gif|jpg|jpeg)$ {
    # 匹配所有以 gif jpg jpeg结尾的请求。
    # 但是 以 /images/开头的请求，将使用 Configuration D，D具有更高的优先级
    [ configuration E ]
}
location /images/ {
    # 字符匹配到 /images/，还会继续往下搜索
    [ configuration F ]
}
location = /test.htm {
    root   /usr/local/var/www/htm;
    index  index.htm;
}
注意：location的优先级与location配置的位置无关</code></pre>
<h2 style="" id="%E4%B8%89%E3%80%81%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86">三、反向代理</h2>
<p style="">反向代理应该是Nginx使用最多的功能了，反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求，然后将请求转发给内部网络上的服务器，并将从服务器上得到的结果返回给internet上请求连接的客户端，此时代理服务器对外就表现为一个反向代理服务器。</p>
<p style="">简单来说就是真实的服务器不能直接被外部网络访问，所以需要一台代理服务器，而代理服务器能被外部网络访问的同时又跟真实服务器在同一个网络环境，当然也可能是同一台服务器，端口不同而已。</p>
<p style="">反向代理通过proxy_pass指令来实现。</p>
<p style="">启动一个Java Web项目，端口号为8081</p>
<pre><code>server {
    listen       80;
    server_name  localhost;

    location / {
        proxy_pass http://localhost:8081;
        proxy_set_header Host $host:$server_port;
        # 设置用户ip地址
         proxy_set_header X-Forwarded-For $remote_addr;
         # 当请求服务器出错去寻找其他服务器
         proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; 
    }
}   </code></pre>
<p style="">当我们访问<a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">localhost</a>的时候，就相当于访问 <a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">localhost:8081</a>了</p>
<h2 style="" id="%E5%9B%9B%E3%80%81%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1">四、负载均衡</h2>
<p style="">负载均衡也是Nginx常用的一个功能，负载均衡其意思就是分摊到多个操作单元上进行执行，例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等，从而共同完成工作任务。</p>
<p style="">简单而言就是当有2台或以上服务器时，根据规则随机的将请求分发到指定的服务器上处理，负载均衡配置一般都需要同时配置反向代理，通过反向代理跳转到负载均衡。而Nginx目前支持自带3种负载均衡策略，还有2种常用的第三方策略。</p>
<p style="">负载均衡通过upstream指令来实现。</p>
<h4 style="" id="1.-rr(round-robin-%3A%E8%BD%AE%E8%AF%A2-%E9%BB%98%E8%AE%A4)">1. RR(round robin :轮询 默认)</h4>
<p style="">每个请求按时间顺序逐一分配到不同的后端服务器，也就是说第一次请求分配到第一台服务器上，第二次请求分配到第二台服务器上，如果只有两台服务器，第三次请求继续分配到第一台上，这样循环轮询下去，也就是服务器接收请求的比例是 1:1， 如果后端服务器down掉，能自动剔除。轮询是默认配置，不需要太多的配置</p>
<p style="">同一个项目分别使用8081和8082端口启动项目</p>
<pre><code>upstream web_servers {  
   server localhost:8081;  
   server localhost:8082;  
}

server {
    listen       80;
    server_name  localhost;
    #access_log  logs/host.access.log  main;

    location / {
        proxy_pass http://web_servers;
        # 必须指定Header Host
        proxy_set_header Host $host:$server_port;
    }
 }</code></pre>
<p style="">访问地址仍然可以获得响应 <a target="_blank" rel="noopener noreferrer nofollow" href="https://monday.blog.csdn.net/">http://localhost/api/user/login?username=zhangsan&amp;password=111111</a> ，这种方式是轮询的</p>
<h4 style="" id="2.-%E6%9D%83%E9%87%8D">2. 权重</h4>
<p style="">指定轮询几率，weight和访问比率成正比, 也就是服务器接收请求的比例就是各自配置的weight的比例，用于后端服务器性能不均的情况,比如服务器性能差点就少接收点请求，服务器性能好点就多处理点请求。</p>
<pre><code>upstream test {
    server localhost:8081 weight=1;
    server localhost:8082 weight=3;
    server localhost:8083 weight=4 backup;
}</code></pre>
<p style="">示例是4次请求只有一次被分配到8081上，其他3次分配到8082上。backup是指热备，只有当8081和8082都宕机的情况下才走8083</p>
<h4 style="" id="3.-ip_hash">3. ip_hash</h4>
<p style="">上面的2种方式都有一个问题，那就是下一个请求来的时候请求可能分发到另外一个服务器，当我们的程序不是无状态的时候(采用了session保存数据)，这时候就有一个很大的很问题了，比如把登录信息保存到了session中，那么跳转到另外一台服务器的时候就需要重新登录了，所以很多时候我们需要一个客户只访问一个服务器，那么就需要用iphash了，iphash的每个请求按访问ip的hash结果分配，这样每个访客固定访问一个后端服务器，可以解决session的问题。</p>
<pre><code>upstream test {
    ip_hash;
    server localhost:8080;
    server localhost:8081;
}</code></pre>
<h4 style="" id="4.-fair(%E7%AC%AC%E4%B8%89%E6%96%B9)">4. fair(第三方)</h4>
<p style="">按后端服务器的响应时间来分配请求，响应时间短的优先分配。这个配置是为了更快的给用户响应</p>
<pre><code>upstream backend {
    fair;
    server localhost:8080;
    server localhost:8081;
}</code></pre>
<h4 style="" id="5.-url_hash(%E7%AC%AC%E4%B8%89%E6%96%B9)">5. url_hash(第三方)</h4>
<p style="">按访问url的hash结果来分配请求，使每个url定向到同一个后端服务器，后端服务器为缓存时比较有效。在upstream中加入hash语句，server语句中不能写入weight等其他的参数，hash_method是使用的hash算法</p>
<pre><code>upstream backend {
    hash $request_uri;
    hash_method crc32;
    server localhost:8080;
    server localhost:8081;
}</code></pre>
<p style="">以上5种负载均衡各自适用不同情况下使用，所以可以根据实际情况选择使用哪种策略模式,不过fair和url_hash需要安装第三方模块才能使用。</p>
<h2 style="" id="%E4%BA%94%E3%80%81%E5%8A%A8%E9%9D%99%E5%88%86%E7%A6%BB">五、动静分离</h2>
<p style="">动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来，动静资源做好了拆分以后，我们就可以根据静态资源的特点将其做缓存操作，这就是网站静态化处理的核心思路。</p>
<pre><code>upstream web_servers {  
       server localhost:8081;  
       server localhost:8082;  
}

server {
    listen       80;
    server_name  localhost;

    set $doc_root /usr/local/var/www;

    location ~* \.(gif|jpg|jpeg|png|bmp|ico|swf|css|js)$ {
       root $doc_root/img;
    }

    location / {
        proxy_pass http://web_servers;
        # 必须指定Header Host
        proxy_set_header Host $host:$server_port;
    }

    error_page 500 502 503 504  /50x.html;  
    location = /50x.html {  
        root $doc_root;
    }

 }</code></pre>
<h2 style="" id="%E5%85%AD%E3%80%81%E5%85%B6%E4%BB%96">六、其他</h2>
<h4 style="" id="1.return%E6%8C%87%E4%BB%A4">1.return指令</h4>
<p style="">返回http状态码 和 可选的第二个参数可以是重定向的URL</p>
<pre><code>location /permanently/moved/url {
    return 301 http://www.example.com/moved/here;
}</code></pre>
<h4 style="" id="2.-rewrite%E6%8C%87%E4%BB%A4">2. rewrite指令</h4>
<p style="">重写URI请求 rewrite，通过使用rewrite指令在请求处理期间多次修改请求URI，该指令具有一个可选参数和两个必需参数。</p>
<p style="">第一个(必需)参数是请求URI必须匹配的正则表达式。</p>
<p style="">第二个参数是用于替换匹配URI的URI。</p>
<p style="">可选的第三个参数是可以停止进一步重写指令的处理或发送重定向(代码301或302)的标志</p>
<pre><code>location /users/ {
    rewrite ^/users/(.*)$ /show?user=$1 break;
}</code></pre>
<h4 style="" id="3.-error_page%E6%8C%87%E4%BB%A4">3. error_page指令</h4>
<p style="">使用error_page指令，您可以配置NGINX返回自定义页面以及错误代码，替换响应中的其他错误代码，或将浏览器重定向到其他URI。在以下示例中，error_page指令指定要返回404页面错误代码的页面(/404.html)。</p>
<pre><code>error_page 404 /404.html;</code></pre>
<h4 style="" id="4.-%E6%97%A5%E5%BF%97">4. 日志</h4>
<p style="">访问日志：需要开启压缩 gzip on; 否则不生成日志文件，打开log_format、access_log注释</p>
<pre><code>log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

access_log  /usr/local/etc/nginx/logs/host.access.log  main;

gzip  on;</code></pre>
<h4 style="" id="5.-deny-%E6%8C%87%E4%BB%A4">5. deny 指令</h4>
<pre><code># 禁止访问某个目录
location ~* \.(txt|doc)${
    root $doc_root;
    deny all;
}   </code></pre>
<h4 style="" id="6.-%E5%86%85%E7%BD%AE%E5%8F%98%E9%87%8F">6. 内置变量</h4>
<p style="">nginx的配置文件中可以使用的内置变量以美元符$开始，也有人叫全局变量。其中，部分预定义的变量的值是可以改变的。</p>
<pre><code>$args ：#这个变量等于请求行中的参数，同$query_string
$content_length ：请求头中的Content-length字段。
$content_type ：请求头中的Content-Type字段。
$document_root ：当前请求在root指令中指定的值。
$host ：请求主机头字段，否则为服务器名称。
$http_user_agent ：客户端agent信息
$http_cookie ：客户端cookie信息
$limit_rate ：这个变量可以限制连接速率。
$request_method ：客户端请求的动作，通常为GET或POST。
$remote_addr ：客户端的IP地址。
$remote_port ：客户端的端口。
$remote_user ：已经经过Auth Basic Module验证的用户名。
$request_filename ：当前请求的文件路径，由root或alias指令与URI请求生成。
$scheme ：HTTP方法（如http，https）。
$server_protocol ：请求使用的协议，通常是HTTP/1.0或HTTP/1.1。
$server_addr ：服务器地址，在完成一次系统调用后可以确定这个值。
$server_name ：服务器名称。
$server_port ：请求到达服务器的端口号。
$request_uri ：包含请求参数的原始URI，不包含主机名，如：”/foo/bar.php?arg=baz”。
$uri ：不带请求参数的当前URI，$uri不包含主机名，如”/foo/bar.html”。
$document_uri ：与$uri相同</code></pre>
<p style=""><br><br></p>]]></description><guid isPermaLink="false">/archives/1702297930752</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fnginx.png&amp;size=m" type="image/jpeg" length="0"/><category>Nginx技术</category><pubDate>Mon, 11 Dec 2023 12:32:00 GMT</pubDate></item><item><title><![CDATA[Git如何管理代码分支]]></title><link>https://xiaoming728.com/archives/1702297771251</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Git%E5%A6%82%E4%BD%95%E7%AE%A1%E7%90%86%E4%BB%A3%E7%A0%81%E5%88%86%E6%94%AF&amp;url=/archives/1702297771251" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 15px">来源：微信公众号-Java爱好者</span></p>
<p style=""><span style="font-size: 15px">时间： 2021-05-21</span></p>
<p style=""><span style="font-size: 15px">连接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzI1MTA3Mzk4Mg==&amp;amp;mid=2651026507&amp;amp;idx=1&amp;amp;sn=c8211cda505bded764c169dc7eedc5fa&amp;amp;chksm=f20f509ac578d98c6f44add31b0dab2ae3a387247e2d7fb8cebb299748f882bad09b8d636331&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=05223q9ummsjuFVbLUBzhiOv&amp;amp;sharer_sharetime=1621639688459&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px">https://mp.weixin.qq.com/s?__biz=MzI1MTA3Mzk4Mg==&amp;mid=2651026507&amp;idx=1&amp;sn=c8211cda505bded764c169dc7eedc5fa&amp;chksm=f20f509ac578d98c6f44add31b0dab2ae3a387247e2d7fb8cebb299748f882bad09b8d636331&amp;mpshare=1&amp;scene=23&amp;srcid=05223q9ummsjuFVbLUBzhiOv&amp;sharer_sharetime=1621639688459&amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd</span></a></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">Git作为现在主流的版本控制工具，但是如何在软件开发过程中进行合理的分支管理是一个见仁见智的问题。</span></p>
<p style=""><span style="font-size: 15px">接下来我会对比下现有的几种比较普遍的分支管理方式和之前在阿里时候使用Aone的区别。</span></p>
<h1 style="" id="git-flow">Git Flow</h1>
<p style=""><span style="font-size: 15px">先看一张图片，这张图片来自Vincent在2010年提出的方案，完美的诠释了Git Flow的工作模式。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-muzu.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">作为已经提出了10多年的模式，Git Flow相对来说还算是比较简单的。</span></p>
<p style=""><span style="font-size: 15px">稳定的分支就两个：develop和master，这两个分支是不会被删除的，master对应稳定的版本，develop则是用于日常开发的稳定版本。</span></p>
<p style=""><span style="font-size: 15px">其他的feature、release、hotfix分支都是用完即删。</span></p>
<p style=""><span style="font-size: 15px">feature分支是每个人的开发分支，有新的需求都应该基于develop拉出feature分支进行开发。</span></p>
<p style=""><span style="font-size: 15px">release分支则是用于测试和发布的分支。</span></p>
<p style=""><span style="font-size: 15px">hotfix用于紧急的bug修复。</span></p>
<p style=""></p>
<p style=""><span style="font-size: 15px">开发流程如下：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 15px">最开始的时候我们创建好了master分支，然后基于master分支创建出了develop分支</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">然后A和B同时基于某个版本的develop分支拉出代码进行开发，分支分别叫做feature-A和feature-B</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">如果开发过程中需要修复bug上线，那么就从master拉个分支出来，命名为hotfix-xxx进行修复，修复完成之后合并到develop和master，然后hotfix分支删除</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">然后A代码撸的比较快，先一步完成了开发，feature-A分支的代码就合并到develop，feature分支被删除，然后我们基于develop新建一个release-A分支进行测试</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">测试过程中如果发现问题那么我们就在release分支修复，把修复的代码合并到develop去</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">release-A一旦测试完成上线，就把代码合并到master和develop，release分支被删除</span></p></li>
 <li>
  <p style=""><span style="font-size: 15px">这时候B总算把需求开发完了，然后也按照合并到develop，再新建release-B，合并到master和develop的过程来一遍</span></p></li>
</ul>
<p style=""></p>
<p style=""><span style="font-size: 15px">对于实际应用也比较简单，对于Mac我们可以直接用最方便的方式进行安装。</span></p>
<p style=""><span style="font-size: 15px">首先，安装Git Flow，brew install git-flow-avh，安装好之后执行git flow init就会进行初始化，可以输入你的master和develop分支名字，然后设置其他几个默认分支的前缀。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-pwcq.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">然后执行git flow feature start irving就可以初始化创建一个feature分支进行开发，默认我们可以看到是基于develop来创建的。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-bvou.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">如果开发完成，我们执行命令git flow feature finish irving，然后我们的开发分支就自动合并到了develop，并且开发分支已经被删除。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-wwcx.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">接着我们的分支需要提测和发布，执行命令git flow release start irving，如果修复bug就直接在这上面修复。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-eusf.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">测试完成之后，执行命令git flow release finish irving，代码将会被合并到master和develop，然后分支被删除。<br></span><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-xust.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">原理和实现方式都说了，那么这个模式其实还是一样的问题，就是他比较适合固定版本的迭代开发，对于互联网不要脸的每天都要发版，每天10几个需求都要上线来说未免太难了。</span></p>
<p style=""><span style="font-size: 15px">develop分支我今天有10个需求，8个要上线，2个不上，代码还有先后顺序依赖之类的，这就没法玩好不好，但是他提供了一个比较好的规范和思路。</span></p>
<h1 style="" id="github-flow">Github Flow</h1>
<p style=""><span style="font-size: 15px">Github Flow可以说非常简单了，它的提出是在2011年，主要就是针对Git Flow。</span></p>
<p style=""><span style="font-size: 15px">它就是基于master分支拉一个分支出来开发，然后可以在新的分支中进行开发，完成之后提交pull request，如果接受之后就合并代码部署了。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fwebp%2F550960%2F1621932265287-870af9a6-24b7-4127-9096-5ed290f61820.webp&amp;size=m" width="540" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">具体可以看官方介绍。</span></p>
<p style=""><span style="font-size: 15px">这个方式简单是简单，但是在很多公司的业务开发过程中一般都有开发、测试、预发、生产几个环境，没有强有力的工具来支撑，我认为很难用这种简单的模式来实现管理。</span></p>
<p style=""><span style="font-size: 15px">我觉得这种模式特别适合小团队，人少，需求少，那就很容易了。</span></p>
<h1 style="" id="trunk-based">Trunk-Based</h1>
<p style=""><span style="font-size: 15px">这个模型提出的时间更晚一点，是在2013年Paul Hammant提出的方案。</span></p>
<p style=""><span style="font-size: 15px">看图基本就能明白，这不就是SVN的模式嘛，主干trunk开发，拉出新的分支进行开发部署、修复BUG。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-avuw.png&amp;size=m" style="display: inline-block"></p>
<h1 style="" id="%E7%94%A8%E8%BF%87%E7%9A%84%E6%96%B9%E6%A1%88">用过的方案</h1>
<p style=""><span style="font-size: 15px">我们之前用过一个方案，和Git Flow比较类似，但是不依赖工具的支持，更多的是依靠团队本身的约定和规范。</span></p>
<p style=""><span style="font-size: 15px">对于开发、测试、预发、生产分别使用分支develop、test、release、master分支，其中master分支作为稳定分支，不能直接提交代码，同时这几个分支是固定唯一的分支。</span></p>
<p style=""><span style="font-size: 15px">首先开发阶段，大家都需要基于master创建最新的功能开发分支，命名为feature/xxx。</span></p>
<p style=""><span style="font-size: 15px">如果需要发布到开发环境，所有人的代码都需要合并到develop，并且只能用develop分支进行发布开发环境。</span></p>
<p style=""><span style="font-size: 15px">如果开发完成，需要提测的分支合并到test分支，那些还在开发阶段的就在develop好了。</span></p>
<p style=""><span style="font-size: 15px">测试完成之后需要发布预发(当然叫灰度、uat都行)，就把代码合并到release进行发布。</span></p>
<p style=""><span style="font-size: 15px">发布完成之后，代码自动合并到master。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-ovej.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">这样做的好处就是首先规范了分支的开发和管理，开发中不会产生太多的纠纷，而且对于同时有多个需求开发并且不同时间上线都可以做到很好的管理。</span></p>
<p style=""><span style="font-size: 15px">缺点就是一个项目多个需求开发上线，需要合并多次代码，从develop、teest到release都要分别合并一次代码并且解决冲突。</span></p>
<p style=""><span style="font-size: 15px">总的来说，这只是一个基于团队的规范，对于环境和中间件CI/CD能力没有太多的要求，可以简单的套用在各个公司的场景。</span></p>
<h1 style="" id="%E9%98%BF%E9%87%8C%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88">阿里的解决方案</h1>
<p style=""><span style="font-size: 15px">阿里的解决方案基本上可以说是上面几个模式的一个结合体，称作Aone Flow，可能因为工具本身就叫做Aone吧。</span></p>
<p style=""><span style="font-size: 15px">分支只有3个，master分支、功能分支feature、发布分支release，其中release分支基本上是不需要开发人员来参与管理的。</span></p>
<p style=""><span style="font-size: 15px">首先，分支的创建也都是基于master，如果有需求，首先创建一个feature分支，部署前Aone会自动合并master代码，所以不用操心代码没有合并的问题，如果存在冲突需要手动解决，反之则就自动生成一个新的分支用于部署，这个分支就是release分支。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-excz.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span style="font-size: 15px">这个分支可以一直用来发布日常(理解成开发或者测试环境合体)、预发和生产环境。</span></p>
<p style=""><span style="font-size: 15px">那如果多个需求同时在开发有冲突不需要合并代码吗？首先，Aone部署可以同时部署多个分支，选择部署多个功能分支代码会自动合并，如果存在冲突需要手动解决，另外可以单独建立一个环境来部署，互不影响，这个功能真是蛮吊的。</span></p>
<p style=""><span style="font-size: 15px">这个规则对于预发和生产环境也是同理。</span></p>
<p style=""><span style="font-size: 15px">整个开发过程，我们不需要管各种分支，只需要一个feature功能分支用于发布各个环境，最终发布完成之后代码自动合并到master主分支（可选项，也可以不合并）。</span></p>
<p style=""><span style="font-size: 15px">整个模式看下来就是集成了各个模式的特点，参考了Git Flow的分支的特点，只不过其他的分支不用开发人员关心，基于主干master拉出分支开发，自动合并又像是TrunkBased的做法，最终整个流程对于开发人员体验下来又像是更简化版的Github Flow了。</span></p>
<p style=""></p>
<blockquote>
 <p style=""><span style="font-size: 15px">文章参考：</span></p>
 <p style=""><span style="font-size: 15px">Git 代码分支模型(1)：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzI1MTA3Mzk4Mg==&amp;amp;mid=2651026507&amp;amp;idx=1&amp;amp;sn=c8211cda505bded764c169dc7eedc5fa&amp;amp;chksm=f20f509ac578d98c6f44add31b0dab2ae3a387247e2d7fb8cebb299748f882bad09b8d636331&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=05223q9ummsjuFVbLUBzhiOv&amp;amp;sharer_sharetime=1621639688459&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px">http://www.brofive.org/?p=2165</span></a></p>
 <p style=""><span style="font-size: 15px">Git 代码分支模型(2)：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzI1MTA3Mzk4Mg==&amp;amp;mid=2651026507&amp;amp;idx=1&amp;amp;sn=c8211cda505bded764c169dc7eedc5fa&amp;amp;chksm=f20f509ac578d98c6f44add31b0dab2ae3a387247e2d7fb8cebb299748f882bad09b8d636331&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=05223q9ummsjuFVbLUBzhiOv&amp;amp;sharer_sharetime=1621639688459&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px">http://www.brofive.org/?p=2233</span></a></p>
 <p style=""><span style="font-size: 15px">Git 代码分支模型(3)：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzI1MTA3Mzk4Mg==&amp;amp;mid=2651026507&amp;amp;idx=1&amp;amp;sn=c8211cda505bded764c169dc7eedc5fa&amp;amp;chksm=f20f509ac578d98c6f44add31b0dab2ae3a387247e2d7fb8cebb299748f882bad09b8d636331&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=05223q9ummsjuFVbLUBzhiOv&amp;amp;sharer_sharetime=1621639688459&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px">http://www.brofive.org/?p=4352</span></a></p>
 <p style=""><span style="font-size: 15px">阿里如何管理代码分支：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://mp.weixin.qq.com/s?__biz=MzI1MTA3Mzk4Mg==&amp;amp;mid=2651026507&amp;amp;idx=1&amp;amp;sn=c8211cda505bded764c169dc7eedc5fa&amp;amp;chksm=f20f509ac578d98c6f44add31b0dab2ae3a387247e2d7fb8cebb299748f882bad09b8d636331&amp;amp;mpshare=1&amp;amp;scene=23&amp;amp;srcid=05223q9ummsjuFVbLUBzhiOv&amp;amp;sharer_sharetime=1621639688459&amp;amp;sharer_shareid=a942c1cbd91979daf19ca2cc49fbda8d#rd"><span style="font-size: 15px">https://mp.weixin.qq.com/s?__biz=MzAxNDU0MTE0OA==&amp;mid=2661008528&amp;idx=1&amp;sn=748c3b5bdaa28c3c7b3c06614fd69d47&amp;scene=21#wechat_redirect%20%20https://cloud.tencent.com/developer/article/1646937</span></a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702297771251</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FGit.png&amp;size=m" type="image/jpeg" length="0"/><category>Git技术</category><pubDate>Mon, 11 Dec 2023 12:31:00 GMT</pubDate></item><item><title><![CDATA[关于Gitlab权限管理]]></title><link>https://xiaoming728.com/archives/1702297737189</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%85%B3%E4%BA%8EGitlab%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86&amp;url=/archives/1702297737189" width="1" height="1" alt="" style="opacity:0;">
<p style=""><span style="font-size: 13px">公司切入Gitlab来管理代码已经有一年多了，其中遇到很多权限问题，如没有权限clone、没有权限提交代码等等，这里做个总结. 权限分为访问权限和行为权限两个层次.</span></p>
<h2 style="" id="%E8%AE%BF%E9%97%AE%E6%9D%83%E9%99%90---visibility-level">访问权限 - Visibility Level</h2>
<p style=""><span style="font-size: 13px">这个是在建立项目时就需要选定的，主要用于决定哪些人可以访问此项目，包含3种</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 13px">Private - 私有，只有属于该项目成员才有原先查看</span></p></li>
 <li>
  <p style=""><span style="font-size: 13px">Internal - 内部，用个Gitlab账号的人都可以clone</span></p></li>
 <li>
  <p style=""><span style="font-size: 13px">Public - 公开，任何人可以clone</span></p></li>
</ul>
<h2 style="" id="%E8%A1%8C%E4%B8%BA%E6%9D%83%E9%99%90">行为权限</h2>
<p style=""><span style="font-size: 13px">在满足行为权限之前，必须具备访问权限（如果没有访问权限，那就无所谓行为权限了），行为权限是指对该项目进行某些操作，比如提交、创建问题、创建新分支、删除分支、创建标签、删除标签等.</span></p>
<h3 style="" id="%E8%A7%92%E8%89%B2">角色</h3>
<p style=""><span style="font-size: 13px">Gitlab定义了以下几个角色:</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 13px">Guest - 访客</span></p></li>
 <li>
  <p style=""><span style="font-size: 13px">Reporter - 报告者; 可以理解为测试员、产品经理等，一般负责提交issue等</span></p></li>
 <li>
  <p style=""><span style="font-size: 13px">Developer - 开发者; 负责开发</span></p></li>
 <li>
  <p style=""><span style="font-size: 13px">Master - 主人; 一般是组长，负责对Master分支进行维护</span></p></li>
 <li>
  <p style=""><span style="font-size: 13px">Owner - 拥有者; 一般是项目经理</span></p></li>
</ul>
<h3 style="" id="%E6%9D%83%E9%99%90">权限</h3>
<p style=""><span style="font-size: 13px">不同角色，拥有不同权限，下面列出Gitlab各角色权限</span></p>
<h5 style="" id="1.-%E5%B7%A5%E7%A8%8B%E6%9D%83%E9%99%90">1. 工程权限</h5>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">行为</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Guest</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Reporter</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Developer</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Master</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Owner</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">创建issue</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">留言评论</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">更新代码</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">下载工程</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">创建代码片段</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">创建合并请求</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">创建新分支</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">提交代码到非保护分支</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">强制提交到非保护分支</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">移除非保护分支</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">添加tag</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">创建wiki</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">管理issue处理者</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">管理labels</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">创建里程碑</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">添加项目成员</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">提交保护分支</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">使能分支保护</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">修改/移除tag</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">编辑工程</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">添加deploy keys</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">配置hooks</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">切换visibility level</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">切换工程namespace</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">移除工程</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">强制提交保护分支</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">移除保护分支</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
 </tbody>
</table>
<p style=""><span style="font-size: 13px">PS: 关于保护分支的设置，可以进入Settings-&gt;Protected branches进行管理</span></p>
<h5 style="" id="2.-%E7%BB%84%E6%9D%83%E9%99%90">2. 组权限</h5>
<table>
 <tbody>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">行为</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Guest</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Reporter</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Developer</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Master</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">Owner</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">浏览组</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">编辑组</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">创建项目</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">管理组成员</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
  <tr>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">移除组</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">&nbsp;&nbsp;</span></p></td>
   <td colspan="1" rowspan="1" colwidth="100" style="border: 1px solid #d9d9d9">
    <p style=""><span style="font-size: 13px">✓</span></p></td>
  </tr>
 </tbody>
</table>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702297737189</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fopen-graph-gitlab.png&amp;size=m" type="image/jpeg" length="0"/><category>Git技术</category><pubDate>Mon, 11 Dec 2023 12:29:00 GMT</pubDate></item><item><title><![CDATA[常用 Git 命令清单]]></title><link>https://xiaoming728.com/archives/1702297649296</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E5%B8%B8%E7%94%A8%20Git%20%E5%91%BD%E4%BB%A4%E6%B8%85%E5%8D%95&amp;url=/archives/1702297649296" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">来源：<a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://www.ruanyifeng.com/blog/">阮一峰的网络日志</a></p>
 <p style="">作者： <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://www.ruanyifeng.com/">阮一峰</a></p>
 <p style="">日期： <a target="_blank" rel="noopener noreferrer nofollow" class="ne-link" href="http://www.ruanyifeng.com/blog/2015/12/">2015年12月24日</a></p>
 <p style="">链接：<a target="_blank" rel="noopener noreferrer nofollow" href="http://www.ruanyifeng.com/blog/">http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html</a></p>
</blockquote>
<p style=""></p>
<p style=""><span fontsize="" color="rgb(17, 17, 17)" style="color: rgb(17, 17, 17)">我每天使用 Git ，但是很多命令记不住。</span></p>
<p style=""><span fontsize="" color="rgb(17, 17, 17)" style="color: rgb(17, 17, 17)">一般来说，日常使用只要记住下图6个命令，就可以了。但是熟练使用，恐怕要记住60～100个命令。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qhpa.png&amp;size=m" style="display: inline-block"></p>
<p style=""><span fontsize="" color="rgb(17, 17, 17)" style="color: rgb(17, 17, 17)">下面是我整理的常用 Git 命令清单。几个专用名词的译名如下。</span></p>
<ul>
 <li>
  <p style=""><span fontsize="" color="rgb(17, 17, 17)" style="color: rgb(17, 17, 17)">Workspace：工作区</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(17, 17, 17)" style="color: rgb(17, 17, 17)">Index / Stage：暂存区</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(17, 17, 17)" style="color: rgb(17, 17, 17)">Repository：仓库区（或本地仓库）</span></p></li>
 <li>
  <p style=""><span fontsize="" color="rgb(17, 17, 17)" style="color: rgb(17, 17, 17)">Remote：远程仓库</span></p></li>
</ul>
<h2 style="" id="%E4%B8%80%E3%80%81%E6%96%B0%E5%BB%BA%E4%BB%A3%E7%A0%81%E5%BA%93"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">一、新建代码库</span></h2>
<pre><code># 在当前目录新建一个Git代码库
$ git init
# 新建一个目录，将其初始化为Git代码库
$ git init [project-name]
# 下载一个项目和它的整个代码历史
$ git clone [url]</code></pre>
<h2 style="" id="%E4%BA%8C%E3%80%81%E9%85%8D%E7%BD%AE"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">二、配置</span></h2>
<p style=""><span fontsize="" color="rgb(17, 17, 17)" style="color: rgb(17, 17, 17)">Git的设置文件为.gitconfig，它可以在用户主目录下（全局配置），也可以在项目目录下（项目配置）。</span></p>
<pre><code># 显示当前的Git配置
$ git config --list
# 编辑Git配置文件
$ git config -e [--global]
# 设置提交代码时的用户信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]"</code></pre>
<h2 style="" id="%E4%B8%89%E3%80%81%E5%A2%9E%E5%8A%A0%2F%E5%88%A0%E9%99%A4%E6%96%87%E4%BB%B6"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">三、增加/删除文件</span></h2>
<pre><code># 添加指定文件到暂存区
$ git add [file1] [file2] ...
# 添加指定目录到暂存区，包括子目录
$ git add [dir]
# 添加当前目录的所有文件到暂存区
$ git add .
# 添加每个变化前，都会要求确认
# 对于同一个文件的多处变化，可以实现分次提交
$ git add -p
# 删除工作区文件，并且将这次删除放入暂存区
$ git rm [file1] [file2] ...
# 停止追踪指定文件，但该文件会保留在工作区
$ git rm --cached [file]
# 改名文件，并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]</code></pre>
<h2 style="" id="%E5%9B%9B%E3%80%81%E4%BB%A3%E7%A0%81%E6%8F%90%E4%BA%A4"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">四、代码提交</span></h2>
<pre><code># 提交暂存区到仓库区
$ git commit -m [message]
# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]
# 提交工作区自上次commit之后的变化，直接到仓库区
$ git commit -a
# 提交时显示所有diff信息
$ git commit -v
# 使用一次新的commit，替代上一次提交
# 如果代码没有任何新变化，则用来改写上一次commit的提交信息
$ git commit --amend -m [message]
# 重做上一次commit，并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...</code></pre>
<h2 style="" id="%E4%BA%94%E3%80%81%E5%88%86%E6%94%AF"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">五、分支</span></h2>
<pre><code># 列出所有本地分支
$ git branch
# 列出所有远程分支
$ git branch -r
# 列出所有本地分支和远程分支
$ git branch -a
# 新建一个分支，但依然停留在当前分支
$ git branch [branch-name]
# 新建一个分支，并切换到该分支
$ git checkout -b [branch]
# 新建一个分支，指向指定commit
$ git branch [branch] [commit]
# 新建一个分支，与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]
# 切换到指定分支，并更新工作区
$ git checkout [branch-name]
# 切换到上一个分支
$ git checkout -
# 建立追踪关系，在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]
# 合并指定分支到当前分支
$ git merge [branch]
# 选择一个commit，合并进当前分支
$ git cherry-pick [commit]
# 删除分支
$ git branch -d [branch-name]
# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]</code></pre>
<h2 style="" id="%E5%85%AD%E3%80%81%E6%A0%87%E7%AD%BE"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">六、标签</span></h2>
<pre><code># 列出所有tag
$ git tag
# 新建一个tag在当前commit
$ git tag [tag]
# 新建一个tag在指定commit
$ git tag [tag] [commit]
# 删除本地tag
$ git tag -d [tag]
# 删除远程tag
$ git push origin :refs/tags/[tagName]
# 查看tag信息
$ git show [tag]
# 提交指定tag
$ git push [remote] [tag]
# 提交所有tag
$ git push [remote] --tags
# 新建一个分支，指向某个tag
$ git checkout -b [branch] [tag]</code></pre>
<h2 style="" id="%E4%B8%83%E3%80%81%E6%9F%A5%E7%9C%8B%E4%BF%A1%E6%81%AF"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">七、查看信息</span></h2>
<pre><code># 显示有变更的文件
$ git status
# 显示当前分支的版本历史
$ git log
# 显示commit历史，以及每次commit发生变更的文件
$ git log --stat
# 搜索提交历史，根据关键词
$ git log -S [keyword]
# 显示某个commit之后的所有变动，每个commit占据一行
$ git log [tag] HEAD --pretty=format:%s
# 显示某个commit之后的所有变动，其"提交说明"必须符合搜索条件
$ git log [tag] HEAD --grep feature
# 显示某个文件的版本历史，包括文件改名
$ git log --follow [file]
$ git whatchanged [file]
# 显示指定文件相关的每一次diff
$ git log -p [file]
# 显示过去5次提交
$ git log -5 --pretty --oneline
# 显示所有提交过的用户，按提交次数排序
$ git shortlog -sn
# 显示指定文件是什么人在什么时间修改过
$ git blame [file]
# 显示暂存区和工作区的差异
$ git diff
# 显示暂存区和上一个commit的差异
$ git diff --cached [file]
# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD
# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]
# 显示今天你写了多少行代码
$ git diff --shortstat "@{0 day ago}"
# 显示某次提交的元数据和内容变化
$ git show [commit]
# 显示某次提交发生变化的文件
$ git show --name-only [commit]
# 显示某次提交时，某个文件的内容
$ git show [commit]:[filename]
# 显示当前分支的最近几次提交
$ git reflog</code></pre>
<h2 style="" id="%E5%85%AB%E3%80%81%E8%BF%9C%E7%A8%8B%E5%90%8C%E6%AD%A5"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">八、远程同步</span></h2>
<pre><code># 下载远程仓库的所有变动
$ git fetch [remote]
# 显示所有远程仓库
$ git remote -v
# 显示某个远程仓库的信息
$ git remote show [remote]
# 增加一个新的远程仓库，并命名
$ git remote add [shortname] [url]
# 取回远程仓库的变化，并与本地分支合并
$ git pull [remote] [branch]
# 上传本地指定分支到远程仓库
$ git push [remote] [branch]
# 强行推送当前分支到远程仓库，即使有冲突
$ git push [remote] --force
# 推送所有分支到远程仓库
$ git push [remote] --all</code></pre>
<h2 style="" id="%E4%B9%9D%E3%80%81%E6%92%A4%E9%94%80"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">九、撤销</span></h2>
<pre><code># 恢复暂存区的指定文件到工作区
$ git checkout [file]
# 恢复某个commit的指定文件到暂存区和工作区
$ git checkout [commit] [file]
# 恢复暂存区的所有文件到工作区
$ git checkout .
# 重置暂存区的指定文件，与上一次commit保持一致，但工作区不变
$ git reset [file]
# 重置暂存区与工作区，与上一次commit保持一致
$ git reset --hard
# 重置当前分支的指针为指定commit，同时重置暂存区，但工作区不变
$ git reset [commit]
# 重置当前分支的HEAD为指定commit，同时重置暂存区和工作区，与指定commit一致
$ git reset --hard [commit]
# 重置当前HEAD为指定commit，但保持暂存区和工作区不变
$ git reset --keep [commit]
# 新建一个commit，用来撤销指定commit
# 后者的所有变化都将被前者抵消，并且应用到当前分支
$ git revert [commit]
# 暂时将未提交的变化移除，稍后再移入
$ git stash
$ git stash pop</code></pre>
<h2 style="" id="%E5%8D%81%E3%80%81%E5%85%B6%E4%BB%96"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">十、其他</span></h2>
<pre><code>#  生成一个可供发布的压缩包
$  git  archive</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702297649296</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FGit.png&amp;size=m" type="image/jpeg" length="0"/><category>Git技术</category><pubDate>Mon, 11 Dec 2023 12:28:11 GMT</pubDate></item><item><title><![CDATA[Git常用命令总结]]></title><link>https://xiaoming728.com/archives/1702297712426</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Git%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E6%80%BB%E7%BB%93&amp;url=/archives/1702297712426" width="1" height="1" alt="" style="opacity:0;">
<h3 style="" id="git-init">git init</h3>
<p style="">&nbsp; &nbsp; &nbsp;在本地新建一个repo,进入一个项目目录,执行git init,会初始化一个repo,并在当前文件夹下创建一个.git文件夹.</p>
<h3 style="" id="git-clone">git clone</h3>
<p style="">&nbsp; &nbsp; &nbsp;获取一个url对应的远程Git repo, 创建一个local copy.</p>
<p style="">&nbsp; &nbsp; &nbsp;一般的格式是git clone [url].</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;clone下来的repo会以url最后一个斜线后面的名称命名,创建一个文件夹,如果想要指定特定的名称,可以git clone [url] newname指定.</p>
<h3 style="" id="git-status">git status</h3>
<p style="">&nbsp; &nbsp; &nbsp;查询repo的状态.</p>
<p style="">&nbsp; &nbsp; &nbsp;git status -s: -s表示short, -s的输出标记会有两列,第一列是对staging区域而言,第二列是对working目录而言.</p>
<h3 style="" id="git-log">git log</h3>
<p style="">&nbsp; &nbsp; &nbsp;显示分支的提交历史。</p>
<p style="">&nbsp; &nbsp; &nbsp;git log --oneline --number: 每条log只显示一行,显示number条.</p>
<p style="">&nbsp; &nbsp; &nbsp;git log --oneline --graph:可以图形化地表示出分支合并历史.</p>
<p style="">&nbsp; &nbsp; &nbsp;git log branchname可以显示特定分支的log.</p>
<p style="">&nbsp; &nbsp; &nbsp;git log --oneline branch1 ^branch2,可以查看在分支1,却不在分支2中的提交.^表示排除这个分支(Window下可能要给^branch2加上引号).</p>
<p style="">&nbsp; &nbsp; &nbsp;git log --decorate会显示出tag信息.</p>
<p style="">&nbsp; &nbsp; &nbsp;git log --author=[author name] 可以指定作者的提交历史.</p>
<p style="">&nbsp; &nbsp; &nbsp;git log --since --before --until --after 根据提交时间筛选log.</p>
<p style="">&nbsp; &nbsp; &nbsp;--no-merges可以将merge的commits排除在外.</p>
<p style="">&nbsp; &nbsp; &nbsp;git log --grep 根据commit信息过滤log: git log --grep=keywords</p>
<p style="">&nbsp; &nbsp; &nbsp;默认情况下, git log --grep --author是OR的关系,即满足一条即被返回,如果你想让它们是AND的关系,可以加上--all-match的option.</p>
<p style="">&nbsp; &nbsp; &nbsp;git log -S: filter by introduced diff.</p>
<p style="">&nbsp; &nbsp; &nbsp;比如: git log -SmethodName (注意S和后面的词之间没有等号分隔).</p>
<p style="">&nbsp; &nbsp; &nbsp;git log -p: show patch introduced at each commit.</p>
<p style="">&nbsp; &nbsp; &nbsp;每一个提交都是一个快照(snapshot),Git会把每次提交的diff计算出来,作为一个patch显示给你看.</p>
<p style="">&nbsp; &nbsp; &nbsp;另一种方法是git show [SHA].</p>
<p style="">&nbsp; &nbsp; &nbsp;git log --stat: show diffstat of changes introduced at each commit.</p>
<p style="">&nbsp; &nbsp; &nbsp;同样是用来看改动的相对信息的,--stat比-p的输出更简单一些.</p>
<h3 style="" id="git-add">git add</h3>
<p style="">&nbsp; &nbsp; &nbsp;在提交之前,Git有一个暂存区(staging area),可以放入新添加的文件或者加入新的改动. commit时提交的改动是上一次加入到staging area中的改动,而不是我们disk上的改动.</p>
<p style="">&nbsp; &nbsp; &nbsp;git add .</p>
<p style="">&nbsp; &nbsp; &nbsp;会递归地添加当前工作目录中的所有文件.</p>
<h3 style="" id="git-diff">git diff</h3>
<p style="">&nbsp; &nbsp; &nbsp;不加参数的git diff:</p>
<p style="">&nbsp; &nbsp; &nbsp;show diff of unstaged changes.</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容.</p>
<p style="">&nbsp;</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;若要看已经暂存起来的文件和上次提交时的快照之间的差异,可以用:</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;git diff --cached 命令.</p>
<p style="">&nbsp; &nbsp; &nbsp;show diff of staged changes.</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(Git 1.6.1 及更高版本还允许使用 git diff --staged，效果是相同的).</p>
<p style="">&nbsp;</p>
<p style="">&nbsp; &nbsp; &nbsp;git diff HEAD</p>
<p style="">&nbsp; &nbsp; &nbsp;show diff of all staged or unstated changes.</p>
<p style="">&nbsp; &nbsp; &nbsp;也即比较woking directory和上次提交之间所有的改动.</p>
<p style="">&nbsp;</p>
<p style="">&nbsp; &nbsp; &nbsp;如果想看自从某个版本之后都改动了什么,可以用:</p>
<p style="">&nbsp; &nbsp; &nbsp;git diff [version tag]</p>
<p style="">&nbsp; &nbsp; &nbsp;跟log命令一样,diff也可以加上--stat参数来简化输出.</p>
<p style="">&nbsp;</p>
<p style="">&nbsp; &nbsp; &nbsp;git diff [branchA] [branchB]可以用来比较两个分支.</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;它实际上会返回一个由A到B的patch,不是我们想要的结果.</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;一般我们想要的结果是两个分支分开以后各自的改动都是什么,是由命令:</p>
<p style="">&nbsp; &nbsp; &nbsp;git diff [branchA]…[branchB]给出的.</p>
<p style="">&nbsp; &nbsp; &nbsp;实际上它是:git diff $(git merge-base [branchA] [branchB]) [branchB]的结果.</p>
<h3 style="" id="git-commit">git commit</h3>
<p style="">&nbsp; &nbsp; &nbsp;提交已经被add进来的改动.</p>
<p style="">&nbsp; &nbsp; &nbsp;git commit -m “the commit message"</p>
<p style="">&nbsp; &nbsp; &nbsp;git commit -a 会先把所有已经track的文件的改动add进来,然后提交(有点像svn的一次提交,不用先暂存). 对于没有track的文件,还是需要git add一下.</p>
<p style="">&nbsp; &nbsp; &nbsp;git commit --amend 增补提交. 会使用与当前提交节点相同的父节点进行一次新的提交,旧的提交将会被取消.</p>
<h3 style="" id="git-reset">git reset</h3>
<p style="">&nbsp; &nbsp; &nbsp;undo changes and commits.</p>
<p style="">&nbsp; &nbsp; &nbsp;这里的HEAD关键字指的是当前分支最末梢最新的一个提交.也就是版本库中该分支上的最新版本.</p>
<p style="">&nbsp; &nbsp; &nbsp;git reset HEAD: unstage files from index and reset pointer to HEAD</p>
<p style="">&nbsp; &nbsp; &nbsp;这个命令用来把不小心add进去的文件从staged状态取出来,可以单独针对某一个文件操作: git reset HEAD - - filename, 这个- - 也可以不加.</p>
<p style="">&nbsp; &nbsp; &nbsp;git reset --soft</p>
<p style="">&nbsp; &nbsp; &nbsp;move HEAD to specific commit reference, index and staging are untouched.</p>
<p style="">&nbsp; &nbsp; &nbsp;git reset --hard</p>
<p style="">&nbsp; &nbsp; &nbsp;unstage files AND undo any changes in the working directory since last commit.</p>
<p style="">&nbsp; &nbsp; &nbsp;使用git reset —hard HEAD进行reset,即上次提交之后,所有staged的改动和工作目录的改动都会消失,还原到上次提交的状态.</p>
<p style="">&nbsp; &nbsp; &nbsp;这里的HEAD可以被写成任何一次提交的SHA-1.</p>
<p style="">&nbsp; &nbsp; &nbsp;不带soft和hard参数的git reset,实际上带的是默认参数mixed.</p>
<p style="">&nbsp; &nbsp; &nbsp;总结:</p>
<p style="">&nbsp; &nbsp; &nbsp;git reset --mixed id,是将git的HEAD变了(也就是提交记录变了),但文件并没有改变，(也就是working tree并没有改变). 取消了commit和add的内容.</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;git reset --soft id. 实际上，是git reset –mixed id 后,又做了一次git add.即取消了commit的内容.</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;git reset --hard id.是将git的HEAD变了,文件也变了.</p>
<p style="">&nbsp; &nbsp; &nbsp;按改动范围排序如下:</p>
<p style="">&nbsp; &nbsp; &nbsp;soft (commit) &lt; mixed (commit + add) &lt; hard (commit + add + local working)</p>
<h3 style="" id="git-revert">git revert</h3>
<p style="">&nbsp; &nbsp; &nbsp;反转撤销提交.只要把出错的提交(commit)的名字(reference)作为参数传给命令就可以了.</p>
<p style="">&nbsp; &nbsp; &nbsp;git revert HEAD: 撤销最近的一个提交.</p>
<p style="">&nbsp; &nbsp; &nbsp;git revert会创建一个反向的新提交,可以通过参数-n来告诉Git先不要提交.&nbsp; &nbsp;&nbsp;</p>
<h3 style="" id="git-rm">git rm</h3>
<p style="">&nbsp; &nbsp; &nbsp;git rm file: 从staging区移除文件,同时也移除出工作目录.</p>
<p style="">&nbsp; &nbsp; &nbsp;git rm --cached: 从staging区移除文件,但留在工作目录中.</p>
<p style="">&nbsp; &nbsp; &nbsp;git rm --cached从功能上等同于git reset HEAD,清除了缓存区,但不动工作目录树.</p>
<h3 style="" id="git-clean">git clean</h3>
<p style="">&nbsp; &nbsp; &nbsp;git&nbsp;clean是从工作目录中移除没有track的文件.</p>
<p style="">&nbsp; &nbsp; &nbsp;通常的参数是git clean -df:</p>
<p style="">&nbsp; &nbsp; &nbsp;-d表示同时移除目录,-f表示force,因为在git的配置文件中, clean.requireForce=true,如果不加-f,clean将会拒绝执行.&nbsp;</p>
<h3 style="" id="git-mv">git mv</h3>
<p style="">&nbsp; &nbsp; &nbsp;git rm - - cached orig; mv orig new; git add new&nbsp;</p>
<h3 style="" id="git-stash">git stash</h3>
<p style="">&nbsp; &nbsp; &nbsp;把当前的改动压入一个栈.</p>
<p style="">&nbsp; &nbsp; &nbsp;git stash将会把当前目录和index中的所有改动(但不包括未track的文件)压入一个栈,然后留给你一个clean的工作状态,即处于上一次最新提交处.</p>
<p style="">&nbsp; &nbsp; &nbsp;git stash list会显示这个栈的list.</p>
<p style="">&nbsp; &nbsp; &nbsp;git stash apply:取出stash中的上一个项目(stash@{0}),并且应用于当前的工作目录.</p>
<p style="">&nbsp; &nbsp; &nbsp;也可以指定别的项目,比如git stash apply stash@{1}.</p>
<p style="">&nbsp; &nbsp; &nbsp;如果你在应用stash中项目的同时想要删除它,可以用git stash pop</p>
<p style="">&nbsp;</p>
<p style="">&nbsp; &nbsp; &nbsp;删除stash中的项目:</p>
<p style="">&nbsp; &nbsp; &nbsp;git stash drop: 删除上一个,也可指定参数删除指定的一个项目.</p>
<p style="">&nbsp; &nbsp; &nbsp;git stash clear: 删除所有项目.</p>
<h3 style="" id="git-branch">git branch</h3>
<p style="">&nbsp; &nbsp; &nbsp;git branch可以用来列出分支,创建分支和删除分支.</p>
<p style="">&nbsp; &nbsp; &nbsp;git branch -v可以看见每一个分支的最后一次提交.</p>
<p style="">&nbsp; &nbsp; &nbsp;git branch: 列出本地所有分支,当前分支会被星号标示出.</p>
<p style="">&nbsp; &nbsp; &nbsp;git branch (branchname): 创建一个新的分支(当你用这种方式创建分支的时候,分支是基于你的上一次提交建立的).&nbsp;</p>
<p style="">&nbsp; &nbsp; &nbsp;git branch -d (branchname): 删除一个分支.</p>
<p style="">&nbsp; &nbsp; &nbsp;删除remote的分支:</p>
<p style="">&nbsp; &nbsp; &nbsp;git push (remote-name) :(branch-name): delete a remote branch.</p>
<p style="">&nbsp; &nbsp; &nbsp;这个是因为完整的命令形式是:</p>
<p style="">&nbsp; &nbsp; &nbsp;git push remote-name local-branch:remote-branch</p>
<p style="">&nbsp; &nbsp; &nbsp;而这里local-branch的部分为空,就意味着删除了remote-branch</p>
<h3 style="" id="git-checkout">git checkout</h3>
<p style="">　　git checkout (branchname)&nbsp;切换到一个分支.</p>
<p style="">&nbsp; &nbsp; &nbsp;git checkout -b (branchname): 创建并切换到新的分支.</p>
<p style="">&nbsp; &nbsp; &nbsp;这个命令是将git branch newbranch和git checkout newbranch合在一起的结果.</p>
<p style="">&nbsp; &nbsp; &nbsp;checkout还有另一个作用:替换本地改动:</p>
<p style="">&nbsp; &nbsp; &nbsp;git checkout --&lt;filename&gt;</p>
<p style="">&nbsp; &nbsp; &nbsp;此命令会使用HEAD中的最新内容替换掉你的工作目录中的文件.已添加到暂存区的改动以及新文件都不会受到影响.</p>
<p style="">&nbsp; &nbsp; &nbsp;注意:git checkout filename会删除该文件中所有没有暂存和提交的改动,这个操作是不可逆的.</p>
<h3 style="" id="git-merge">git merge</h3>
<p style="">&nbsp; &nbsp; &nbsp;把一个分支merge进当前的分支.</p>
<p style="">&nbsp; &nbsp; &nbsp;git merge [alias]/[branch]</p>
<p style="">&nbsp; &nbsp; &nbsp;把远程分支merge到当前分支.</p>
<p style="">&nbsp;</p>
<p style="">&nbsp; &nbsp; &nbsp;如果出现冲突,需要手动修改,可以用git mergetool.</p>
<p style="">&nbsp; &nbsp; &nbsp;解决冲突的时候可以用到git diff,解决完之后用git add添加,即表示冲突已经被resolved.</p>
<h3 style="" id="git-tag">git tag</h3>
<p style="">&nbsp; &nbsp; &nbsp;tag a point in history as import.</p>
<p style="">&nbsp; &nbsp; &nbsp;会在一个提交上建立永久性的书签,通常是发布一个release版本或者ship了什么东西之后加tag.</p>
<p style="">&nbsp; &nbsp; &nbsp;比如: git tag v1.0</p>
<p style="">&nbsp; &nbsp; &nbsp;git tag -a v1.0, -a参数会允许你添加一些信息,即make an annotated tag.</p>
<p style="">&nbsp; &nbsp; &nbsp;当你运行git tag -a命令的时候,Git会打开一个编辑器让你输入tag信息.</p>
<p style="">&nbsp; &nbsp; &nbsp;</p>
<p style="">&nbsp; &nbsp; &nbsp;我们可以利用commit SHA来给一个过去的提交打tag:</p>
<p style="">&nbsp; &nbsp; &nbsp;git tag -a v0.9 XXXX</p>
<p style="">&nbsp;</p>
<p style="">&nbsp; &nbsp; &nbsp;push的时候是不包含tag的,如果想包含,可以在push时加上--tags参数.</p>
<p style="">&nbsp; &nbsp; &nbsp;fetch的时候,branch HEAD可以reach的tags是自动被fetch下来的, tags that aren’t reachable from branch heads will be skipped.如果想确保所有的tags都被包含进来,需要加上--tags选项.</p>
<h3 style="" id="git-remote">git remote</h3>
<p style="">&nbsp; &nbsp; &nbsp;list, add and delete remote repository aliases.</p>
<p style="">&nbsp; &nbsp; &nbsp;因为不需要每次都用完整的url,所以Git为每一个remote repo的url都建立一个别名,然后用git remote来管理这个list.</p>
<p style="">&nbsp; &nbsp; &nbsp;git remote: 列出remote aliases.</p>
<p style="">&nbsp; &nbsp; &nbsp;如果你clone一个project,Git会自动将原来的url添加进来,别名就叫做:origin.</p>
<p style="">&nbsp; &nbsp; &nbsp;git remote -v:可以看见每一个别名对应的实际url.</p>
<p style="">&nbsp; &nbsp; &nbsp;git remote add [alias] [url]: 添加一个新的remote repo.</p>
<p style="">&nbsp; &nbsp; &nbsp;git remote rm [alias]: 删除一个存在的remote alias.</p>
<p style="">&nbsp; &nbsp; &nbsp;git remote rename [old-alias] [new-alias]: 重命名.</p>
<p style="">&nbsp; &nbsp; &nbsp;git remote set-url [alias] [url]:更新url. 可以加上—push和fetch参数,为同一个别名set不同的存取地址.&nbsp;</p>
<h3 style="" id="git-fetch">git fetch</h3>
<p style="">&nbsp; &nbsp; &nbsp;download new branches and data from a remote repository.</p>
<p style="">&nbsp; &nbsp; &nbsp;可以git fetch [alias]取某一个远程repo,也可以git fetch --all取到全部repo</p>
<p style="">&nbsp; &nbsp; &nbsp;fetch将会取到所有你本地没有的数据,所有取下来的分支可以被叫做remote branches,它们和本地分支一样(可以看diff,log等,也可以merge到其他分支),但是Git不允许你checkout到它们.&nbsp;</p>
<h3 style="" id="git-pull">git pull</h3>
<p style="">&nbsp; &nbsp; &nbsp;fetch from a remote repo and try to merge into the current branch.</p>
<p style="">&nbsp; &nbsp; &nbsp;pull == fetch + merge FETCH_HEAD</p>
<p style="">&nbsp; &nbsp; &nbsp;git pull会首先执行git fetch,然后执行git merge,把取来的分支的head merge到当前分支.这个merge操作会产生一个新的commit.&nbsp; &nbsp;&nbsp;</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果使用--rebase参数,它会执行git rebase来取代原来的git merge.&nbsp;</p>
<h3 style="" id="git-rebase">git rebase</h3>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;--rebase不会产生合并的提交,它会将本地的所有提交临时保存为补丁(patch),放在”.git/rebase”目录中,然后将当前分支更新到最新的分支尖端,最后把保存的补丁应用到分支上.</p>
<p style="">&nbsp; &nbsp; &nbsp;rebase的过程中,也许会出现冲突,Git会停止rebase并让你解决冲突,在解决完冲突之后,用git add去更新这些内容,然后无需执行commit,只需要:</p>
<p style="">&nbsp; &nbsp; &nbsp;git rebase --continue就会继续打余下的补丁.</p>
<p style="">&nbsp; &nbsp; &nbsp;git rebase --abort将会终止rebase,当前分支将会回到rebase之前的状态.</p>
<h3 style="" id="git-push">git push</h3>
<p style="">&nbsp; &nbsp; &nbsp;push your new branches and data to a remote repository.</p>
<p style="">&nbsp; &nbsp; &nbsp;git push [alias] [branch]</p>
<p style="">&nbsp; &nbsp; &nbsp;将会把当前分支merge到alias上的[branch]分支.如果分支已经存在,将会更新,如果不存在,将会添加这个分支.</p>
<p style="">&nbsp; &nbsp; &nbsp;如果有多个人向同一个remote repo push代码, Git会首先在你试图push的分支上运行git log,检查它的历史中是否能看到server上的branch现在的tip,如果本地历史中不能看到server的tip,说明本地的代码不是最新的,Git会拒绝你的push,让你先fetch,merge,之后再push,这样就保证了所有人的改动都会被考虑进来.</p>
<h3 style="" id="git-reflog">git reflog</h3>
<p style="">&nbsp; &nbsp; &nbsp;git reflog是对reflog进行管理的命令,reflog是git用来记录引用变化的一种机制,比如记录分支的变化或者是HEAD引用的变化.</p>
<p style="">&nbsp; &nbsp; &nbsp;当git reflog不指定引用的时候,默认列出HEAD的reflog.</p>
<p style="">&nbsp; &nbsp; &nbsp;HEAD@{0}代表HEAD当前的值,HEAD@{3}代表HEAD在3次变化之前的值.</p>
<p style="">&nbsp; &nbsp; &nbsp;git会将变化记录到HEAD对应的reflog文件中,其路径为.git/logs/HEAD, 分支的reflog文件都放在.git/logs/refs目录下的子目录中.</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;^代表父提交,当一个提交有多个父提交时,可以通过在^后面跟上一个数字,表示第几个父提交: ^相当于^1.</p>
<p style="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;~&lt;n&gt;相当于连续的&lt;n&gt;个^.&nbsp;</p>
<p style=""></p>
<blockquote>
 <p style="">参考资料</p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="http://gitref.org/index.html">http://gitref.org/index.html</a></p>
 <p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="http://gitref.org/index.html">http://git-scm.com/book/zh/v1</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702297712426</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2FGit.png&amp;size=m" type="image/jpeg" length="0"/><category>Git技术</category><pubDate>Mon, 11 Dec 2023 12:28:00 GMT</pubDate></item><item><title><![CDATA[Redis 开源客户端工具]]></title><link>https://xiaoming728.com/archives/1702297220906</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=Redis%20%E5%BC%80%E6%BA%90%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%B7%A5%E5%85%B7&amp;url=/archives/1702297220906" width="1" height="1" alt="" style="opacity:0;">
<h1 style="" id="redis-%E5%BC%80%E6%BA%90%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%B7%A5%E5%85%B7"><strong><span style="font-size: 21ptpx; color: rgb(0, 0, 0)">Redis 开源客户端工具</span></strong></h1>
<h2 style="" id="anotherredisdesktopmanager"><strong><span style="font-size: 19ptpx; color: rgb(255, 204, 0)">AnotherRedisDesktopManager</span></strong></h2>
<p style=""><span style="font-size: 11ptpx; color: rgb(74, 74, 74)">一个更快、更好、更稳定的redis桌面管理工具，可以运行于Linux、Windows、Mac三大平台，并且当加载大数量的key不会crash。</span></p>
<p style=""><span style="font-size: 11ptpx; color: rgb(74, 74, 74)">A faster, better and more stable redis desktop manager, compatible with Linux, windows, mac. What's more, it won't crash when loading a large number of keys.</span></p>
<h2 style="" id="%E5%BC%80%E6%BA%90%E5%9C%B0%E5%9D%80%EF%BC%9A"><strong><span style="font-size: 19ptpx; color: rgb(255, 204, 0)">开源地址：</span></strong></h2>
<p style=""><a target="_blank" rel="noopener noreferrer nofollow" href="https://gitee.com/qishibo/AnotherRedisDesktopManager"><span style="font-size: 11ptpx; color: rgb(0, 0, 255)">https://gitee.com/qishibo/AnotherRedisDesktopManager</span></a></p>
<h2 style="" id="%E8%B0%83%E6%95%B4%E4%B8%BB%E9%A2%98%E6%A8%A1%E5%BC%8F"><strong><span style="font-size: 19ptpx; color: rgb(255, 204, 0)">调整主题模式</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-sgps.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E6%94%AF%E6%8C%81%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87"><strong><span style="font-size: 19ptpx; color: rgb(255, 204, 0)">支持简体中文</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qlov.png&amp;size=m" style="display: inline-block"><span style="font-size: 11ptpx">&nbsp;</span></p>
<h2 style="" id="%E6%96%B0%E5%BB%BA%E9%93%BE%E6%8E%A5%EF%BC%8C%E6%94%AF%E6%8C%81ssh%E8%B7%B3%E6%9D%BF%E6%9C%BA%E3%80%81ssl%E6%A8%A1%E5%BC%8F%E3%80%81%E9%9B%86%E7%BE%A4%E6%A8%A1%E5%BC%8F%E9%93%BE%E6%8E%A5"><strong><span style="font-size: 19ptpx; color: rgb(255, 204, 0)">新建链接，支持SSH跳板机、SSL模式、集群模式链接</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-gysu.png&amp;size=m" style="display: inline-block"></p>
<h2 style="" id="%E5%8F%AF%E4%BB%A5%E6%9F%A5%E7%9C%8Bredis%E7%8A%B6%E6%80%81"><strong><span style="font-size: 19ptpx; color: rgb(255, 204, 0)">可以查看redis状态</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-rhjx.png&amp;size=m" style="display: inline-block"><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-lifo.png&amp;size=m" style="display: inline-block"><span style="font-size: 11ptpx">&nbsp;</span></p>
<h2 style="" id="%E6%96%B0%E5%A2%9Ekey"><strong><span style="font-size: 19ptpx; color: rgb(255, 204, 0)">新增key</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-jeki.png&amp;size=m" style="display: inline-block"><span style="font-size: 11ptpx">&nbsp;&nbsp;</span></p>
<h2 style="" id="%E4%BF%AE%E6%94%B9%EF%BC%88%E4%BF%AE%E6%94%B9key%E5%90%8D%E7%A7%B0%E3%80%81value%E3%80%81%E8%BF%87%E6%9C%9F%E6%97%B6%E9%97%B4%E3%80%81%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%EF%BC%89"><strong><span style="font-size: 19ptpx; color: rgb(255, 204, 0)">修改（修改key名称、value、过期时间、数据类型）</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.com%2Fupload%2Fimage-axmt.png&amp;size=m" style="display: inline-block"><span style="font-size: 11ptpx">&nbsp;</span></p>
<h2 style="" id="%E5%8F%AF%E4%BB%A5%E4%B8%80%E9%94%AE%E5%88%87%E6%8D%A2%E6%88%90%E5%91%BD%E4%BB%A4%E8%A1%8C%E6%93%8D%E4%BD%9C"><strong><span style="font-size: 19ptpx; color: rgb(255, 204, 0)">可以一键切换成命令行操作</span></strong></h2>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-gunu.png&amp;size=m" style="display: inline-block"><span style="font-size: 11ptpx">&nbsp;</span></p>]]></description><guid isPermaLink="false">/archives/1702297220906</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fredis.jpg&amp;size=m" type="image/jpeg" length="0"/><category>开源工具</category><pubDate>Mon, 11 Dec 2023 12:20:00 GMT</pubDate></item><item><title><![CDATA[mybatis generator自定义comment生成器并使用数据库注释生成实体类]]></title><link>https://xiaoming728.com/archives/1702292823948</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=mybatis%20generator%E8%87%AA%E5%AE%9A%E4%B9%89comment%E7%94%9F%E6%88%90%E5%99%A8%E5%B9%B6%E4%BD%BF%E7%94%A8%E6%95%B0%E6%8D%AE%E5%BA%93%E6%B3%A8%E9%87%8A%E7%94%9F%E6%88%90%E5%AE%9E%E4%BD%93%E7%B1%BB&amp;url=/archives/1702292823948" width="1" height="1" alt="" style="opacity:0;">]]></description><guid isPermaLink="false">/archives/1702292823948</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fmybatis.jpg&amp;size=m" type="image/jpeg" length="0"/><category>开源工具</category><pubDate>Mon, 11 Dec 2023 12:17:00 GMT</pubDate></item><item><title><![CDATA[优秀的开源http框架-Forest]]></title><link>https://xiaoming728.com/archives/1702292920097</link><description><![CDATA[<img src="https://xiaoming728.com/plugins/feed/assets/telemetry.gif?title=%E4%BC%98%E7%A7%80%E7%9A%84%E5%BC%80%E6%BA%90http%E6%A1%86%E6%9E%B6-Forest&amp;url=/archives/1702292920097" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">框架地址：<a target="_blank" rel="noopener noreferrer nofollow" href="https://gitee.com/dromara/forest">https://gitee.com/dromara/forest</a></p>
</blockquote>
<h2 style="" id="%E4%B8%8A%E6%89%8B">上手</h2>
<p style=""><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">支持了</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Springboot</span><span style="font-size: 16px; color: rgb(0, 0, 0)">的自动装配，所以只需要引入一个依赖就行</span></p>
<pre><code>&lt;dependency&gt;
  &lt;groupId&gt;com.dtflys.forest&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-forest&lt;/artifactId&gt;
  &lt;version&gt;1.3.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">定义自己的接口类</span></p>
<pre><code>public interface MyClient {

    @Request(url = "http://baidu.com")
    String simpleRequest();

    @Request(
            url = "http://ditu.amap.com/service/regeo",
            dataType = "json"
    )
    Map getLocation(@DataParam("longitude") String longitude, @DataParam("latitude") String latitude);
  
}</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">在启动类里配置代理接口类的扫描包</span></p>
<pre><code>@SpringBootApplication
@ForestScan(basePackages = "com.example.demo.forest")
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">这时候，你就可以从spring容器中注入你的代理接口，像调用本地方法一样去调用http的api了</span></p>
<pre><code>@Autowired
private MyClient myClient;

@Override
public void yourMethod throws Exception {
    Map result = myClient.getLocation("124.730329","31.463683");
    System.out.println(JSON.toJSONString(result,true));
}</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">日志打印，</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">打印了内部所用的http框架，和实际请求url和返回。当然日志可以通过配置去控制开关。</span></p>
<p style=""><img src="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2021%2Fpng%2F550960%2F1618794531462-f766beeb-4c61-4a83-89fd-73528b7fe771.png&amp;size=m" width="680" style="display: inline-block"></p>
<h2 style="" id="%E7%89%B9%E7%82%B9">特点</h2>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">我觉得对于尤其是做对接第三方api的开发同学来说，这款开源框架能帮你提高很多效率。</span></p>
<p style=""><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">&nbsp;底层封装了2种不同的http框架：</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Apache httpClient</span><span style="font-size: 16px; color: rgb(0, 0, 0)">和</span><span style="font-size: 14px; color: rgb(239, 112, 96)">OKhttp</span><span style="font-size: 16px; color: rgb(0, 0, 0)">。所以这个开源框架并没有对底层实现进行重复造轮子，而是在易用性上面下足了功夫。</span></p>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">我用</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">最终完成了和多个服务商api对接的项目，这些风格迥异的API，我仅用了1个小时时间就把他们转化为了本地方法。然后项目顺利上线。</span></p>
<p style=""><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">作为一款更加高层的http框架，其实你并不需要写很多代码，大多数时候，你仅通过一些配置就能完成http的本地化调用。而这个框架所能覆盖的面，却非常之广，满足你绝大多数的http调用请求。</span></p>
<p style=""><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">有以下特点：</span></p>
<ul>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">以</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Httpclient</span><span style="font-size: 16px; color: rgb(1, 1, 1)">和</span><span style="font-size: 14px; color: rgb(239, 112, 96)">OkHttp</span><span style="font-size: 16px; color: rgb(1, 1, 1)">为后端框架</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">通过调用本地方法的方式去发送Http请求, 实现了业务逻辑与Http协议之间的解耦</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">相比Feign更轻量，不依赖</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Spring Cloud</span><span style="font-size: 16px; color: rgb(1, 1, 1)">和任何注册中心</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">支持所有请求方法：</span><span style="font-size: 14px; color: rgb(239, 112, 96)">GET</span><span style="font-size: 16px; color: rgb(1, 1, 1)">,&nbsp;</span><span style="font-size: 14px; color: rgb(239, 112, 96)">HEAD</span><span style="font-size: 16px; color: rgb(1, 1, 1)">,&nbsp;</span><span style="font-size: 14px; color: rgb(239, 112, 96)">OPTIONS</span><span style="font-size: 16px; color: rgb(1, 1, 1)">,&nbsp;</span><span style="font-size: 14px; color: rgb(239, 112, 96)">TRACE</span><span style="font-size: 16px; color: rgb(1, 1, 1)">,&nbsp;</span><span style="font-size: 14px; color: rgb(239, 112, 96)">POST</span><span style="font-size: 16px; color: rgb(1, 1, 1)">,&nbsp;</span><span style="font-size: 14px; color: rgb(239, 112, 96)">DELETE</span><span style="font-size: 16px; color: rgb(1, 1, 1)">,&nbsp;</span><span style="font-size: 14px; color: rgb(239, 112, 96)">PUT</span><span style="font-size: 16px; color: rgb(1, 1, 1)">,&nbsp;</span><span style="font-size: 14px; color: rgb(239, 112, 96)">PATCH</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">支持灵活的模板表达式</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">支持过滤器来过滤传入的数据</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">基于注解、配置化的方式定义</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Http</span><span style="font-size: 16px; color: rgb(1, 1, 1)">请求</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">支持</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Spring</span><span style="font-size: 16px; color: rgb(1, 1, 1)">和</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Springboot</span><span style="font-size: 16px; color: rgb(1, 1, 1)">集成</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">实现</span><span style="font-size: 14px; color: rgb(239, 112, 96)">JSON</span><span style="font-size: 16px; color: rgb(1, 1, 1)">和</span><span style="font-size: 14px; color: rgb(239, 112, 96)">XML</span><span style="font-size: 16px; color: rgb(1, 1, 1)">的序列化和反序列化</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">支持JSON转换框架:&nbsp;</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Fastjson</span><span style="font-size: 16px; color: rgb(1, 1, 1)">,</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Jackson</span><span style="font-size: 16px; color: rgb(1, 1, 1)">,&nbsp;</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Gson</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">支持</span><span style="font-size: 14px; color: rgb(239, 112, 96)">JAXB</span><span style="font-size: 16px; color: rgb(1, 1, 1)">形式的</span><span style="font-size: 14px; color: rgb(239, 112, 96)">XML</span><span style="font-size: 16px; color: rgb(1, 1, 1)">转换</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">支持</span><span style="font-size: 14px; color: rgb(239, 112, 96)">SSL</span><span style="font-size: 16px; color: rgb(1, 1, 1)">的单向和双向加密</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">支持http连接池的设定</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">可以通过</span><span style="font-size: 14px; color: rgb(239, 112, 96)">OnSuccess</span><span style="font-size: 16px; color: rgb(1, 1, 1)">和</span><span style="font-size: 14px; color: rgb(239, 112, 96)">OnError</span><span style="font-size: 16px; color: rgb(1, 1, 1)">接口参数实现请求结果的回调</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">配置简单，一般只需要</span><span style="font-size: 14px; color: rgb(239, 112, 96)">@Request</span><span style="font-size: 16px; color: rgb(1, 1, 1)">一个注解就能完成绝大多数请求的定义</span></p></li>
 <li>
  <p style=""><span style="font-size: 16px; color: rgb(1, 1, 1)">支持异步请求调用</span></p></li>
</ul>
<h2 style="" id="%E4%B8%A4%E4%B8%AA%E5%BE%88%E6%A3%92%E7%9A%84%E5%8A%9F%E8%83%BD">两个很棒的功能</h2>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">这里不对使用方式和配置方式一一描述，有兴趣的可以去阅读详细文档：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://gitee.com/dromara/forest">http://forest.dtflyx.com/</a></p>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">这里只想分析这个框架2个我认为比较好的功能</span></p>
<h3 style="" id="%E6%A8%A1%E6%9D%BF%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%92%8C%E5%8F%82%E6%95%B0%E7%9A%84%E6%98%A0%E5%B0%84%E7%BB%91%E5%AE%9A%E5%8A%9F%E8%83%BD"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">模板表达式和参数的映射绑定功能</span></h3>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">模板表达式在使用的时候特别方便，举个栗子</span></p>
<pre><code>@Request(
    url = "${0}/send?un=${1}&amp;pw=${2}&amp;ph=${3}&amp;ct=${4}",
    type = "get",
    dataType = "json"
)
public Map send(
    String base,
    String userName,
    String password,
    String phone,
    String content
);</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">上述是用序号下标进行取值，也可以通过名字进行取值：</span></p>
<pre><code>@Request(
    url = "${base}/send?un=${un}&amp;pw=${pw}&amp;ph=${3}&amp;ct=${ct}",
    type = "get",
    dataType = "json"
)
public Map send(
    @DataVariable("base") String base,
    @DataVariable("un") String userName,
    @DataVariable("pw") String password,
    @DataVariable("ph") String phone,
    @DataVariable("ct") String content
);</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">甚至于可以这样简化写：</span></p>
<pre><code>@Request(
    url = "${base}/send",
    type = "get",
    dataType = "json"
)
public Map send(
    @DataVariable("base") String base,
    @DataParam("un") String userName,
    @DataParam("pw") String password,
    @DataParam("ph") String phone,
    @DataParam("ct") String content
);</code></pre>
<p style=""><strong><span style="font-size: 16px; color: rgb(0, 0, 0)">以上三种写法是等价的</span></strong></p>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">当然你也可以把参数绑定到header和body里去，你甚至于可以用一些表达式简单的把对象序列化成json或者xml：</span></p>
<pre><code>@Request(
    url = "${base}/pay",
   contentType = "application/json",
    type = "post",
    dataType = "json",
    headers = {"Authorization: ${1}"},
    data = "${json($0)}"
)
public PayResponse pay(PayRequest request, String auth);</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">当然数据绑定这块详情请参阅文档</span></p>
<h3 style="" id="%E5%AF%B9https%E7%9A%84%E6%94%AF%E6%8C%81"><span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">对HTTPS的支持</span></h3>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">以前用其他http框架处理https的时候，总觉得特别麻烦，尤其是双向证书。每次碰到问题也只能去baidu。然后根据别人的经验来修改自己的代码。</span></p>
<p style=""><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">对于这方面也想的很周到，底层完美封装了对https单双向证书的支持。也是只要通过简单的配置就能迅速完成。举个双向证书栗子：</span></p>
<pre><code>@Request(
    url = "${base}/pay",
   contentType = "application/json",
    type = "post",
    dataType = "json",
   keyStore = "pay-keystore",
   data = "${json($0)}"
)
public PayResponse pay(PayRequest request);</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">其中</span><span style="font-size: 14px; color: rgb(239, 112, 96)">pay-keystore</span><span style="font-size: 16px; color: rgb(0, 0, 0)">对应着</span><span style="font-size: 14px; color: rgb(239, 112, 96)">application.yml</span><span style="font-size: 16px; color: rgb(0, 0, 0)">里的</span><span style="font-size: 14px; color: rgb(239, 112, 96)">ssl-key-stores</span></p>
<pre><code>forest:
  ...
  ssl-key-stores:
    - id: pay-keystore
      file: test.keystore
      keystore-pass: 123456
      cert-pass: 123456
      protocols: SSLv3</code></pre>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">这样设置，就ok了，剩下的，就是本地代码形式的调用了。</span></p>
<h2 style="" id="%E6%9C%80%E5%90%8E">最后</h2>
<p style=""><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">有很多其他的功能设定，如果感兴趣的同学还请仔细去阅读文档和示例。</span></p>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">但是我想说的是，相信看到这里，很多人一定会说，这不就是</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Feign</span><span style="font-size: 16px; color: rgb(0, 0, 0)">吗？</span></p>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">我在开发</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Spring Cloud</span><span style="font-size: 16px; color: rgb(0, 0, 0)">项目的时候，也用过一段时间</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Feign</span><span style="font-size: 16px; color: rgb(0, 0, 0)">，个人感觉</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">的确在配置和用法上和</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Feign</span><span style="font-size: 16px; color: rgb(0, 0, 0)">的设计很像，但</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Feign</span><span style="font-size: 16px; color: rgb(0, 0, 0)">的角色更多是作为</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Spring Cloud</span><span style="font-size: 16px; color: rgb(0, 0, 0)">生态里的一个成员。充当RPC通信的角色，其承担的不仅是http通讯，还要对注册中心下发的调用地址进行负载均衡。</span></p>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">而</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">这个开源项目其定位则是一个高阶的http工具，主打友好和易用性。从使用角度出发，个人感觉</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span><span style="font-size: 16px; color: rgb(0, 0, 0)">配置性更加简单直接。提供的很多功能也能解决很多人的痛点。</span></p>
<p style=""><span style="font-size: 16px; color: rgb(0, 0, 0)">开源精神难能可贵，好的开源需要大家的添砖加瓦和支持。希望这篇文章能给大家在选择http客户端框架时带来一个新的选择：</span><span style="font-size: 14px; color: rgb(239, 112, 96)">Forest</span></p>
<p style=""></p>
<blockquote>
 <p style="text-align: justify; "><span style="font-size: 14px; color: rgb(51, 51, 51)">转自：元人部落</span></p>
 <p style="text-align: justify; "><span style="font-size: 14px; color: rgb(51, 51, 51)">链接：</span><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.cnblogs.com/bryan31/p/13359376.html"><span style="font-size: 14px">www.cnblogs.com/bryan31/p/13359376.html</span></a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/1702292920097</guid><dc:creator>xiaoming728</dc:creator><enclosure url="https://xiaoming728.com/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fxiaoming728.oss-cn-hangzhou.aliyuncs.com%2Fxiaoming728%2Fforest.png&amp;size=m" type="image/jpeg" length="0"/><category>开源工具</category><pubDate>Mon, 11 Dec 2023 11:10:00 GMT</pubDate></item></channel></rss>