<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>CodeVoy</title><description>人生在勤，不索何获。</description><link>https://zzyang.top/</link><language>zh_CN</language><item><title>Astro 集成 ech0 实现朋友圈</title><link>https://zzyang.top/posts/ech0/</link><guid isPermaLink="true">https://zzyang.top/posts/ech0/</guid><description>Astro + Ech0 实现 朋友圈动态 功能</description><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Docker部署Ech0&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 先拉取镜像
docker pull hub.rat.dev/sn0wl1n/ech0:latest

# 创建挂载目录
mkdir -p /home/docker/ech0/data/backup
mkdir -p /home/docker/ech0/data/ech0-data
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 创建docker-compose，在ech0目录下
[root@iZbp143l1lire02d1p2giaZ ech0]# cat docker-compose.yml 
services:
  ech0:
    image: sn0wl1n/ech0:latest
    container_name: ech0-service
    environment:
      - JWT_SECRET=xxxx  # 随便写一个长字符串，用于登录加密
    ports:
      - &quot;6277:6277&quot; # 保持内部 6277 端口映射到宿主机的 6277
    volumes:
      - ./data/ech0-data:/app/data
      - ./data/backup:/app/backup
    restart: always
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将以上内容写入任意文件内，命名为docker-compose.yml，在当前目录下执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;准备服务器&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;在域名解析中配置我们的二级域名&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;添加一个新的记录 记录类型为‘A’ ,主机记录就是我们的二级域名，记录值为我们的服务器IP；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;放行端口号&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在云服务器的安全组，放行我们刚刚&lt;code&gt;docker-compose&lt;/code&gt;中写的端口号：6277&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;修改nginx文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 处理 ech0.zzyang.top 的 HTTP 请求，强制跳转 HTTPS
server {
    listen 80;
    server_name ech0.zzyang.top;
    return 301 https://ech0.zzyang.top$request_uri;
}

# 2. 处理 ech0.zzyang.top 的 HTTPS 请求
server {
    listen 443 ssl;
    server_name ech0.zzyang.top;

    # 使用你现有的证书（如果是泛域名证书可以直接用，如果不是，请确保该证书包含 ech0 域名）
    ssl_certificate /etc/nginx/ssl/zzyang.top.pem;
    ssl_certificate_key /etc/nginx/ssl/zzyang.top.key;

    # 复用你已有的安全设置
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # 反向代理到 ech0 容器
    location / {
        proxy_pass http://你的IP:6277; 
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 必须：支持 Websocket，否则 ech0 无法实时同步
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection &quot;upgrade&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修改Astro代码&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;添加新的样式代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
    --liushen-card-bg: #fff;
    --liushen-card-border: 1px solid #e3e8f7;
    --card-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.09);
    --card-hover-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.2);
    --liushen-card-secondbg: #f1f3f8;
    --liushen-button-hover-bg: #2679cc;
    --liushen-text: #4c4948;
    --liushen-button-bg: #f1f3f8;
    --liushen-fancybox-bg: rgba(255,255,255,0.5);
}

:root.dark, .dark {
    --liushen-card-bg: #181818;
    --liushen-card-secondbg: #30343f;
    --liushen-card-border: 1px solid #42444a;
    --card-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.09);
    --card-hover-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.2);
    --liushen-button-bg: #30343f;
    --liushen-button-hover-bg: #2679cc;
    --liushen-text: rgba(255,255,255,0.702);
    --liushen-fancybox-bg: rgba(0,0,0,0.5);
}

#talk .talk_item {
    width: 100%;
    background: var(--liushen-card-bg);
    border: var(--liushen-card-border);
    box-shadow: var(--card-box-shadow);
    transition: box-shadow .3s ease-in-out;
    border-radius: 12px;
    display: inline-block;
    flex-direction: column;
    padding: 20px;
    margin: 0 0 16px;
    color: var(--liushen-text);
    break-inside: avoid;
}
#talk .talk_item:hover {
    box-shadow: var(--card-hover-box-shadow);
}

#talk{
    position: relative;
    width: 100%;
    box-sizing: border-box;
    column-count: 2;
    column-gap: 16px;
}

@media (max-width: 900px) {
    #talk {
      column-count: 1;
      column-gap: 0;
    }
}

#talk .talk_meta .avatar {
    margin: 0 !important;
    width: 60px;
    height: 60px;
    border-radius: 12px;
    flex-shrink: 0;
}
#talk .talk_bottom,
#talk .talk_meta {
    display: flex;
    align-items: center;
}
#talk .talk_meta {
    display: flex;
    align-items: center;
    width: 100%;
    gap: 10px;
    padding-bottom: 10px;
    border-bottom: 1px dashed rgba(128,128,128,0.6);
}
#talk .talk_bottom {
    margin-top: 15px;
    padding-top: 10px;
    border-top: 1px dashed grey;
    justify-content: space-between;
}
#talk .talk_meta .info {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    flex: 1 1 auto;
}
#talk .talk_meta .info .talk_nick {
    color: #6dbdc3;
    font-size: 1.2rem;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    line-height: 1.2;
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
#talk .talk_meta .info svg.is-badge.icon {
    width: 15px;
    display: inline-block;
    vertical-align: middle;
}
#talk .talk_meta .info span.talk_date {
    opacity: .6;
    font-size: 0.85rem;
}
#talk .talk_item .talk_content {
    margin-top: 10px;
    font-size: 0.95rem;
    line-height: 1.65;
}
#talk .talk_item .talk_content .zone_imgbox {
    display: flex;
    flex-wrap: wrap;
    --w: calc(25% - 8px);
    gap: 10px;
    margin-top: 10px;
}
#talk .talk_item .talk_content .zone_imgbox a {
    display: block;
    border-radius: 12px;
    width: var(--w);
    aspect-ratio: 1/1;
    position: relative;
}
#talk .talk_item .talk_content .zone_imgbox a:first-child {
    width: 100%;
    aspect-ratio: 1.8;
}
#talk .talk_item .talk_content .zone_imgbox img {
    border-radius: 10px;
    width: 100%;
    height: 100%;
    margin: 0 !important;
    object-fit: cover;
}
#talk .talk_item .talk_bottom {
    opacity: .9;
}
#talk .talk_item .talk_bottom .icon {
    float: right;
    transition: all .3s;
}
#talk .talk_item .talk_bottom .icon:hover {
    color: #49b1f5;
}
#talk .talk_item .talk_bottom span.talk_tag,
#talk .talk_item .talk_bottom span.location_tag {
    font-size: 0.85rem;
    background-color: var(--liushen-card-secondbg);
    border-radius: 12px;
    padding: 3px 15px 3px 10px;
    transition: box-shadow 0.3s ease;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

#talk .talk_item .talk_bottom span.location_tag {
    margin-left: 5px;
}

#talk .talk_item .talk_bottom span.talk_tag:hover,
#talk .talk_item .talk_bottom span.location_tag:hover {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
#talk .talk_item .talk_content&amp;gt;a {
    margin: 0 3px;
    color: #ff7d73 !important;
}
#talk .talk_item .talk_content&amp;gt;a:hover{
    text-decoration: none !important;
    color: #ff5143 !important
}

@media screen and (max-width: 900px) {
    #talk .talk_item .talk_content .zone_imgbox {
        --w: calc(33% - 5px);
    }
    #talk .talk_item #post-comment{
        margin: 0 3px
    }
}
@media screen and (max-width: 768px) {
    .zone_imgbox {
        gap: 6px;
    }
    .zone_imgbox {
        --w: calc(50% - 3px);
    }
    span.talk_date {
        font-size: 14px;
    }
}

#talk .talk_item .talk_content .douban-card {
    margin-top: 10px !important;
    text-decoration: none;
    align-items: center;
    border-radius: 12px;
    color: #faebd7;
    display: flex;
    justify-content: center;
    margin: 10px;
    max-width: 400px;
    overflow: hidden;
    padding: 15px;
    position: relative;
}

#talk .talk_item .talk_content .shuoshuo-external-link {
    width: 100%;
    height: 80px;
    margin-top: 10px;
    border-radius: 12px;
    background-color: var(--liushen-card-secondbg);
    color: var(--liushen-text);
    border: var(--liushen-card-border);
    transition: background-color .3s ease-in-out;
}

.shuoshuo-external-link:hover {
    background-color: var(--liushen-button-hover-bg);
}

.shuoshuo-external-link .external-link {
    display: flex;
    color: var(--liushen-text) !important;
    width: 100%;
    height: 100%;
}

.shuoshuo-external-link .external-link:hover {
    color: white !important;
}

.shuoshuo-external-link .external-link:hover {
    text-decoration: none !important;
}

.shuoshuo-external-link .external-link-left {
    width: 60px;
    height: 60px;
    margin: 10px;
    border-radius: 12px;
    background-size: cover;
    background-position: center;
}

.shuoshuo-external-link .external-link-right {
    display: flex;
    flex-direction: column;
    justify-content: center;
    width: calc(100% - 80px);
    padding: 10px;
}

.shuoshuo-external-link .external-link-right .external-link-title {
    font-size: 1.0rem;
    font-weight: 800;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.shuoshuo-external-link .external-link-right i {
    margin-left: 5px;
}

.limit {
    width: 100%;
    text-align: center;
    margin-top: 30px;
    color: var(--liushen-text);
    opacity: 0.75;
}

#main_top {
  display: flex;
  justify-content: center;
  width: 100%;
  margin: 0 0 16px;
}

#bber-talk {
  box-sizing: border-box;
  cursor: pointer;
  width: 100%;
  min-height: 50px;
  padding: .65rem 1rem;
  display: flex;
  align-items: center;
  overflow: hidden;
  font-weight: 700;
  border-radius: 16px;
  background: var(--liushen-card-bg);
  border: var(--liushen-card-border);
  box-shadow: var(--card-box-shadow);
}

#bber-talk,
#bber-talk a {
  color: var(--liushen-text);
}

#bber-talk svg.icon {
  width: 1em;
  height: 1em;
  vertical-align: -.15em;
  fill: currentColor;
  overflow: hidden;
  font-size: 20px;
}

#bber-talk .item i {
  margin-left: 5px;
}

#bber-talk &amp;gt; i {
  font-size: 1.1rem;
}

#bber-talk .talk-list {
  flex: 1;
  max-height: 28px;
  font-size: 0.95rem;
  padding: 0;
  margin: 0;
  overflow: hidden;
}

#bber-talk .talk-list:hover {
  color: var(--default-bg-color);
  transition: all .2s ease-in-out;
}

#bber-talk .talk-list li {
  list-style: none;
  width: 100%;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  margin-left: 10px;
}

@media screen and (min-width: 770px) {
  #bber-talk .talk-list {
    text-align: center;
    margin-right: 20px;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新增两个js文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(() =&amp;gt; {
	const apiUrl = &quot;https://ech0.zzyang.top/api/echo/page&quot;;
	let talkTimer = null;
	const cacheDuration = 30 * 60 * 1000;
	const cacheKey = `talksCache:${apiUrl}`;
	const cacheTimeKey = `talksCacheTime:${apiUrl}`;

	function toText(list) {
		return list.map((item) =&amp;gt; {
			let content = item.content || &quot;&quot;;

			const hasImg = /!\[.*?\]\(.*?\)/.test(content);
			const hasLink = /\[.*?\]\(.*?\)/.test(content);

			content = content
				.replace(/#(.*?)\s/g, &quot;&quot;)
				.replace(/\{.*?\}/g, &quot;&quot;)
				.replace(/!\[.*?\]\(.*?\)/g, &apos;&amp;lt;i class=&quot;fa-solid fa-image&quot;&amp;gt;&amp;lt;/i&amp;gt;&apos;)
				.replace(/\[.*?\]\(.*?\)/g, &apos;&amp;lt;i class=&quot;fa-solid fa-link&quot;&amp;gt;&amp;lt;/i&amp;gt;&apos;);

			const icons = [];

			if (item.images?.length &amp;amp;&amp;amp; !hasImg) icons.push(&quot;fa-solid fa-image&quot;);
			if (item.extension_type === &quot;VIDEO&quot;) icons.push(&quot;fa-solid fa-video&quot;);
			if (item.extension_type === &quot;MUSIC&quot;) icons.push(&quot;fa-solid fa-music&quot;);
			if (item.extension_type === &quot;WEBSITE&quot; &amp;amp;&amp;amp; !hasLink)
				icons.push(&quot;fa-solid fa-link&quot;);
			if (item.extension_type === &quot;GITHUBPROJ&quot; &amp;amp;&amp;amp; !hasLink)
				icons.push(&quot;fab fa-github&quot;);

			if (icons.length) {
				content += ` ${icons.map((icon) =&amp;gt; `&amp;lt;i class=&quot;${icon}&quot;&amp;gt;&amp;lt;/i&amp;gt;`).join(&quot; &quot;)}`;
			}

			return content;
		});
	}

	function renderTalk(list) {
		let html = &quot;&quot;;
		list.forEach((item, index) =&amp;gt; {
			html += `&amp;lt;li class=&quot;item item-${index + 1}&quot;&amp;gt;${item}&amp;lt;/li&amp;gt;`;
		});

		const box = document.querySelector(&quot;#bber-talk .talk-list&quot;);
		if (!box) return;

		box.innerHTML = html;

		talkTimer = setInterval(() =&amp;gt; {
			if (box.children.length &amp;gt; 0) {
				box.appendChild(box.children[0]);
			}
		}, 3000);
	}

	function indexTalk() {
		if (talkTimer) {
			clearInterval(talkTimer);
			talkTimer = null;
		}

		if (!document.getElementById(&quot;bber-talk&quot;)) return;

		const cachedData = localStorage.getItem(cacheKey);
		const cachedTime = localStorage.getItem(cacheTimeKey);
		const currentTime = Date.now();

		if (
			cachedData &amp;amp;&amp;amp;
			cachedTime &amp;amp;&amp;amp;
			currentTime - Number(cachedTime) &amp;lt; cacheDuration
		) {
			const data = toText(JSON.parse(cachedData));
			renderTalk(data.slice(0, 6));
			return;
		}

		fetch(apiUrl, {
			method: &quot;POST&quot;,
			headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
			body: JSON.stringify({ page: 1, pageSize: 30 }),
		})
			.then((res) =&amp;gt; res.json())
			.then((data) =&amp;gt; {
				if (data.code === 1 &amp;amp;&amp;amp; data.data &amp;amp;&amp;amp; Array.isArray(data.data.items)) {
					localStorage.setItem(cacheKey, JSON.stringify(data.data.items));
					localStorage.setItem(cacheTimeKey, currentTime.toString());
					const formattedData = toText(data.data.items);
					renderTalk(formattedData.slice(0, 6));
				} else {
					console.warn(&quot;Unexpected API response format:&quot;, data);
				}
			})
			.catch((error) =&amp;gt; console.error(&quot;Error fetching data:&quot;, error));
	}

	function init() {
		indexTalk();
	}

	if (!window.__ech0IndexTalkBound) {
		document.addEventListener(&quot;astro:page-load&quot;, init);
		window.__ech0IndexTalkBound = true;
	}

	if (document.readyState === &quot;loading&quot;) {
		document.addEventListener(&quot;DOMContentLoaded&quot;, init);
	} else {
		init();
	}
})();

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;(() =&amp;gt; {
	const API_URL = &quot;https://ech0.zzyang.top/api/echo/page&quot;;
	const TALK_CONTAINER_ID = &quot;talk&quot;;
	const CACHE_DURATION = 30 * 60 * 1000;
	const CACHE_KEY = `talksCache:${API_URL}`;
	const CACHE_TIME_KEY = `talksCacheTime:${API_URL}`;

	const generateIconSVG = () =&amp;gt;
		`&amp;lt;svg viewBox=&quot;0 0 512 512&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; class=&quot;is-badge icon&quot;&amp;gt;&amp;lt;path d=&quot;m512 268c0 17.9-4.3 34.5-12.9 49.7s-20.1 27.1-34.6 35.4c.4 2.7.6 6.9.6 12.6 0 27.1-9.1 50.1-27.1 69.1-18.1 19.1-39.9 28.6-65.4 28.6-11.4 0-22.3-2.1-32.6-6.3-8 16.4-19.5 29.6-34.6 39.7-15 10.2-31.5 15.2-49.4 15.2-18.3 0-34.9-4.9-49.7-14.9-14.9-9.9-26.3-23.2-34.3-40-10.3 4.2-21.1 6.3-32.6 6.3-25.5 0-47.4-9.5-65.7-28.6-18.3-19-27.4-42.1-27.4-69.1 0-3 .4-7.2 1.1-12.6-14.5-8.4-26-20.2-34.6-35.4-8.5-15.2-12.8-31.8-12.8-49.7 0-19 4.8-36.5 14.3-52.3s22.3-27.5 38.3-35.1c-4.2-11.4-6.3-22.9-6.3-34.3 0-27 9.1-50.1 27.4-69.1s40.2-28.6 65.7-28.6c11.4 0 22.3 2.1 32.6 6.3 8-16.4 19.5-29.6 34.6-39.7 15-10.1 31.5-15.2 49.4-15.2s34.4 5.1 49.4 15.1c15 10.1 26.6 23.3 34.6 39.7 10.3-4.2 21.1-6.3 32.6-6.3 25.5 0 47.3 9.5 65.4 28.6s27.1 42.1 27.1 69.1c0 12.6-1.9 24-5.7 34.3 16 7.6 28.8 19.3 38.3 35.1 9.5 15.9 14.3 33.4 14.3 52.4zm-266.9 77.1 105.7-158.3c2.7-4.2 3.5-8.8 2.6-13.7-1-4.9-3.5-8.8-7.7-11.4-4.2-2.7-8.8-3.6-13.7-2.9-5 .8-9 3.2-12 7.4l-93.1 140-42.9-42.8c-3.8-3.8-8.2-5.6-13.1-5.4-5 .2-9.3 2-13.1 5.4-3.4 3.4-5.1 7.7-5.1 12.9 0 5.1 1.7 9.4 5.1 12.9l58.9 58.9 2.9 2.3c3.4 2.3 6.9 3.4 10.3 3.4 6.7-.1 11.8-2.9 15.2-8.7z&quot; fill=&quot;#1da1f2&quot;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;`;

	const _waterfall = (container) =&amp;gt; {
		function getMargin(side, el) {
			const style = window.getComputedStyle(el);
			return Number.parseFloat(style[`margin${side}`]) || 0;
		}

		function px(value) {
			return `${value}px`;
		}

		function top(el) {
			return Number.parseFloat(el.style.top);
		}

		function left(el) {
			return Number.parseFloat(el.style.left);
		}

		function width(el) {
			return el.clientWidth;
		}

		function height(el) {
			return el.clientHeight;
		}

		function bottom(el) {
			return top(el) + height(el) + getMargin(&quot;Bottom&quot;, el);
		}

		function right(el) {
			return left(el) + width(el) + getMargin(&quot;Right&quot;, el);
		}

		function sortCols(cols) {
			cols.sort((a, b) =&amp;gt;
				bottom(a) === bottom(b) ? left(b) - left(a) : bottom(b) - bottom(a),
			);
		}

		function onResize(event) {
			if (width(container) !== containerWidth) {
				event.target.removeEventListener(event.type, onResize);
				_waterfall(container);
			}
		}

		if (typeof container === &quot;string&quot;) {
			container = document.querySelector(container);
		}
		if (!container) return;

		const items = Array.from(container.children);
		items.forEach((el) =&amp;gt; {
			el.style.position = &quot;absolute&quot;;
		});
		container.style.position = &quot;relative&quot;;

		const cols = [];
		if (items.length) {
			items[0].style.top = &quot;0px&quot;;
			items[0].style.left = px(getMargin(&quot;Left&quot;, items[0]));
			cols.push(items[0]);
		}

		let index = 1;
		for (; index &amp;lt; items.length; index += 1) {
			const prev = items[index - 1];
			const current = items[index];
			const fits = right(prev) + width(current) &amp;lt;= width(container);
			if (!fits) break;
			current.style.top = prev.style.top;
			current.style.left = px(right(prev) + getMargin(&quot;Left&quot;, current));
			cols.push(current);
		}

		for (; index &amp;lt; items.length; index += 1) {
			sortCols(cols);
			const current = items[index];
			const col = cols.pop();
			current.style.top = px(bottom(col) + getMargin(&quot;Top&quot;, current));
			current.style.left = px(left(col));
			cols.push(current);
		}

		sortCols(cols);
		const tallest = cols[0];
		container.style.height = px(bottom(tallest) + getMargin(&quot;Bottom&quot;, tallest));
		const containerWidth = width(container);
		window.addEventListener(&quot;resize&quot;, onResize);
	};

	const formatTime = (time) =&amp;gt; {
		const date = new Date(time);
		const pad = (value) =&amp;gt; value.toString().padStart(2, &quot;0&quot;);
		return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
			date.getDate(),
		)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
	};

	const normalizeTalk = (item) =&amp;gt; {
		const date = formatTime(item.created_at);
		let content = item.content || &quot;&quot;;
		content = content
			.replace(
				/\[(.*?)\]\((.*?)\)/g,
				`&amp;lt;a href=&quot;$2&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&amp;gt;@$1&amp;lt;/a&amp;gt;`,
			)
			.replace(/- \[ \]/g, &quot;o&quot;)
			.replace(/- \[x\]/g, &quot;x&quot;)
			.replace(/\n/g, &quot;&amp;lt;br&amp;gt;&quot;);
		content = `&amp;lt;div class=&quot;talk_content_text&quot;&amp;gt;${content}&amp;lt;/div&amp;gt;`;

		const origin = new URL(API_URL).origin;
		const images = Array.isArray(item.images)
			? item.images.map((img) =&amp;gt; img.image_url).filter(Boolean)
			: Array.isArray(item.echo_files)
				? item.echo_files
						.map((fileItem) =&amp;gt; fileItem?.file?.url)
						.filter(Boolean)
						.map((url) =&amp;gt; (url.startsWith(&quot;http&quot;) ? url : `${origin}${url}`))
				: [];

		if (images.length &amp;gt; 0) {
			const imgDiv = document.createElement(&quot;div&quot;);
			imgDiv.className = &quot;zone_imgbox&quot;;
			images.forEach((url) =&amp;gt; {
				const link = document.createElement(&quot;a&quot;);
				link.href = `${url}?fmt=webp&amp;amp;q=75`;
				link.setAttribute(&quot;data-fancybox&quot;, &quot;gallery&quot;);
				link.className = &quot;fancybox&quot;;
				const imgTag = document.createElement(&quot;img&quot;);
				imgTag.src = `${url}?fmt=webp&amp;amp;q=75`;
				link.appendChild(imgTag);
				imgDiv.appendChild(link);
			});
			content += imgDiv.outerHTML;
		}

		const extensionType = item.extension?.type || item.extension_type || &quot;&quot;;
		const extensionPayload =
			item.extension?.payload ||
			item.extension_payload ||
			item.extension ||
			null;

		if ([&quot;WEBSITE&quot;, &quot;GITHUBPROJ&quot;].includes(extensionType)) {
			let siteUrl = &quot;&quot;;
			let title = &quot;&quot;;
			let extensionBack = &quot;https://img.meituan.net/content/76ce3481bf9a82056df39e03c54d6ba117154003.png&quot;;
			try {
				const extObj =
					typeof extensionPayload === &quot;string&quot;
						? JSON.parse(extensionPayload)
						: extensionPayload;
				siteUrl =
					extObj.site || extObj.url || extObj.repoUrl || extensionPayload;
				title = extObj.title || siteUrl;
			} catch {
				siteUrl = extensionPayload;
				title = siteUrl;
			}

			if (extensionType === &quot;GITHUBPROJ&quot;) {
				extensionBack = &quot;https://img.meituan.net/content/cc7948ab56f263bbe66e7562e53d6b0c1524.png&quot;;
				const match = siteUrl.match(
					/^https?:\/\/github\.com\/[^/]+\/([^/?#]+)/i,
				);
				if (match) {
					title = match[1];
				} else {
					try {
						const parts = new URL(siteUrl).pathname.split(&quot;/&quot;).filter(Boolean);
						title = parts.pop() || siteUrl;
					} catch {
						// keep original title
					}
				}
			}

			content += `
        &amp;lt;div class=&quot;shuoshuo-external-link&quot;&amp;gt;
          &amp;lt;a class=&quot;external-link&quot; href=&quot;${siteUrl}&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&amp;gt;
            &amp;lt;div class=&quot;external-link-left&quot; style=&quot;background-image:url(${extensionBack})&quot;&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;external-link-right&quot;&amp;gt;
              &amp;lt;div class=&quot;external-link-title&quot;&amp;gt;${title}&amp;lt;/div&amp;gt;
              &amp;lt;div&amp;gt;Open&amp;lt;i class=&quot;fa-solid fa-angle-right&quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;`;
		}

		if (extensionType === &quot;MUSIC&quot; &amp;amp;&amp;amp; extensionPayload) {
			const link =
				typeof extensionPayload === &quot;object&quot;
					? extensionPayload.url
					: extensionPayload;
			let server = &quot;&quot;;
			if (link.includes(&quot;music.163.com&quot;)) server = &quot;netease&quot;;
			else if (link.includes(&quot;y.qq.com&quot;)) server = &quot;tencent&quot;;
			const idMatch = link.match(/id=(\d+)/);
			const id = idMatch ? idMatch[1] : &quot;&quot;;
			if (server &amp;amp;&amp;amp; id) {
				content += `&amp;lt;meting-js server=&quot;${server}&quot; type=&quot;song&quot; id=&quot;${id}&quot; api=&quot;https://met.liiiu.cn/meting/api?server=:server&amp;amp;type=:type&amp;amp;id=:id&amp;amp;auth=:auth&amp;amp;r=:r&quot;&amp;gt;&amp;lt;/meting-js&amp;gt;`;
			}
		}

		if (extensionType === &quot;VIDEO&quot; &amp;amp;&amp;amp; extensionPayload) {
			const video =
				typeof extensionPayload === &quot;object&quot;
					? extensionPayload.videoId
					: extensionPayload;
			if (video?.startsWith(&quot;BV&quot;)) {
				const bilibiliUrl = `https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=${video}&amp;amp;as_wide=1&amp;amp;high_quality=1&amp;amp;danmaku=0`;
				content += `
          &amp;lt;div style=&quot;position: relative; padding: 30% 45%; margin-top: 10px;&quot;&amp;gt;
            &amp;lt;iframe style=&quot;position:absolute;width:100%;height:100%;left:0;top:0;border-radius:12px;&quot;
              src=&quot;${bilibiliUrl}&quot;
              frameborder=&quot;no&quot;
              allowfullscreen=&quot;true&quot;
              loading=&quot;lazy&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
          &amp;lt;/div&amp;gt;`;
			} else if (video) {
				const youtubeUrl = `https://www.youtube.com/embed/${video}`;
				content += `
          &amp;lt;div style=&quot;position: relative; padding: 30% 45%; margin-top: 10px;&quot;&amp;gt;
            &amp;lt;iframe style=&quot;position:absolute;width:100%;height:100%;left:0;top:0;border-radius:12px;&quot;
              src=&quot;${youtubeUrl}&quot;
              title=&quot;YouTube video player&quot;
              frameborder=&quot;0&quot;
              allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot;
              allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
          &amp;lt;/div&amp;gt;`;
			}
		}

		return {
			content,
			user: item.username || &quot;Anonymous&quot;,
			avatar:
				&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_2026-03-18_002824_901.jpg&quot;,
			date,
			tags:
				Array.isArray(item.tags) &amp;amp;&amp;amp; item.tags.length
					? item.tags.map((t) =&amp;gt; t.name)
					: [&quot;No Tags&quot;],
			text: content.replace(/\[(.*?)\]\((.*?)\)/g, &quot;[link]&quot;),
		};
	};

	const generateTalkElement = (item) =&amp;gt; {
		const talkItem = document.createElement(&quot;div&quot;);
		talkItem.className = &quot;talk_item&quot;;

		const talkMeta = document.createElement(&quot;div&quot;);
		talkMeta.className = &quot;talk_meta&quot;;
		const avatar = document.createElement(&quot;img&quot;);
		avatar.className = &quot;no-lightbox avatar&quot;;
		avatar.src = item.avatar;

		const info = document.createElement(&quot;div&quot;);
		info.className = &quot;info&quot;;
		const nick = document.createElement(&quot;span&quot;);
		nick.className = &quot;talk_nick&quot;;
		nick.innerHTML = `${item.user} ${generateIconSVG()}`;
		const date = document.createElement(&quot;span&quot;);
		date.className = &quot;talk_date&quot;;
		date.textContent = item.date;
		info.appendChild(nick);
		info.appendChild(date);
		talkMeta.appendChild(avatar);
		talkMeta.appendChild(info);

		const talkContent = document.createElement(&quot;div&quot;);
		talkContent.className = &quot;talk_content&quot;;
		talkContent.innerHTML = item.content;

		const talkBottom = document.createElement(&quot;div&quot;);
		talkBottom.className = &quot;talk_bottom&quot;;
		const tags = document.createElement(&quot;div&quot;);
		const tag = document.createElement(&quot;span&quot;);
		tag.className = &quot;talk_tag&quot;;
		tag.textContent = `${item.tags}`;
		tags.appendChild(tag);

		const commentLink = document.createElement(&quot;a&quot;);
		commentLink.href = &quot;javascript:;&quot;;
		commentLink.onclick = () =&amp;gt; goComment(item.text);
		const icon = document.createElement(&quot;span&quot;);
		icon.className = &quot;icon&quot;;
		icon.innerHTML = &apos;&amp;lt;i class=&quot;fa-solid fa-message fa-fw&quot;&amp;gt;&amp;lt;/i&amp;gt;&apos;;
		commentLink.appendChild(icon);

		talkBottom.appendChild(tags);
		talkBottom.appendChild(commentLink);

		talkItem.appendChild(talkMeta);
		talkItem.appendChild(talkContent);
		talkItem.appendChild(talkBottom);

		return talkItem;
	};

	const goComment = (text) =&amp;gt; {
		const match = text.match(
			/&amp;lt;div class=&quot;talk_content_text&quot;&amp;gt;([\s\S]*?)&amp;lt;\/div&amp;gt;/,
		);
		const textContent = match ? match[1] : &quot;&quot;;
		const textarea =
			document.querySelector(&quot;.atk-textarea&quot;) ||
			document.querySelector(&quot;.tk-input textarea&quot;) ||
			document.querySelector(&quot;.tk-comment-input textarea&quot;);
		if (!textarea) return;
		textarea.value = `&amp;gt; ${textContent}\n\n`;
		textarea.focus();
	};

	const renderTalksList = (list, container) =&amp;gt; {
		list.map(normalizeTalk).forEach((item) =&amp;gt; {
			container.appendChild(generateTalkElement(item));
		});
	};

	const fetchAndRenderTalks = (container) =&amp;gt; {
		const cachedData = localStorage.getItem(CACHE_KEY);
		const cachedTime = localStorage.getItem(CACHE_TIME_KEY);
		const now = Date.now();

		if (cachedData &amp;amp;&amp;amp; cachedTime &amp;amp;&amp;amp; now - Number(cachedTime) &amp;lt; CACHE_DURATION) {
			renderTalksList(JSON.parse(cachedData), container);
		}

		fetch(API_URL, {
			method: &quot;POST&quot;,
			headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
			body: JSON.stringify({ page: 1, pageSize: 30 }),
		})
			.then((res) =&amp;gt; res.json())
			.then((data) =&amp;gt; {
				if (data.code === 1 &amp;amp;&amp;amp; data.data &amp;amp;&amp;amp; Array.isArray(data.data.items)) {
					const latestItems = data.data.items;
					const cachedItems = cachedData ? JSON.parse(cachedData) : [];
					const cachedFirstId = cachedItems[0]?.id;
					const latestFirstId = latestItems[0]?.id;

					if (!cachedFirstId || cachedFirstId !== latestFirstId) {
						container.innerHTML = &quot;&quot;;
						renderTalksList(latestItems, container);
					}

					localStorage.setItem(CACHE_KEY, JSON.stringify(latestItems));
					localStorage.setItem(CACHE_TIME_KEY, now.toString());
				}
			})
			.catch((err) =&amp;gt; console.error(&quot;Error fetching:&quot;, err));
	};

	const renderTalks = () =&amp;gt; {
		const talkContainer = document.getElementById(TALK_CONTAINER_ID);
		if (!talkContainer) return;
		talkContainer.innerHTML = &quot;&quot;;
		fetchAndRenderTalks(talkContainer);
	};

	const run = () =&amp;gt; {
		renderTalks();
	};

	if (!window.__ech0TalkBound) {
		document.addEventListener(&quot;astro:page-load&quot;, run);
		window.__ech0TalkBound = true;
	}

	if (document.readyState === &quot;loading&quot;) {
		document.addEventListener(&quot;DOMContentLoaded&quot;, run);
	} else {
		run();
	}
})();

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;修改config.ts&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		LinkPreset.About,
		{
			name: &quot;说说&quot;,
			url: &quot;/shuoshuo/&quot;,
		},
		LinkPreset.Friends,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;新增组件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import { url } from &quot;../../utils/url-utils&quot;;
---
&amp;lt;div id=&quot;main_top&quot;&amp;gt;
    &amp;lt;a id=&quot;bber-talk&quot; class=&quot;card-base bb_talk_swipper&quot; href={url(&quot;/shuoshuo/&quot;)} aria-label=&quot;Go to Shuoshuo&quot;&amp;gt;
        &amp;lt;svg
            class=&quot;icon&quot;
            viewBox=&quot;0 0 1024 1024&quot;
            version=&quot;1.1&quot;
            xmlns=&quot;http://www.w3.org/2000/svg&quot;
            width=&quot;200&quot;
            height=&quot;200&quot;
        &amp;gt;
            &amp;lt;path
                d=&quot;M526.432 924.064c-20.96 0-44.16-12.576-68.96-37.344L274.752 704H192c-52.928 0-96-43.072-96-96V416c0-52.928 43.072-96 96-96h82.752l182.624-182.624c24.576-24.576 47.744-37.024 68.864-37.024C549.184 100.352 576 116 576 160v704c0 44.352-26.72 60.064-49.568 60.064zM192 384c-17.632 0-32 14.368-32 32v192c0 17.664 14.368 32 32 32h96c8.48 0 16.64 3.36 22.624 9.376l192.064 192.096c3.392 3.36 6.496 6.208 9.312 8.576V174.016a145.824 145.824 0 0 0-9.376 8.608l-192 192C304.64 380.64 296.48 384 288 384h-96zM687.584 730.368a31.898 31.898 0 0 1-18.656-6.016c-14.336-10.304-17.632-30.304-7.328-44.672l12.672-17.344C707.392 617.44 736 578.624 736 512c0-69.024-25.344-102.528-57.44-144.928-5.664-7.456-11.328-15.008-16.928-22.784-10.304-14.336-7.04-34.336 7.328-44.672 14.368-10.368 34.336-7.04 44.672 7.328 5.248 7.328 10.656 14.464 15.968 21.504C764.224 374.208 800 421.504 800 512c0 87.648-39.392 141.12-74.144 188.32l-12.224 16.736c-6.272 8.704-16.064 13.312-26.048 13.312z&quot;
                p-id=&quot;3947&quot;
            &amp;gt;&amp;lt;/path&amp;gt;
            &amp;lt;path
                d=&quot;M796.448 839.008a31.906 31.906 0 0 1-21.088-7.936c-13.28-11.648-14.624-31.872-2.976-45.152C836.608 712.672 896 628.864 896 512s-59.392-200.704-123.616-273.888c-11.648-13.312-10.304-33.504 2.976-45.184 13.216-11.648 33.44-10.336 45.152 2.944C889.472 274.56 960 373.6 960 512s-70.528 237.472-139.488 316.096c-6.368 7.232-15.2 10.912-24.064 10.912z&quot;
                p-id=&quot;3948&quot;
            &amp;gt;&amp;lt;/path&amp;gt;
        &amp;lt;/svg&amp;gt;
        &amp;lt;ul class=&quot;talk-list&quot;&amp;gt;说说加载中。。。&amp;lt;/ul&amp;gt;
    &amp;lt;/a&amp;gt;
&amp;lt;/div&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;修改Layout.astro&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在111 line增加&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		&amp;lt;link rel=&quot;stylesheet&quot; href={url(&quot;/css/ech0-talk.css&quot;)} media=&quot;all&quot; /&amp;gt;
		&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://fastly.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css&quot; media=&quot;all&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;151 line 增加&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		&amp;lt;script src=&quot;https://fastly.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;
		&amp;lt;script src=&quot;https://fastly.jsdelivr.net/npm/meting@2.0.1/dist/Meting.min.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;添加shuoshuo.astro&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import MainGridLayout from &quot;../layouts/MainGridLayout.astro&quot;;
---
&amp;lt;MainGridLayout title=&quot;键盘侠的日常哔哔&quot; description=&quot;说说列表&quot;&amp;gt;
    &amp;lt;div class=&quot;flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32 mb-4&quot;&amp;gt;
        &amp;lt;div class=&quot;card-base z-10 px-6 md:px-9 pt-6 pb-8 relative w-full&quot;&amp;gt;
            &amp;lt;div id=&quot;talk&quot;&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;limit&quot;&amp;gt;- 只展示最近30条说说 -&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;/js/ech0-shuoshuo.js&quot; is:inline&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/MainGridLayout&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此结束，看所有完整的可以看我的 &lt;a href=&quot;https://github.com/zxyang3636/CodeVoy&quot;&gt;github&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>LangChain4j</title><link>https://zzyang.top/posts/langchain4j/</link><guid isPermaLink="true">https://zzyang.top/posts/langchain4j/</guid><description>Java优雅的操作大模型对话</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.pixiv.net/artworks/120982147&quot;&gt;封面图&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;:::warning
LangChain4j JDK不能低于17
:::&lt;/p&gt;
&lt;h2&gt;大模型部署&lt;/h2&gt;
&lt;p&gt;智能应用就是在传统软件的基础上接入大模型，所以，我们要完成智能应用的开发，首先得把大模型这种软件部署起来，而大模型的部署会有两种方式，自己部署、他人部署。自己部署大模型自己直接用，他人部署的大模型我们掏钱用。接下来我们分别聊一聊这两种方式的优缺点。&lt;/p&gt;
&lt;p&gt;:::tip
自己部署：&lt;/p&gt;
&lt;p&gt;云服务器部署：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优势：前期成本低，维护简单&lt;/li&gt;
&lt;li&gt;劣势：数据不安全，长期使用成本高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本地机器部署：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优势：数据安全，长期使用成本低&lt;/li&gt;
&lt;li&gt;劣势：初期成本高，维护困难&lt;br /&gt;
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
他人部署：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优势：无需部署&lt;/li&gt;
&lt;li&gt;劣势：数据不安全，长期使用成本高&lt;br /&gt;
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先看自己部署，我们自己在部署大模型的时候，也会有两种方式，一种是在云端部署，另外一种是在本地机房部署。在云端部署的优点是前期部署成本低，维护简单，比如你去阿里云租服务器，按天收费，我们可以花很少的费用，就能快速上手，并且像阿里云这样的平台，服务器维护成本也是很低的。但缺点就是数据不安全，因为使用别人提供的服务器，数据都得从这个服务器过一圈，数据自然就不安全了；还有就是长期使用成本高，虽然阿里云租服务器每天的收费看起来不算贵，但是你只要用一天，就得付一天钱，时间长了，这个费用其实还是蛮高的。&lt;/p&gt;
&lt;p&gt;我们自己部署的另外一种方式就是部署在本地机房中，这种方式相比较云端部署，它的优势是数据安全，毕竟自己的服务器嘛，数据并不会向外部暴露，还有就是长期成本低，因为是一次性投入，时间越长，平均成本就越低。反过来，它的缺点是初期成本高，买服务器的钱是一次性支付的，还有就是维护困难一些，因为自己买的服务器，所有的维护工作都需要自己来做。&lt;/p&gt;
&lt;p&gt;我们再来看他人部署，都有谁会帮我们部署大模型呢？这样的好事者有很多，常见的比如有阿里云百炼、百度智能云、硅基流动、火山引擎等等。它们部署好的大模型，我们怎么用呢？常规思路，使用他们提供的API接口使用，当然了，你使用的时候，它会按照流量进行收费的，毕竟天下没有免费的午餐。使用这些平台的大模型，优点是我们自己无需部署，缺点是数据不安全、长期使用成本高。&lt;/p&gt;
&lt;p&gt;ollama、LM Studio可以
一键下载、运行大模型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251229235403618.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;ollama本机部署大模型并使用&lt;/h3&gt;
&lt;h4&gt;安装ollama&lt;/h4&gt;
&lt;p&gt;Ollama的官网是:https://ollama.com/&lt;/p&gt;
&lt;p&gt;大家打开后，首页就有一个下载按钮，你只要点击一下download，选择对应的操作系统，就可以下载对应版本的ollama了。&lt;/p&gt;
&lt;p&gt;:::info
ollama安装完毕后，会自动的配置系统环境变量，因此接下来我们就可以直接执行ollama的命令去部署大模型了，如果有同学将来执行命令的时候报错，请记得检查一下你的环境变量, 可以手动的配置一下
:::&lt;/p&gt;
&lt;h4&gt;部署大模型&lt;/h4&gt;
&lt;p&gt;ollama官网上给出了很多大模型，大家可以根据自己的需求选择对应的大模型安装，这里咱们安装qwen3系列模型，首先点击导航栏的Models来到模型列表&lt;/p&gt;
&lt;p&gt;然后点击模型列表中的qwen3, 来到qwen3详情页面&lt;/p&gt;
&lt;p&gt;这里提供了不同参数规模的qwen3模型，由于参数规模越大，对电脑的配置要求越高，为了照顾到大部分同学的电脑，这里我们部署最小参数规模的大模型qwen3:0.6b来部署，点击模型的名称，来到该模型的详情页面，并赋值右上角的命令。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251230000152165.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;找到安装位置&lt;code&gt;D:\app\Ollama&lt;/code&gt;，进行cmd命令操作&lt;/p&gt;
&lt;p&gt;打开命令行提示符窗口，执行这个命令，命令执行的过程中，会自动下载qwen3:0.6b这个模型到电脑本地，并自动的运行起来，命令行提示符窗口如果自动进入到聊天界面，证明模型部署正确。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama run qwen3:0.6b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来你就可以跟本地部署的大模型进行对话了，输入问题敲回车即可&lt;/p&gt;
&lt;p&gt;如果不想继续与大模型对话，可以使用 &lt;code&gt;/bye&lt;/code&gt; 命令退出聊天界面&lt;/p&gt;
&lt;p&gt;如果想继续与大模型聊天，可以再次执行 ollama run qwen3:0.6b, 这一次再执行的时候，由于本地已经有了这个大模型并运行起来了，所以不会再次下载，而是直接进入聊天界面。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;有关ollama提供的命令有很多，如下&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;一、基础操作指令&lt;/strong&gt;&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指令&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ollama run &amp;lt;模型名&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行指定模型（自动下载若不存在）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ollama run llama3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ollama list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看本地已下载的模型列表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ollama list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ollama pull &amp;lt;模型名&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;手动下载模型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ollama pull mistral&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ollama rm &amp;lt;模型名&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除本地模型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ollama rm llama2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ollama help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看帮助文档&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ollama help&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;&lt;strong&gt;二、模型交互指令&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;1. 直接对话&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;ollama run llama3 &quot;用中文写一首关于秋天的诗&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;2. 进入交互模式&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;ollama run llama3
# 进入后输入内容，按 Ctrl+D 或输入 `/bye` 退出
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;3. 从文件输入&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;ollama run llama3 --file input.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;4. 流式输出控制&lt;/strong&gt;&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--verbose&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示详细日志&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ollama run llama3 --verbose&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--nowordwrap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;禁用自动换行&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ollama run llama3 --nowordwrap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;&lt;strong&gt;三、模型管理&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;1. 自定义模型配置（Modelfile）&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;创建 &lt;code&gt;Modelfile&lt;/code&gt; 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM llama3  # 基础模型
PARAMETER temperature 0.7  # 控制随机性（0-1）
PARAMETER num_ctx 4096     # 上下文长度
SYSTEM &quot;&quot;&quot; 你是一个严谨的学术助手，回答需引用论文来源。&quot;&quot;&quot;                # 系统提示词
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建自定义模型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama create my-llama3 -f Modelfile
ollama run my-llama3
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;2. 查看模型信息&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;ollama show &amp;lt;模型名&amp;gt; --modelfile  # 查看模型配置
ollama show &amp;lt;模型名&amp;gt; --parameters # 查看运行参数
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;四、高级功能&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;1. API 调用&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;启动 API 服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama serve
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 HTTP 调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl http://localhost:11434/api/generate -d &apos;{
  &quot;model&quot;: &quot;llama3&quot;,
  &quot;prompt&quot;: &quot;你好&quot;,
  &quot;stream&quot;: false
}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;2. GPU 加速配置&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;# 指定显存分配比例（50%）
ollama run llama3 --num-gpu 50
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;调用大模型&lt;/h4&gt;
&lt;p&gt;文档地址：https://ollama.com/blog/thinking&lt;/p&gt;
&lt;p&gt;ollama平台也开放了API，程序员可以使用发送http请求的方式调用本地部署的大模型，这里咱们借助于Apifox工具调用大模型&lt;/p&gt;
&lt;p&gt;本机ollama默认占用的端口为11434，调用大模型时发送的请求方式必须是post，请求数据必须是json格式，具体样例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST
http://localhost:11434/api/chat
{
    &quot;model&quot;: &quot;qwen3:0.6b&quot;,
    &quot;messages&quot;: [
        {
            &quot;role&quot;: &quot;user&quot;,
            &quot;content&quot;: &quot;你是谁?&quot;
        }
    ],
    &quot;think&quot;: true,
    &quot;stream&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251230002203622.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;阿里云百炼平台使用&lt;/h3&gt;
&lt;p&gt;如果要使用阿里云百炼，需要有如下四个步骤的操作：&lt;/p&gt;
&lt;p&gt;A. 登录阿里云 https://aliyun.com&lt;/p&gt;
&lt;p&gt;B. 开通 大模型服务平台百炼 服务&lt;/p&gt;
&lt;p&gt;C. 申请百炼平台 &lt;a href=&quot;https://bailian.console.aliyun.com/?spm=5176.29597918.J_SEsSjsNv72yRuRFS2VknO.2.1e887b08V2Corc&amp;amp;tab=app#/api-key&quot;&gt;API-KEY&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;D. 选择大模型使用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送http的方式调用大模型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bailian.console.aliyun.com/?spm=5176.29597918.J_SEsSjsNv72yRuRFS2VknO.2.1e887b08V2Corc&amp;amp;tab=api#/api/?type=model&amp;amp;url=2712576&quot;&gt;文档&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions

{
    &quot;model&quot;: &quot;qwen-plus&quot;,
    &quot;messages&quot;: [
        {
            &quot;role&quot;: &quot;system&quot;,
            &quot;content&quot;: &quot;You are a helpful assistant.&quot;
        },
        {
            &quot;role&quot;: &quot;user&quot;,
            &quot;content&quot;: &quot;你好，你是谁？&quot;
        }
    ]
}


响应：
{
    &quot;choices&quot;: [
        {
            &quot;message&quot;: {
                &quot;role&quot;: &quot;assistant&quot;,
                &quot;content&quot;: &quot;你好！我是通义千问（Qwen），是阿里巴巴集团旗下的通义实验室自主研发的超大规模语言模型。我可以帮助你回答问题、创作文字、提供信息查询，还能陪你聊天、写故事、写公文、写邮件、写剧本等等。如果你有任何需要帮助的地方，尽管告诉我哦！😊&quot;
            },
            &quot;finish_reason&quot;: &quot;stop&quot;,
            &quot;index&quot;: 0,
            &quot;logprobs&quot;: null
        }
    ],
    &quot;object&quot;: &quot;chat.completion&quot;,
    &quot;usage&quot;: {
        &quot;prompt_tokens&quot;: 24,
        &quot;completion_tokens&quot;: 67,
        &quot;total_tokens&quot;: 91,
        &quot;prompt_tokens_details&quot;: {
            &quot;cached_tokens&quot;: 0
        }
    },
    &quot;created&quot;: 1767104026,
    &quot;system_fingerprint&quot;: null,
    &quot;model&quot;: &quot;qwen-plus&quot;,
    &quot;id&quot;: &quot;chatcmpl-02502190-0f38-9633-a4d8-a7ad3cc106bc&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;常见参数&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;model: 告诉平台，当前调用哪个模型&lt;/li&gt;
&lt;li&gt;messages: 发送给模型的数据，模型会根据这些数据给出合适的响应
&lt;ul&gt;
&lt;li&gt;content: 消息内容&lt;/li&gt;
&lt;li&gt;role: 消息角色(类型)
&lt;ul&gt;
&lt;li&gt;user: 用户消息&lt;/li&gt;
&lt;li&gt;system: 系统消息&lt;/li&gt;
&lt;li&gt;assistant: 模型响应消息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;stream: 调用方式
&lt;ul&gt;
&lt;li&gt;true: 非阻塞调用(流式调用)&lt;/li&gt;
&lt;li&gt;false: 阻塞调用，一次性响应(默认)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;enable_search: 联网搜索，启用后，模型会将搜索结果作为参考信息
&lt;ul&gt;
&lt;li&gt;true: 开启&lt;/li&gt;
&lt;li&gt;false: 不开启（默认）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;model，由于百炼平台提供了各种各样的模型，所以你需要通过model这个参数来指定接下来要调用的是哪个模型。&lt;/p&gt;
&lt;p&gt;messages，用户发送给大模型的消息有三种，使用role来进行分别，其中user代表的是用户问题；system代表的系统消息，它是用于给大模型设定一个角色，然后大模型就可以用该角色的口吻跟用户对话了；&lt;/p&gt;
&lt;p&gt;assistant代表的是大模型给用户响应的消息，这里很奇怪，为什么大模型响应给用户的消息，再次请求大模型时需要携带给大模型呢？这是因为大模型没有记忆能力，也就是说用户跟大模型交互的过程中，每一次问答都是独立的，互不干扰的。但是实际上我们人与人之间的聊天不是这样的，比如我问你西北大学是211吗？你回答我是！我再问你是985吗？你会回答不是！虽然我第二次问你的时候我并没有问具体哪个大学是985，但是你可以从咱们之前的聊天信息中推断出我要问的是西北大学，因为你已经记住了之前的聊天信息。但是大模型目前做不到，如果要让大模型在与用户沟通的过程中达到人与人沟通的效果，我们唯一的解决方案就是每次与大模型交互的过程中，把之前用户的问题和大模型的响应以及现在的问题，都发送给大模型，这样大模型就可以根据以前的聊天信息从而做出推断了&lt;/p&gt;
&lt;p&gt;下面是一个演示的案例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251230223231291.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;sream代表调用大模型的方式，如果取值为true，代表流式调用，此时大模型会生成一点儿数据，就给客户端响应一点儿数据，最终通过多次响应的方式把所有的结果响应完毕。如果取值为false，代表阻塞式调用，此时大模型会等待将所有的内容生成完毕，然后再一次性的响应给客户端。默认情况下stream的取值为false，下面是两种不同调用方案的演示案例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251230223409748.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;enable_search代表是否开启联网搜索，由于大模型训练完毕后，它的知识库不再更新了，比如大模型时2023年10月训练完毕的，那么2023年10月以后新产生的数据，大模型就无法感知了，如果要让大模型可以根据最新的数据回答问题，其中有一种解决方案就是开启联网搜索，大模型可以根据联网搜索的结果生成最终的答案。默认情况下enable_seach为false，也就是不开启，如果要开启联网搜索，需要手动设置请求参数enable_search为true。下面是一个演示案例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251230223636647.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;开启后：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251230223745233.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;响应数据&lt;/h4&gt;
&lt;p&gt;在与大模型交互的过程中，大模型响应的数据是json格式的数据，下面是一份响应数据的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;choices&quot;: [
        {
            &quot;message&quot;: {
                &quot;role&quot;: &quot;assistant&quot;,
                &quot;content&quot;: &quot;以下是根据最新信息整理的几条重要新闻，均已标注准确日期：\n\n*   **2026年大规模设备更新和消费品以旧换新政策将实施**：国家发展改革委、财政部于2025年12月30日印发《关于2026年实施大规模设备更新和消费品以旧换新政策的通知》。\n*   **韩国总统李在明将访华**：外交部发言人于2025年12月30日就韩国总统李在明将访华一事答记者问。\n*   **中柬泰三方再次聚首并达成共识**：外交部于2025年12月30日表示，中柬泰三方再次聚首并达成共识，充分彰显了中国负责任大国的形象。\n*   **我国力争到2030年累计制修订制造业中试标准100项以上**：工业和信息化部于2025年12月30日发布相关通知，提出此目标。\n*   **宁夏数据条例将于2026年1月1日起施行**：该条例于2025年12月30日公布。\n*   **国家智慧教育公共服务平台用户总量突破1.78亿**：此数据于2025年12月30日发布。\n*   **国务院公布《中华人民共和国增值税法实施条例》**：李强签署国务院令，于2025年12月30日公布该条例。&quot;
            },
            &quot;finish_reason&quot;: &quot;stop&quot;,
            &quot;index&quot;: 0,
            &quot;logprobs&quot;: null
        }
    ],
    &quot;object&quot;: &quot;chat.completion&quot;,
    &quot;usage&quot;: {
        &quot;prompt_tokens&quot;: 4301,
        &quot;completion_tokens&quot;: 322,
        &quot;total_tokens&quot;: 4623,
        &quot;prompt_tokens_details&quot;: {
            &quot;cached_tokens&quot;: 0
        }
    },
    &quot;created&quot;: 1767105514,
    &quot;system_fingerprint&quot;: null,
    &quot;model&quot;: &quot;qwen-flash&quot;,
    &quot;id&quot;: &quot;chatcmpl-ab754db3-709e-99fe-bde5-fd30f18b9aee&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;choices: 模型生成的内容数组，可以包含一条或多条内容&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;message: 本次调用模型输出的消息&lt;/li&gt;
&lt;li&gt;finish_reason: 自然结束(stop)，生成内容过长(length)&lt;/li&gt;
&lt;li&gt;index: 当前内容在choices数组中的索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;object: 始终为chat.completion, 无需关注&lt;/p&gt;
&lt;p&gt;usage: 本次对话过程中使用的token信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;prompt_tokens: 用户的输入转换成token的个数&lt;/li&gt;
&lt;li&gt;completion_tokens: 模型生成的回复转换成token的个数&lt;/li&gt;
&lt;li&gt;total_tokens: 用户输入和模型生成的总token个数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;created: 本次会话被创建时的时间戳&lt;/p&gt;
&lt;p&gt;system_fingerprint: 固定为null，无需关注&lt;/p&gt;
&lt;p&gt;model: 本次会话使用的模型名称&lt;/p&gt;
&lt;p&gt;id: 本次调用的唯一标识符&lt;/p&gt;
&lt;p&gt;重点关注choices和usage，其中choices里面封装的是大模型响应给客户端的核心数据，也就是用户问题的答案。而usage代表本次对话过程中使用的token信息&lt;/p&gt;
&lt;p&gt;在大语言模型中，token 是大模型处理文本的基本单位，可以理解为模型&quot;看得懂&quot;的最小文本片段,用户输入的内容都需要转换成token，才能让大模型更好的处理。将来文本要转化成token，需要使用到一个叫分词器的东西，不同的分词器，相同的文本转化成token的个数不完全一致，但是目前大部分分词器在处理英文的时候，一个token大概等于4个字符，而处理中文的时候，一个汉字字符大概等于1~2个token。&lt;/p&gt;
&lt;p&gt;通过API调用百炼平台提供的大模型，是按照流量收费的, 其实更准确的说法应该是按照token数量进行收费。&lt;/p&gt;
&lt;h2&gt;LangChain4j&lt;/h2&gt;
&lt;h3&gt;快速入门&lt;/h3&gt;
&lt;p&gt;创建一个普通的maven工程（JDK17）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引入依赖&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;dev.langchain4j&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;langchain4j-open-ai&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.0.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置环境变量API_KEY&lt;/p&gt;
&lt;p&gt;打开环境变量&lt;/p&gt;
&lt;p&gt;在用户变量新建
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251231000544580.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class App {
    public static void main(String[] args) {
        // 构建OpenAiChatModel对象
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl(&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;)
                .apiKey(System.getenv(&quot;API_KEY&quot;))  // 写死也可以（不安全）
                .modelName(&quot;qwen-flash&quot;)
                .build();
        // 调用chat方法交互
        String result = model.chat(&quot;介绍你自己&quot;);
        System.out.println(&quot;result = &quot; + result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
Exception in thread &quot;main&quot; dev.langchain4j.exception.AuthenticationException: {&quot;error&quot;:{&quot;message&quot;:&quot;You didn&apos;t provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY). &quot;,&quot;type&quot;:&quot;invalid_request_error&quot;,&quot;param&quot;:null,&quot;code&quot;:null},&quot;request_id&quot;:&quot;95182d29-56ad-929e-b0cc-85d0e041ce01&quot;}
	at dev.langchain4j.internal.ExceptionMapper$DefaultExceptionMapper.mapHttpStatusCode(ExceptionMapper.java:59)
	at dev.langchain4j.internal.ExceptionMapper$DefaultExceptionMapper.mapException(ExceptionMapper.java:44)
	at dev.langchain4j.internal.ExceptionMapper.withExceptionMapper(ExceptionMapper.java:31)
	at dev.langchain4j.internal.RetryUtils.lambda$withRetryMappingExceptions$2(RetryUtils.java:324)
	at dev.langchain4j.internal.RetryUtils$RetryPolicy.withRetry(RetryUtils.java:211)
	at dev.langchain4j.internal.RetryUtils.withRetry(RetryUtils.java:264)
	at dev.langchain4j.internal.RetryUtils.withRetryMappingExceptions(RetryUtils.java:324)
	at dev.langchain4j.internal.RetryUtils.withRetryMappingExceptions(RetryUtils.java:308)
	at dev.langchain4j.model.openai.OpenAiChatModel.doChat(OpenAiChatModel.java:142)
	at dev.langchain4j.model.chat.ChatModel.chat(ChatModel.java:46)
	at dev.langchain4j.model.chat.ChatModel.chat(ChatModel.java:77)
	at com.zzyang.langchain4j.App.main(App.java:17)
Caused
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
idea只会在打开时，读取一次系统变量，“重启”idea才会读取新的环境变量。如果重启无效，可以使用“管理员身份”运行idea，或配置系统环境变量；
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;打印日志信息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了查看与大模型交互过程中具体发送的请求消息和大模型响应的数据，可以打开日志开关，我们只需要在构建OpenAiChatModel对象的时候调用logRequests和logResponses方法设置一下即可。(需要引入logback依赖)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--    logback 依赖--&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;logback-classic&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;1.5.18&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class App {
    public static void main(String[] args) {
        // 构建OpenAiChatModel对象
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl(&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;)
                .apiKey(System.getenv(&quot;API_KEY&quot;))  // 写死也可以（不安全）
                .modelName(&quot;qwen-flash&quot;)
                .logRequests(true)  // 请求日志
                .logResponses(true) // 响应日志
                .build();
        // 调用chat方法交互
        String result = model.chat(&quot;介绍你自己&quot;);
        System.out.println(&quot;result = &quot; + result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;00:04:55.235 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP request:
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...c4], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  &quot;model&quot; : &quot;qwen-flash&quot;,
  &quot;messages&quot; : [ {
    &quot;role&quot; : &quot;user&quot;,
    &quot;content&quot; : &quot;介绍你自己&quot;
  } ],
  &quot;stream&quot; : false
}

00:04:56.522 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP response:
- status code: 200
- headers: [:status: 200], [content-length: 722], [content-type: application/json], [date: Tue, 30 Dec 2025 16:04:54 GMT], [req-arrive-time: 1767110693465], [req-cost-time: 1005], [resp-start-time: 1767110694471], [server: istio-envoy], [vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding], [x-dashscope-call-gateway: true], [x-envoy-upstream-service-time: 1004], [x-request-id: 60520db4-63a4-90e3-90f8-18d216eb35df]
- body: {&quot;choices&quot;:[{&quot;message&quot;:{&quot;role&quot;:&quot;assistant&quot;,&quot;content&quot;:&quot;你好！我是通义千问（Qwen），是阿里巴巴集团旗下的通义实验室自主研发的超大规模语言模型。我能够回答问题、创作文字，比如写故事、写公文、写邮件、写剧本、逻辑推理、编程等等，还能表达观点，玩游戏等。如果你有任何问题或需要帮助，欢迎随时告诉我！&quot;},&quot;finish_reason&quot;:&quot;stop&quot;,&quot;index&quot;:0,&quot;logprobs&quot;:null}],&quot;object&quot;:&quot;chat.completion&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:10,&quot;completion_tokens&quot;:71,&quot;total_tokens&quot;:81,&quot;prompt_tokens_details&quot;:{&quot;cached_tokens&quot;:0}},&quot;created&quot;:1767110694,&quot;system_fingerprint&quot;:null,&quot;model&quot;:&quot;qwen-flash&quot;,&quot;id&quot;:&quot;chatcmpl-60520db4-63a4-90e3-90f8-18d216eb35df&quot;}

result = 你好！我是通义千问（Qwen），是阿里巴巴集团旗下的通义实验室自主研发的超大规模语言模型。我能够回答问题、创作文字，比如写故事、写公文、写邮件、写剧本、逻辑推理、编程等等，还能表达观点，玩游戏等。如果你有任何问题或需要帮助，欢迎随时告诉我！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;引入 langchain4j-open-ai 依赖&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构建 OpenAIChatModel 对象&lt;br /&gt;
配置 url、api-key、模型名称&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用 chat 方法完成对话&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入 logback 依赖，并设置 logRequests 和 logResponses&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Spring整合LangChain4j&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;创建SpringBoot项目&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251231001359361.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251231001447197.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;引入LangChain4j起步依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;dev.langchain4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;langchain4j-open-ai-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0.1-beta6&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在application.yml中配置调用大模型的信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;langchain4j:
  open-ai:
    chat-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API_KEY}
      model-name: qwen-flash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;起步依赖会检测到配置信息，自动的往IOC容器中注入一个OpenAiChatModel对象。&lt;/p&gt;
&lt;p&gt;开发接口，调用大模型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class ChatController {


    private final OpenAiChatModel model;

    @GetMapping(&quot;/chat&quot;)
    public String chat(String message) {
        String res = model.chat(message);
        return res;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看日志信息(引入下lombom依赖)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;langchain4j:
  open-ai:
    chat-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API_KEY}
      model-name: qwen-plus
      log-requests: true #请求消息日志
      log-responses: true #响应消息日志
logging:
  level:
    dev.langchain4j: debug #日志级别
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用一下
&lt;code&gt;http://localhost:8080/chat?message=你好&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251231002422576.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;AiServices工具类&lt;/h3&gt;
&lt;p&gt;LangChain4j提供的工具类AiServices，在之前的案例中，我们访问大模型是借助于OpenAiChatModel的chat方法完成的。其实这种方式在实际开发中并不是很常用，因为如果使用这种方式调用大模型，将来我们完成一些高阶的功能，比如会话记忆/RAG知识库/Tools工具的时候，在调用chat方法访问大模型前，我们需要自己做很多很多的工作，完成起来是比较复杂的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20260104221842529.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了简化我们程序员的使用，LangChain4j提供了AiServices工具类，封装了有关model对象和其它一些功能的操作，用起来会非常简单&lt;/p&gt;
&lt;h4&gt;AiServices工具类基本使用&lt;/h4&gt;
&lt;p&gt;引入AiServices相关依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;dev.langchain4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;langchain4j-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0.1-beta6&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;声明用于封装聊天方法的接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface ConsultantService {
    //用于聊天的方法,message为用户输入的内容
    String chat(String message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用AiServices工具类创建接口的动态代理对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class CommonConfig {
    @Autowired
    private OpenAiChatModel model;
    @Bean         
    public ConsultantService consultantService() {
        ConsultantService cs = AiServices.builder(ConsultantService.class)
                .chatModel(model)//设置对话时使用的模型对象
                .build();
        return cs;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ChatController中注入ConsultantService并使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class ChatController {
    @Autowired
    private ConsultantService consultantService;    // // 不再注入OpenAiChatModel了，而是注入接口的代理对象
    @RequestMapping(&quot;/chat&quot;)
    public String chat(String message){
        String result = consultantService.chat(message);
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;AiServices工具类声明式使用&lt;/h4&gt;
&lt;p&gt;为了简化AIServices工具类的使用，LangChain4j提供了声明式使用方法，想为哪个接口创建代理对象，只需要在该接口上添加@AiService注解并指定要使用的模型，将来LangChain4j扫描到该注解后会自动的创建该接口的代理对象并注入到IOC容器中。接下来修改ConsultantService中的代码，并重新测试。&lt;/p&gt;
&lt;p&gt;先删掉我们的CommonConfig类，然后在ConsultantService接口上使用&lt;code&gt;AiService&lt;/code&gt;注解，声明式的指定模型对象和装配模式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,  // 手动装配
        chatModel = &quot;openAiChatModel&quot;           // 指定模型
)
public interface ConsultantService {
       //用于聊天的方法,message为用户输入的内容
    public String chat(String message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AiService&lt;/code&gt;注解的&lt;code&gt;wiringMode&lt;/code&gt;用于指定装配模式，默认的取值为&lt;code&gt;AiServiceWiringMode.AUTOMATIC&lt;/code&gt;，表示自动装配的意思，这里咱们设置为手动装配：&lt;code&gt;AiServiceWiringMode.EXPLICIT&lt;/code&gt;。chatModel注解用于指定对话时需要使用的模型对象在IOC容器中的名字，由于IOC容器中Bean对象的名字默认是类名首字母小写，所以这里的取值为 &lt;code&gt;openAiChatModel&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;实际上，在使用AiService注解时，我们不手动的指定这两个属性的值，也就是说采用AiService的自动装配模式也是可以的。如果配置了多个模型的话是需要手动指定调用的是哪个模型的;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AiService
public interface ConsultantService {
       //用于聊天的方法,message为用户输入的内容
    String chat(String message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;流式调用&lt;/h3&gt;
&lt;p&gt;调用大模型有两种方式：流式调用和阻塞式调用。在我们前面演示的过程中，其实都是用的是阻塞式调用, 结果是一次性响应的&lt;/p&gt;
&lt;h4&gt;流式调用步骤&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;引入依赖&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-webflux&amp;lt;/artifactId&amp;gt;
 &amp;lt;/dependency&amp;gt;
 &amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;dev.langchain4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;langchain4j-reactor&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0.1-beta6&amp;lt;/version&amp;gt;
 &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配置流式模型对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;之前咱们配置的是阻塞式对话模型对象，在流式调用中，我们需要使用LangChain4j的流式模型对象。和之前一样，也需要在配置文件中完成配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;langchain4j:
  open-ai:
    chat-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API_KEY}
      model-name: qwen-plus
      log-requests: true
      log-responses: true
    streaming-chat-model:       # 新增
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${API_KEY}
      model-name: qwen-plus
      log-requests: true
      log-responses: true
logging:
  level:
    dev.langchain4j: debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调整ConsultantService中的代码&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ConsultantService&lt;/code&gt;中的chat方法的返回值类型，需要修改为支持流式处理的类型&lt;code&gt;Flux&lt;/code&gt;，同时还需要在&lt;code&gt;AiService&lt;/code&gt;注解中，通过&lt;code&gt;streamingChatModel&lt;/code&gt;属性, 配置一下流式调用的模型对象，值为&lt;code&gt;openAistreamingChatModel&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = &quot;openAiChatModel&quot;,
        streamingChatModel = &quot;openAiStreamingChatModel&quot;
)
public interface ConsultantService {
    public Flux&amp;lt;String&amp;gt; chat(String message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调整ChatController中的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class ChatController {
    @Autowired
    private ConsultantService consultantService;

    @RequestMapping(value = &quot;/chat&quot;,produces = &quot;text/html;charset=utf-8&quot;)       // 解决乱码问题
    public Flux&amp;lt;String&amp;gt; chat(String memoryId,String message){
        Flux&amp;lt;String&amp;gt; result = consultantService.chat(memoryId,message);
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中@RequestMapping注解的produces属性，用于解决乱码问题。&lt;/p&gt;
&lt;p&gt;对接前端页面&lt;/p&gt;
&lt;p&gt;搞一个简单的demo页面，放到resources/static目录下，访问&lt;code&gt;http://localhost:8080/index.html&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;zh-CN&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title&amp;gt;AI志愿填报顾问&amp;lt;/title&amp;gt;
    &amp;lt;link href=&quot;https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css&quot;&amp;gt;
    &amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue@3.2.31/dist/vue.global.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
        body {
            font-family: &apos;Inter&apos;, -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
        }

        /* 滚动条样式 */
        ::-webkit-scrollbar {
            width: 6px;
        }

        ::-webkit-scrollbar-track {
            background: #f1f1f1;
        }

        ::-webkit-scrollbar-thumb {
            background: #c1c1c1;
            border-radius: 3px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: #a8a8a8;
        }

        /* 输入框自适应高度 */
        textarea {
            min-height: 44px;
            max-height: 200px;
            transition: height 0.2s;
        }

        /* 加载动画 */
        @keyframes pulse {
            0%, 100% {
                opacity: 0.5;
            }
            50% {
                opacity: 1;
            }
        }

        .animate-pulse {
            animation: pulse 1.5s infinite;
        }

        .delay-100 {
            animation-delay: 0.1s;
        }

        .delay-200 {
            animation-delay: 0.2s;
        }

        /* 打字机效果 */
        .typing-cursor::after {
            content: &quot;|&quot;;
            animation: blink 1s step-end infinite;
        }

        @keyframes blink {
            from, to {
                opacity: 1;
            }
            50% {
                opacity: 0;
            }
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div id=&quot;app&quot; class=&quot;flex flex-col h-screen bg-gray-50&quot;&amp;gt;
    &amp;lt;!-- 顶部导航栏 --&amp;gt;
    &amp;lt;header class=&quot;bg-white shadow-sm py-3 px-4 flex items-center justify-between&quot;&amp;gt;
        &amp;lt;div class=&quot;flex items-center&quot;&amp;gt;
            &amp;lt;div class=&quot;text-xl font-bold text-blue-600&quot;&amp;gt;AI志愿填报顾问&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;flex items-center space-x-3&quot;&amp;gt;
            &amp;lt;button
                    @click=&quot;startNewConversation&quot;
                    class=&quot;ml-2 p-3 rounded-lg bg-green-500 hover:bg-green-600 text-white&quot; style=&quot;width: 50px&quot;&amp;gt;
                &amp;lt;i class=&quot;fas fa-plus&quot;&amp;gt;&amp;lt;/i&amp;gt;
            &amp;lt;/button&amp;gt;
            &amp;lt;button @click=&quot;toggleDarkMode&quot; class=&quot;p-2 rounded-full hover:bg-gray-100&quot;&amp;gt;
                &amp;lt;i :class=&quot;darkMode ? &apos;fas fa-moon text-gray-600&apos; : &apos;fas fa-sun text-gray-600&apos;&quot;&amp;gt;&amp;lt;/i&amp;gt;
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/header&amp;gt;

    &amp;lt;!-- 聊天内容区域 --&amp;gt;
    &amp;lt;main class=&quot;flex-1 overflow-y-auto p-4 space-y-6&quot; ref=&quot;chatContainer&quot; :class=&quot;{ &apos;bg-gray-800&apos;: darkMode }&quot;&amp;gt;
        &amp;lt;div v-for=&quot;(message, index) in messages&quot; :key=&quot;index&quot; class=&quot;max-w-3xl mx-auto&quot;&amp;gt;
            &amp;lt;div :class=&quot;[&apos;flex&apos;, message.role === &apos;user&apos; ? &apos;justify-end&apos; : &apos;justify-start&apos;]&quot;&amp;gt;
                &amp;lt;div :class=&quot;[&apos;flex items-start space-x-3&apos;, message.role === &apos;user&apos; ? &apos;flex-row-reverse space-x-reverse&apos; : &apos;&apos;]&quot;&amp;gt;
                    &amp;lt;div :class=&quot;[&apos;w-8 h-8 rounded-full flex items-center justify-center&apos;,
                                    message.role === &apos;user&apos; ? &apos;bg-blue-100 text-blue-600&apos; : &apos;bg-green-100 text-green-600&apos;,
                                    darkMode &amp;amp;&amp;amp; message.role === &apos;assistant&apos; ? &apos;bg-gray-700 text-green-400&apos; : &apos;&apos;]&quot;&amp;gt;
                        &amp;lt;i :class=&quot;message.role === &apos;user&apos; ? &apos;fas fa-user&apos; : &apos;fas fa-robot&apos;&quot;&amp;gt;&amp;lt;/i&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div :class=&quot;[&apos;p-3 rounded-lg max-w-lg&apos;,
                                    message.role === &apos;user&apos;
                                        ? &apos;bg-blue-500 text-white&apos;
                                        : darkMode
                                            ? &apos;bg-gray-700 text-gray-100 border-gray-600&apos;
                                            : &apos;bg-white shadow border border-gray-100&apos;]&quot;&amp;gt;
                        &amp;lt;div v-if=&quot;message.role === &apos;assistant&apos; &amp;amp;&amp;amp; message.isLoading&quot; class=&quot;flex space-x-2&quot;&amp;gt;
                            &amp;lt;div :class=&quot;[&apos;w-2 h-2 rounded-full&apos;, darkMode ? &apos;bg-gray-400&apos; : &apos;bg-gray-300&apos;, &apos;animate-pulse&apos;]&quot;&amp;gt;&amp;lt;/div&amp;gt;
                            &amp;lt;div :class=&quot;[&apos;w-2 h-2 rounded-full&apos;, darkMode ? &apos;bg-gray-400&apos; : &apos;bg-gray-300&apos;, &apos;animate-pulse delay-100&apos;]&quot;&amp;gt;&amp;lt;/div&amp;gt;
                            &amp;lt;div :class=&quot;[&apos;w-2 h-2 rounded-full&apos;, darkMode ? &apos;bg-gray-400&apos; : &apos;bg-gray-300&apos;, &apos;animate-pulse delay-200&apos;]&quot;&amp;gt;&amp;lt;/div&amp;gt;
                        &amp;lt;/div&amp;gt;
                        &amp;lt;div v-else class=&quot;whitespace-pre-wrap&quot;&amp;gt;
                                &amp;lt;span v-for=&quot;(char, charIndex) in message.content&quot; :key=&quot;charIndex&quot;
                                      :class=&quot;{&apos;opacity-0&apos;: charIndex &amp;gt;= message.visibleChars, &apos;fade-in&apos;: charIndex &amp;lt; message.visibleChars}&quot;&amp;gt;
                                    {{ char }}
                                &amp;lt;/span&amp;gt;
                            &amp;lt;span v-if=&quot;message.isStreaming&quot; class=&quot;typing-cursor&quot;&amp;gt;&amp;lt;/span&amp;gt;
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt;

    &amp;lt;!-- 输入框区域 --&amp;gt;
    &amp;lt;footer :class=&quot;[&apos;border-t p-4&apos;, darkMode ? &apos;bg-gray-800 border-gray-700&apos; : &apos;bg-white border-gray-200&apos;]&quot;&amp;gt;
        &amp;lt;div class=&quot;max-w-3xl mx-auto relative&quot;&amp;gt;
            &amp;lt;div class=&quot;flex items-center&quot;&amp;gt;
                    &amp;lt;textarea
                            v-model=&quot;userInput&quot;
                            @keydown.enter.exact.prevent=&quot;sendMessage&quot;
                            @keydown.ctrl.enter.exact.prevent=&quot;sendMessage&quot;
                            @keydown.esc.exact=&quot;stopResponse&quot;
                            placeholder=&quot;输入您的问题...&quot;
                            :class=&quot;[&apos;flex-1 border rounded-lg py-3 px-4 pr-12 focus:outline-none focus:ring-2 resize-none&apos;,
                                darkMode
                                    ? &apos;bg-gray-700 border-gray-600 text-white focus:ring-blue-400 placeholder-gray-400&apos;
                                    : &apos;border-gray-300 focus:ring-blue-500 focus:border-transparent&apos;]&quot;
                            rows=&quot;1&quot;
                            ref=&quot;textarea&quot;
                            @input=&quot;adjustTextareaHeight&quot;
                    &amp;gt;&amp;lt;/textarea&amp;gt;
                &amp;lt;!-- 新建会话按钮 --&amp;gt;

                &amp;lt;button
                        @click=&quot;isLoading ? stopResponse() : sendMessage()&quot;
                        :disabled=&quot;!userInput.trim() &amp;amp;&amp;amp; !isLoading&quot;
                        :class=&quot;[&apos;ml-2 p-3 rounded-lg&apos;,
                                isLoading
                                    ? &apos;bg-red-500 hover:bg-red-600 text-white&apos;
                                    : &apos;bg-blue-500 hover:bg-blue-600 text-white&apos;,
                                &apos;disabled:opacity-50 disabled:cursor-not-allowed&apos;]&quot;
                &amp;gt;
                    &amp;lt;i :class=&quot;isLoading ? &apos;fas fa-stop&apos; : &apos;fas fa-paper-plane&apos;&quot;&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/footer&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
    const {createApp, ref, nextTick, onMounted, watch} = Vue;

    createApp({
        setup() {
            const messages = ref([]);
            const userInput = ref(&apos;&apos;);
            const isLoading = ref(false);
            const chatContainer = ref(null);
            const textarea = ref(null);
            const darkMode = ref(false);

            const memoeryId = ref(Date.now().toString());
            let controller = null;
            let typingInterval = null;
            let currentTypingIndex = 0;

            // 调整文本区域高度
            const adjustTextareaHeight = () =&amp;gt; {
                const textareaEl = textarea.value;
                textareaEl.style.height = &apos;auto&apos;;
                textareaEl.style.height = `${Math.min(textareaEl.scrollHeight, 200)}px`;
            };

            // 滚动到底部
            const scrollToBottom = () =&amp;gt; {
                nextTick(() =&amp;gt; {
                    if (chatContainer.value) {
                        chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
                    }
                });
            };

            // 切换暗黑模式
            const toggleDarkMode = () =&amp;gt; {
                darkMode.value = !darkMode.value;
                localStorage.setItem(&apos;darkMode&apos;, darkMode.value);
            };

            // 新建会话
            const startNewConversation = () =&amp;gt; {
                // 清空聊天记录
                messages.value = [];
                // 生成新的 memoryId
                memoeryId.value = Date.now().toString();
                // 添加欢迎消息
                messages.value.push({
                    role: &apos;assistant&apos;,
                    content: &apos;你好！我是zzy教育提供的AI志愿填报顾问，请问有什么能帮到您？&apos;,
                    isLoading: false,
                    visibleChars: 0,
                    isStreaming: false
                });
                // 确保欢迎消息完全可见
                messages.value[0].visibleChars = messages.value[0].content.length;
                // 滚动到底部
                scrollToBottom();
                // 聚焦输入框
                nextTick(() =&amp;gt; {
                    textarea.value.focus();
                });
            };

            // 模拟逐字打印效果
            const startTypingEffect = (messageIndex) =&amp;gt; {
                const message = messages.value[messageIndex];
                if (!message || message.visibleChars &amp;gt;= message.content.length) {
                    clearInterval(typingInterval);
                    typingInterval = null;
                    messages.value[messageIndex].isStreaming = false;
                    return;
                }

                messages.value[messageIndex].visibleChars++;
                scrollToBottom();
            };

            // 发送消息
            const sendMessage = async () =&amp;gt; {
                if (!userInput.value.trim() || isLoading.value) return;

                // 中止之前的请求
                if (controller) {
                    controller.abort();
                }
                controller = new AbortController();

                const userMessage = {
                    role: &apos;user&apos;,
                    content: userInput.value.trim(),
                    isLoading: false,
                    visibleChars: userInput.value.trim().length,
                    isStreaming: false
                };

                messages.value.push(userMessage);

                const assistantMessage = {
                    role: &apos;assistant&apos;,
                    content: &apos;&apos;,
                    isLoading: true,
                    visibleChars: 0,
                    isStreaming: true
                };

                messages.value.push(assistantMessage);

                userInput.value = &apos;&apos;;
                adjustTextareaHeight();
                scrollToBottom();

                isLoading.value = true;

                try {
                    const response = await fetch(`/chat?message=${encodeURIComponent(userMessage.content)}&amp;amp;memoryId=${memoeryId.value}`, {
                        signal: controller.signal
                    });

                    if (!response.ok) {
                        throw new Error(`HTTP error! status: ${response.status}`);
                    }

                    const reader = response.body.getReader();
                    const decoder = new TextDecoder(&apos;utf-8&apos;);
                    let buffer = &apos;&apos;;
                    let messageIndex = messages.value.length - 1;

                    // 先清除之前的打字效果
                    if (typingInterval) {
                        clearInterval(typingInterval);
                        typingInterval = null;
                    }

                    // 开始流式处理
                    while (true) {
                        const {done, value} = await reader.read();
                        if (done) break;

                        const chunk = decoder.decode(value, {stream: true});
                        buffer += chunk;

                        // 直接更新内容
                        messages.value[messageIndex].content = buffer;
                        messages.value[messageIndex].isLoading = false;

                        // 启动打字效果
                        if (!typingInterval) {
                            typingInterval = setInterval(() =&amp;gt; {
                                startTypingEffect(messageIndex);
                            }, 20); // 调整这个值可以改变打字速度
                        }

                        scrollToBottom();
                    }

                } catch (error) {
                    if (error.name === &apos;AbortError&apos;) {
                        console.log(&apos;请求被用户中止&apos;);
                    } else {
                        console.error(&apos;请求出错:&apos;, error);
                        const lastMessage = messages.value[messages.value.length - 1];
                        lastMessage.content = &apos;抱歉，请求过程中出现错误: &apos; + error.message;
                        lastMessage.visibleChars = lastMessage.content.length;
                    }
                } finally {
                    const lastMessage = messages.value[messages.value.length - 1];
                    lastMessage.isLoading = false;
                    lastMessage.isStreaming = false;

                    // 确保所有字符都可见
                    if (lastMessage.visibleChars &amp;lt; lastMessage.content.length) {
                        lastMessage.visibleChars = lastMessage.content.length;
                    }

                    isLoading.value = false;
                    controller = null;

                    if (typingInterval) {
                        clearInterval(typingInterval);
                        typingInterval = null;
                    }

                    scrollToBottom();
                }
            };

            // 停止响应
            const stopResponse = () =&amp;gt; {
                if (controller) {
                    controller.abort();
                    const lastMessage = messages.value[messages.value.length - 1];
                    lastMessage.isLoading = false;
                    lastMessage.isStreaming = false;

                    if (lastMessage.visibleChars &amp;lt; lastMessage.content.length) {
                        lastMessage.visibleChars = lastMessage.content.length;
                    }

                    isLoading.value = false;
                    controller = null;

                    if (typingInterval) {
                        clearInterval(typingInterval);
                        typingInterval = null;
                    }
                }
            };

            // 初始化
            onMounted(() =&amp;gt; {
                // 检查暗黑模式偏好
                darkMode.value = localStorage.getItem(&apos;darkMode&apos;) === &apos;true&apos; ||
                    (window.matchMedia &amp;amp;&amp;amp; window.matchMedia(&apos;(prefers-color-scheme: dark)&apos;).matches);

                // 添加欢迎消息
                messages.value.push({
                    role: &apos;assistant&apos;,
                    content: &apos;你好！我是传智教育提供的AI志愿填报顾问，请问有什么能帮到您？&apos;,
                    isLoading: false,
                    visibleChars: 0,
                    isStreaming: false
                });

                // 确保欢迎消息完全可见
                messages.value[0].visibleChars = messages.value[0].content.length;
                scrollToBottom();

                // 聚焦输入框
                nextTick(() =&amp;gt; {
                    textarea.value.focus();
                });
            });

            // 监听消息变化自动滚动
            watch(messages, scrollToBottom, {deep: true});

            return {
                messages,
                userInput,
                isLoading,
                darkMode,
                chatContainer,
                textarea,
                sendMessage,
                stopResponse,
                toggleDarkMode,
                adjustTextareaHeight,
                startNewConversation
            };
        }
    }).mount(&apos;#app&apos;);
&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后即可进行测试了&lt;/p&gt;
&lt;h3&gt;消息注解&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;SystemMessage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SystemMessage，顾名思义，它是用于设置系统消息的，你可以直接在接口的方法上添加这个注解，在注解中书写系统消息即可。当然了, 如果我们的系统消息很长, 直接在代码中写不方便，它还提供了另外一种使用方式，通过fromResource属性，指定一个外部的文件。这样我们就可以把系统消息一次性的写入到外部文件中，管理起来也比较方便。&lt;/p&gt;
&lt;p&gt;将系统提示词粘贴到resources目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你是code视界提供的专业的AI志愿填报顾问，可以给用户提供如下功能：
1.查询目标院校的院校简介
2.查询目标院校的录取规则
3.查询目标院校的奖学金设置状况
4.查询目标院校的食宿条件
5.查询目标院校招生联系方式
6.查询目标院2024年不同专业录取情况
7.查询热门专业
8.查询天坑专业
9.根据学生提供的分数和不同学校以及学校历年录取分数，推荐合适的学校和专业，每次根据匹配度，按照冲、稳、保的逻辑，罗列出合适的学校以及专业，给用户呈现时需要呈现学校名称、专业名称、历年录取分数以及专业热度
10.高考志愿填报一对一沟通预约服务
11.查询志愿指导服务预约详情
说明：
    1.每次回答完用户问题，最后都加上一句话：&amp;lt;br/&amp;gt;志愿填报需要考虑的因素有很多，如果要得到专业的志愿填报指导，建议您预约一个一对一的指导服务，是否需要预约？
    2.下预约单需要用户提供生姓名、考生性别、考生电话、考生预约沟通时间(日期+时间)、考生所在省份、考生预估分数，当用户表达出需要预约志愿指导服务的意愿后，你不能自己模拟这些数据下预约单，而是需要以委婉的方式引导用户提供考生姓名、考生性别、考生电话、考生预约沟通时间(日期+时间)、考生所在省份、考生预估分数,这些信息必须是用户全部提供，不能有模拟数据，否则不要下预约单
    3.一旦预约成功，最后不要再跟上面第1条指定的话术，而是更改为：恭喜您，一对一志愿指导服务已经预约成功，我们会准时联系您，请注意接听电话！
    4.给用户的回复中，不要提及类似&quot;根据您提供的信息/根据资料中的信息&quot;这样的话术

你是传智教育提供的智能志愿填报咨询师，只回答有关高考志愿填报的问题，其它问题不予回答。

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = &quot;openAiChatModel&quot;,
        streamingChatModel = &quot;openAiStreamingChatModel&quot;
)
public interface ConsultantService {
    //用于聊天的方法,message为用户输入的内容
//    @SystemMessage(&quot;你是张紫阳助手小阿巴&quot;)
    @SystemMessage(fromResource = &quot;system.txt&quot;)
    Flux&amp;lt;String&amp;gt; chat(String message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20260106233258094.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UserMessage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设现在没有SystemMessage，那么我们可以借助于UserMessage注解完成同样的效果，我们可以在用户消息前后，拼接提前预设的内容下面给出一个使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = &quot;openAiChatModel&quot;,
        streamingChatModel = &quot;openAiStreamingChatModel&quot;
)
public interface ConsultantService {

    @UserMessage(&quot;你是张紫阳助手小阿巴,{{it}}&quot;)
    Flux&amp;lt;String&amp;gt; chat(String message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20260106233756667.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实际请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...c4], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  &quot;model&quot; : &quot;qwen-flash&quot;,
  &quot;messages&quot; : [ {
    &quot;role&quot; : &quot;user&quot;,
    &quot;content&quot; : &quot;你是张紫阳助手小阿巴,你是谁？&quot;
  } ],
  &quot;stream&quot; : true,
  &quot;stream_options&quot; : {
    &quot;include_usage&quot; : true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&quot;你是谁？&quot; 被替换了&lt;/p&gt;
&lt;p&gt;上面示例中的参数message是用户传递的消息，我们在使用UserMessage注解的时候，可以通过{{it}}的方式, 动态的获取到用户传递的消息，然后再往它的前后拼接上预设的内容即可，想拼什么拼什么。
这里有一点需要说明，这个花括号内的it是固定的，不能随便写。假设你不想使用it这个名字，langchain4j提供了一个V注解，用于解决这个问题。我们在参数前面通过V注解给这个参数起一个名字，然后在花括号内写上同样的名字就能获取到了，下面是一个使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import dev.langchain4j.service.V;


@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = &quot;openAiChatModel&quot;,
        streamingChatModel = &quot;openAiStreamingChatModel&quot;
)
public interface ConsultantService {

    @UserMessage(&quot;你是张紫阳助手小阿巴,{{msg}}&quot;)
    Flux&amp;lt;String&amp;gt; chat(@V(&quot;msg&quot;) String message);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Redis - 实战</title><link>https://zzyang.top/posts/redis-actualcombat/</link><guid isPermaLink="true">https://zzyang.top/posts/redis-actualcombat/</guid><pubDate>Thu, 18 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;短信登录&lt;/h2&gt;
&lt;h3&gt;基于Session实现登录的流程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251125234650839.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送验证码：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用户在提交手机号后，会校验手机号是否合法，如果不合法，则要求用户重新输入手机号。&lt;/p&gt;
&lt;p&gt;如果手机号合法，后台此时生成对应的验证码，同时将验证码进行保存，然后再通过短信的方式将验证码发送给用户&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;短信验证码登录、注册：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​ 用户将验证码和手机号进行输入，后台从session中拿到当前验证码，然后和用户输入的验证码进行校验，如果不一致，则无法通过校验，如果一致，则后台根据手机号查询用户，如果用户不存在，则为用户创建账号信息，保存到数据库，无论是否存在，都会将用户信息保存到session中，方便后续获得当前登录信息&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;校验登录状态&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;用户在请求时候，会从cookie中携带JsessionId到后台，后台通过JsessionId从session中拿到用户信息，如果没有session信息，则进行拦截，如果有session信息，则将用户信息保存到threadLocal中，并且放行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送验证码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
@Slf4j
public class UserInfoServiceImpl extends ServiceImpl&amp;lt;UserInfoMapper, UserInfo&amp;gt; implements IUserInfoService {

    @Override
    public Result sendCode(String phone, HttpSession session) {
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail(&quot;手机号格式错误&quot;);
        }
        // 生成验证码
        String code = RandomUtil.randomNumbers(6);
        // 保存验证码
        session.setAttribute(&quot;code&quot;, code);
        // 发送验证码
        log.info(&quot;发送的验证码：{}&quot;, code);
        return Result.ok();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;登录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String code = loginForm.getCode();
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail(&quot;手机号格式错误&quot;);
        }
        if (RegexUtils.isCodeInvalid(code)) {
            return Result.fail(&quot;手机号格式错误&quot;);
        }
        // 根据手机查用户
        Object cacheCode = session.getAttribute(&quot;code&quot;);
        if (cacheCode == null) {
            return Result.fail(&quot;请填写验证码&quot;);
        }
        if (!cacheCode.equals(code)) {
            return Result.fail(&quot;验证码错误&quot;);
        }
        // 用户是否存在
        User user = lambdaQuery().eq(User::getPhone, phone).one();
        // 不存在，注册
        if (user == null) {
            user = User.builder()
                    .phone(phone)
                    .nickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(6))
                    .build();
            save(user);
        }
        // 存在，写到session
        session.setAttribute(&quot;user&quot;, BeanUtil.copyProperties(user, UserDTO.class));
        return Result.ok();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
这里不需要返回登录凭证，session的原理是基于cookie,每一个session都会有一个唯一的sessionID，在访问tomcat时，这个session就已经写到cookie当中了，以后的每次请求都会带着sessionID
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;登录验证功能&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LoginInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        HttpSession session = request.getSession();
        Object user = session.getAttribute(&quot;user&quot;);
        if (ObjectUtil.isEmpty(user)) {
            response.setStatus(401);
            return false;
        }
        // 保存threadLocal
        UserHolder.saveUser((UserDTO) user);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        &quot;/user/code&quot;,
                        &quot;/user/login&quot;,
                        &quot;/blog/hot&quot;,
                        &quot;/shop/**&quot;,
                        &quot;/shot-type/**&quot;,
                        &quot;/voucher/**&quot;
                );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;集群的Session共享问题&lt;/h3&gt;
&lt;p&gt;session共享问题：多台Tomcat并不共享session存储空间，当请求切换到不同tomcat服务时导致数据丢失的问题。&lt;/p&gt;
&lt;p&gt;​ 每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat，并且把自己的信息存放到第一台服务器的session中，但是第二次这个用户访问到了第二台tomcat，那么在第二台服务器上，肯定没有第一台服务器存放的session，所以此时 整个登录拦截功能就会出现问题。早期的方案是session拷贝，就是说虽然每个tomcat上都有不同的session，但是每当任意一台服务器的session修改时，都会同步给其他的Tomcat服务器的session，这样的话，就可以实现session的共享了&lt;/p&gt;
&lt;p&gt;但是这种方案具有两个大问题&lt;/p&gt;
&lt;p&gt;1、每台服务器中都有完整的一份session数据，内存空间浪费。&lt;/p&gt;
&lt;p&gt;2、session拷贝数据时，可能会出现延迟，会出现数据不一致情况。&lt;/p&gt;
&lt;p&gt;session的替代方案应该满足：数据共享、内存存储，key、value结构。基于以上特性可以选择使用Redis代替Session。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251126203451781.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;基于Reids实现共享Session的登录&lt;/h3&gt;
&lt;p&gt;验证码保存：由于验证码只是简单的数字，故用String类型存储即可
后续用户要提交手机号和收到的验证码进行验证，可以将key
设置为&quot;&lt;strong&gt;phone:手机号&lt;/strong&gt;”的形式，既方便读取Redis中的验证
码，又保证了每个登录用户key的唯一性&lt;/p&gt;
&lt;p&gt;用户保存：使用Hash结构将用户对象保存到Redis
以随机token为key存储用户数据，保证key的唯一性&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251126205449356.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送验证码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;


    @Override
    public Result sendCode(String phone, HttpSession session) {
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail(&quot;手机号格式错误&quot;);
        }
        // 生成验证码
        String code = RandomUtil.randomNumbers(6);
        // 保存到redis
        redisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 发送验证码
        log.info(&quot;发送的验证码：{}&quot;, code);
        return Result.ok();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;登录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String code = loginForm.getCode();
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail(&quot;手机号格式错误&quot;);
        }
        if (RegexUtils.isCodeInvalid(code)) {
            return Result.fail(&quot;手机号格式错误&quot;);
        }
        // 根据手机查用户
        String cacheCode = (String) redisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
        if (cacheCode == null) {
            return Result.fail(&quot;请填写验证码&quot;);
        }
        if (!cacheCode.equals(code)) {
            return Result.fail(&quot;验证码错误&quot;);
        }
        // 用户是否存在
        User user = lambdaQuery().eq(User::getPhone, phone).one();
        // 不存在，注册
        if (user == null) {
            user = User.builder()
                    .phone(phone)
                    .nickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(6))
                    .build();
            save(user);
        }
        // 生成token （可换jwt）
        String token = UUID.randomUUID().toString(true);
        // 写到redis
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map&amp;lt;String, Object&amp;gt; userMap = BeanUtil.beanToMap(userDTO);
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        redisTemplate.opsForHash().putAll(tokenKey, userMap);
        redisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES);
        // 返回token
        return Result.ok(token);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;拦截器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequiredArgsConstructor
public class LoginInterceptor implements HandlerInterceptor {

    private final StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader(&quot;authorization&quot;);
        if (StrUtil.isBlank(token)) {
            response.setStatus(401);
            return false;
        }
        // 基于ToKEN获取redis中的用户
        Map&amp;lt;Object, Object&amp;gt; userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        if (userMap.isEmpty()) {
            response.setStatus(401);
            return false;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 刷新token有效期 (token续期)
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, 30, TimeUnit.MINUTES);
        // 保存threadLocal
        UserHolder.saveUser(userDTO);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {

    private final StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        &quot;/user/code&quot;,
                        &quot;/user/login&quot;,
                        &quot;/blog/hot&quot;,
                        &quot;/shop/**&quot;,
                        &quot;/shot-type/**&quot;,
                        &quot;/voucher/**&quot;
                );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;优化拦截器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当前拦截器有个问题，如果用户访问的一直都是放行的路径，那么就不会进入到拦截器中，所以拦截器中的token续期就无效了，所以我们需要再加一个拦截器（拦截所有路径的）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251126215158488.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequiredArgsConstructor
public class RefreshInterceptor implements HandlerInterceptor {

    private final StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader(&quot;authorization&quot;);
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 基于ToKEN获取redis中的用户
        Map&amp;lt;Object, Object&amp;gt; userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        if (userMap.isEmpty()) {
            return true;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 刷新token有效期 (token续期)
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, 30, TimeUnit.MINUTES);
        // 保存threadLocal
        UserHolder.saveUser(userDTO);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class LoginInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            response.setStatus(401);
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {

    private final StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        &quot;/user/code&quot;,
                        &quot;/user/login&quot;,
                        &quot;/blog/hot&quot;,
                        &quot;/shop/**&quot;,
                        &quot;/shot-type/**&quot;,
                        &quot;/voucher/**&quot;
                ).order(1);
        registry.addInterceptor(new RefreshInterceptor(stringRedisTemplate)).addPathPatterns(&quot;/**&quot;).order(0);   // 先执行
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;商户查询缓存&lt;/h2&gt;
&lt;h3&gt;缓存&lt;/h3&gt;
&lt;p&gt;缓存就是数据交换的缓冲区（cache），是存贮数据的临时地方，一般&lt;strong&gt;读写性能较高&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力，降低响应时间&lt;/p&gt;
&lt;p&gt;实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251126221956887.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251126222310493.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;缓存的成本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据一致性成本（需要保证缓存与数据库中数据的一致性）&lt;/li&gt;
&lt;li&gt;代码维护成本&lt;/li&gt;
&lt;li&gt;运维成本（为保证缓存高可用，需要搭建缓存集群，增加运维成本）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;添加Redis缓存&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;总体流程&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251127210950929.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;业务流程&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251127211013223.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;操作思路：查询数据库之前先查询缓存，如果缓存数据存在，则直接从缓存中返回，如果缓存数据不存在，再查询数据库，然后将数据存入redis并将数据返回。&lt;/p&gt;
&lt;p&gt;key设计为 “固定前缀+商铺id”的形式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    @Override
    public Result queryShopById(Long id) {
        String shopJson = (String) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSON.parseObject(shopJson, Shop.class);
            return Result.ok(shop);
        }

        Shop shop = getById(id);
        if (ObjectUtil.isEmpty(shop)) {
            return Result.ok();
        }
        redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSON.toJSONString(shop));

        return Result.ok(shop);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首页查询做缓存：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;List做缓存&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    @Override
    public Result queryTypeList() {
        List&amp;lt;Object&amp;gt; shopTypeListCache = redisTemplate.opsForList().range(&quot;cache:shopType&quot;, 0L, -1L);
        if (!shopTypeListCache.isEmpty()) {
            return Result.ok(shopTypeListCache);
        }
        List&amp;lt;ShopType&amp;gt; typeList = lambdaQuery()
                .orderByAsc(ShopType::getSort)
                .list();
        if (typeList.isEmpty()) {
            return Result.ok();
        }
        redisTemplate.opsForList().rightPushAll(&quot;cache:shopType&quot;, typeList.toArray());
        return Result.ok(typeList);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info
&lt;code&gt;typeList.toArray()&lt;/code&gt; 变成 &lt;code&gt;ShopType[]&lt;/code&gt; 数组，&lt;code&gt;rightPushAll(ShopType[] array)&lt;/code&gt; 会把数组中的每一个元素逐个写入 Redis;&lt;/p&gt;
&lt;p&gt;如果是&lt;code&gt;redisTemplate.opsForList().rightPushAll(&quot;cache:shopType&quot;, typeList);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rightPushAll(Collection&amp;lt;?&amp;gt; c)&lt;/code&gt; 会把整个集合当作一个元素推入 Redis 列表，得到的将会是(这样是不对的)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
 [ShopType1, ShopType2, ShopType3 ...]
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;缓存更新策略&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;内存淘汰&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;超时剔除&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;主动更新&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不用自己维护，利用Redis的内存淘汰机制，当内存不足时自动淘汰部分数据。下次查询时更新缓存。&lt;/td&gt;
&lt;td&gt;给缓存数据添加TTL时间，到期后自动删除缓存。下次查询时更新缓存。&lt;/td&gt;
&lt;td&gt;编写业务逻辑，在修改数据库的同时，更新缓存。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;一致性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;差&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;维护成本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;淘汰哪部分数据和淘汰的时机无法确定，如果旧数据一直为被淘汰，会造成数据的不一致&lt;/td&gt;
&lt;td&gt;一致性的强弱取决于所设置TTL的长短，同时如果在所设置的更新时间内发生数据更新，还是会造成数据的不一致&lt;/td&gt;
&lt;td&gt;更新可控性高，但需要编写额外业务逻辑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以根据业务场景选择更新策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;低一致性需求：使用内存淘汰机制。如店铺类型的查询缓存&lt;/li&gt;
&lt;li&gt;高一致性需求：主动更新，并以超时剔除为兜底方案。如店铺详情查询的缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;主动更新&lt;/strong&gt;策略主要有三种：Cache Aside模式、Read/Write Through模式、Write Behind Cahing模式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache Aside(缓存旁路模式)：由缓存调用者，在更新数据库的同时更新缓存。（代码复杂，但可人为控制）&lt;/li&gt;
&lt;li&gt;Read/Write Through(读写穿透模式)：缓存与数据库整合为一个服务，由服务来维护一致性。调用者调用该服务，无需关心缓存一致性问题。（维护服务复杂，无现成服务）&lt;/li&gt;
&lt;li&gt;Write Behind Caching(写回模式)：调用者只操作缓存，由其它线程异步的将缓存数据持久化到数据库，保证&lt;strong&gt;最终一致&lt;/strong&gt;。（维护异步任务复杂，在异步进程修改数据库前，难以保证一致性，若服务器宕机，内存中的Redis数据将丢失 ）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综合考虑，在企业中使用最多的策略是：Cache Aside。由调用者自己更新缓存。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;操作缓存和数据库时有三个问题需要考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;删除缓存还是更新缓存？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;更新缓存：每次更新数据库都要更新缓存，无效的写操作多。&lt;/li&gt;
&lt;li&gt;删除缓存：更新数据库时让缓存失效，查询时再更新缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;删除缓存，意思就是说，更新数据库以后直接将缓存中的旧数据直接删除了，等下一次查询在往缓存中存储，这种方式是更加合适的。&lt;/p&gt;
&lt;p&gt;如果是更新缓存，假如我们往数据库进行了100次更新，那么redis就需要进行100次更新，如果这100次期间并没有人来访问，就会造成写多读少的问题。而删除缓存，则是直接删除缓存，等什么时候有人来访问，再来写入缓存，这样就不会造成写多读少的问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;如何保证缓存与数据库的操作的同时成功或失败？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;单体系统：将缓存和数据库操作放在一个事务中。&lt;/li&gt;
&lt;li&gt;分布式系统：利用TCC等分布式事务方案（如：使用MQ通知其他服务进行数据同步）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;先操作缓存还是先操作数据库？（线程安全问题）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先删除缓存，再操作数据库。在多线程下可能会出现如下情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程1执行写操作，首先删除缓存，准备更新数据库（耗时较长）&lt;/li&gt;
&lt;li&gt;线程2查询数据，缓存未命中，读取数据库旧值并回填缓存。&lt;/li&gt;
&lt;li&gt;线程1完成数据库更新&lt;/li&gt;
&lt;li&gt;后序查询请求都会命中过期的缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从清空缓存到更新完数据库，整个过程耗时较长，其他线程很有可能在此期间读到数据库中的旧数据并写入缓存。缓存中存在旧数据，后续请求持续读到旧值，直到缓存过期或主动删除。&lt;/p&gt;
&lt;p&gt;正常情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251128215550827.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;异常情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251128215728981.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先操作数据库，再删除缓存&lt;/strong&gt;。此时可能出现线程安全的情况如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程1执行写操作，先更新数据库，此时尚未删除缓存&lt;/li&gt;
&lt;li&gt;线程2查询数据，命中缓存中的旧数据，返回。&lt;/li&gt;
&lt;li&gt;线程1删除缓存&lt;/li&gt;
&lt;li&gt;后续请求查询缓存未命中，从数据库读取新值并回填缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种方法会造成短暂的数据不一致，但缓存删除后数据恢复一致。后续请求缓存新值，无长期问题。&lt;/p&gt;
&lt;p&gt;正常情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251128220154940.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;异常情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251128220647333.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还有另一种可能的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于缓存过期或者首次查询，线程1查询缓存未命中，开始读取数据库v=10。&lt;/li&gt;
&lt;li&gt;在线程1读取数据库的过程中，线程2更新数据库为v=20，并删除缓存。&lt;/li&gt;
&lt;li&gt;线程2的删除缓存操作完成。&lt;/li&gt;
&lt;li&gt;线程1将读取到的旧数据v=10写入缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是写入Redis缓存的用时很短，不太可能在此期间完成更新数据库和删除缓存的可能，发生数据不一致的可能很小。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251128220610099.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;综合来看，方案二出现不一致性问题概率更低；&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;缓存更新策略的最佳实践方案：&lt;/p&gt;
&lt;p&gt;1、低一致性需求：使用Redis自带的内存淘汰机制&lt;/p&gt;
&lt;p&gt;2、高一致性需求：主动更新（Cache Aside），并以超时剔除作为兜底方案&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;读操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存命中则直接返回&lt;/li&gt;
&lt;li&gt;缓存未命中则查询数据库，并写入缓存，设定超时时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先写数据库，然后再删除缓存&lt;/li&gt;
&lt;li&gt;要确保数据库与缓存操作的原子性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实现商铺缓存&lt;/h3&gt;
&lt;p&gt;查询这里增加时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSON.toJSONString(shop), 30L, TimeUnit.MINUTES);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新商品逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result updateShop(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail(&quot;id不能为空&quot;);
        }
        // 更新数据库
        updateById(shop);
        // 删除缓存
        redisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
        return Result.ok();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;缓存穿透&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;缓存穿透&lt;/strong&gt;指客户端请求的数据在缓存中和数据库中都不存在，这样缓存永远不会生效，这些请求都会打到数据库。&lt;/p&gt;
&lt;p&gt;常见解决方案有两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;缓存空对象&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当我们发现请求的数据即不存在于缓存，也不存于与数据库时，将空值缓存到Redis，并设置过期时间，避免频繁查询数据库。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251201214207173.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现简单，维护⽅便&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;额外的内存消耗；可能发生不一致问题（在TTL内真的有对应数据存入数据库中）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假如用户刚好请求了一个id，但是这个id的数据不存在，我们给缓存了个null，就在此时我们真的给这个id插入了一条数据，但是缓存中缓存的是null，出现了数据不一致的问题。&lt;/p&gt;
&lt;p&gt;我们可以在新增数据的时候，我们主动把redis中的数据进行覆盖掉，也可以解决这个问题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;布隆过滤&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251201214639408.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：内存占用较少，没有多余key&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;实现复杂&lt;/li&gt;
&lt;li&gt;存在误判可能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决缓存穿透问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251201215707349.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    @Override
    public Result queryShopById(Long id) {
        String shopJson = (String) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSON.parseObject(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 命中的是否是空值
        if (shopJson != null) {
            return Result.fail(&quot;店铺不存在&quot;);
        }
        Shop shop = getById(id);
        if (ObjectUtil.isEmpty(shop)) {
            // 缓存空值到redis
            redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, &quot;&quot;, 2L, TimeUnit.MINUTES);
            return Result.fail(&quot;店铺不存在&quot;);
        }
        redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSON.toJSONString(shop), 30L, TimeUnit.MINUTES);

        return Result.ok(shop);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;缓存穿透产生的原因是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户请求的数据在缓存中和数据库中都不存在，不断发起这样的请求，给数据库带来巨大压力&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缓存穿透的解决方案有哪些？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存 null 值&lt;/li&gt;
&lt;li&gt;布隆过滤&lt;/li&gt;
&lt;li&gt;增强 id 的复杂度，避免被猜测 id 规律&lt;/li&gt;
&lt;li&gt;做好数据的基础格式校验&lt;/li&gt;
&lt;li&gt;加强用户权限校验&lt;/li&gt;
&lt;li&gt;做好热点参数的限流&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存雪崩&lt;/h3&gt;
&lt;p&gt;缓存雪崩是指在同⼀时段 &lt;strong&gt;⼤量的缓存key同时失效&lt;/strong&gt; 或者 &lt;strong&gt;Redis服务宕机&lt;/strong&gt;，导致⼤量请求到达数据库，带来巨⼤压⼒。&lt;/p&gt;
&lt;p&gt;与缓存击穿的区别：雪崩是很多key，击穿是某一个key缓存。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251201221150009.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;常见的解决方案有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于设置缓存时采用了相同的过期时间，导致缓存在某一时刻同时失效。因此&lt;strong&gt;给不同的Key在原本TTL的基础上添加随机值&lt;/strong&gt;，这样KEY的过期时间不同，不会大量KEY同时过期&lt;/li&gt;
&lt;li&gt;利用Redis集群提高服务的可用性，避免缓存服务宕机&lt;/li&gt;
&lt;li&gt;给缓存业务添加降级限流策略（服务降级、快速失败等）&lt;/li&gt;
&lt;li&gt;给业务添加多级缓存，比如先查询本地缓存，本地缓存未命中再查询Redis，Redis未命中再查询数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存击穿&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;缓存击穿问题&lt;/strong&gt;也叫热点Key问题，就是⼀个被 &lt;strong&gt;⾼并发访问&lt;/strong&gt; 并且 &lt;strong&gt;缓存重建业务较复杂&lt;/strong&gt; 的key突然失效了，⽆数的请求访问会在瞬间给数据库带来巨⼤的冲击。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251201222938789.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥锁：给重建缓存逻辑加锁，避免多线程同时进行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251201223154715.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当线程1发现缓存过期并尝试重建缓存时，首先获取互斥锁，再查询数据库并写入缓存，之后释放锁。在重建过程中，有其他线程也发现缓存过期并尝试重建时，会获取互斥锁失败，休眠一会再尝试查询缓存和获取锁的操作，直到查询到新的缓存数据时直接返回。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;逻辑过期：热点key不要设置过期时间，通过逻辑过期字段标识是否过期。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251201223611034.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当一个线程发现缓存已经过期时，获取互斥锁进行缓存重建，与前一种方案不同的是，缓存重建时会创建新的线程去完成，重建完成后释放互斥锁，自己直接返回过期数据。在重建缓存过程中，有新线程发现缓存过期并尝试重建时，会获取锁失败，此时直接返回过期数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对比&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;解决方案&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;互斥锁&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;• 没有额外的内存消耗&amp;lt;br&amp;gt;• 保证一致性&amp;lt;br&amp;gt;• 实现简单&lt;/td&gt;
&lt;td&gt;• 线程需要等待，性能受影响&amp;lt;br&amp;gt;• 可能有死锁风险&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;逻辑过期&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;• 线程无需等待，性能较好&lt;/td&gt;
&lt;td&gt;• 不保证一致性&amp;lt;br&amp;gt;• 有额外内存消耗&amp;lt;br&amp;gt;• 实现复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;互斥锁不需要保存逻辑过期时间，没有额外的内存消耗，而逻辑过期需要额外维护一个逻辑过期时间，有额外的内存消耗。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;互斥锁能够保证数据的强一致性，但由于锁的存在会降低并发性能；逻辑过期的方式优先保障高可用，性能好，但存在数据不一致情况。根据项目的实际需要选择合适的解决方案&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;利用互斥锁解决店铺详情查询的缓存击穿问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需求：修改根据id查询商铺的业务，基于互斥锁方式来解决缓存击穿问题&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251201233027840.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以利用setnx来实现互斥锁，setnx添加成功则返回1，添加失败则返回0；释放锁可以利用 &lt;code&gt;del xxx&lt;/code&gt;来释放锁，当然我们使用setnx要注意&lt;strong&gt;给key设置过期时间&lt;/strong&gt;，避免程序出现问题，导致锁永远无法释放，一般设置个10s足够了，业务重建缓存时间顶天1s不到；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现思路：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;进行查询之后，如果从缓存没有查询到数据，则进行互斥锁的获取（构建缓存）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若获取锁成功，则再次检测redis缓存是否存在，做DoubleCheck，如果存在则无需重建缓存，如果不存在则查询数据库重建缓存。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;对于第一次获取就得到互斥锁的线程而言，再次检测redis缓存，结果还是不存在，然后重建缓存。&lt;/li&gt;
&lt;li&gt;对于上次获得锁失败的线程而言，本次获取锁成功，说明已经有线程完成缓存重建，再次查询缓存即可获得数据，不用再执行重建缓存操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;若没有获取到互斥锁，则自旋等待一段时间后再次尝试获取锁，获取成功则回到 第1步；&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public Result queryShopById(Long id) {
        String shopJson = (String) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSON.parseObject(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 命中的是否是空值
        if (shopJson != null) {
            return Result.fail(&quot;店铺不存在&quot;);
        }
        // 实现缓存重建
        // 获取互斥锁
        String lockKey = &quot;lock:shop:&quot; + id;
        boolean isLocked = tryLock(lockKey);
        // 是否获取成功
        if (!isLocked) {
            // 失败-休眠重试
            ThreadUtil.sleep(50);
            return queryShopById(id);  // 递归重试
        }
        // Double-Check：第二次检查缓存
        shopJson = (String) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSON.parseObject(shopJson, Shop.class);
            return Result.ok(shop);
        }
        if (shopJson != null) {
            return Result.fail(&quot;店铺不存在&quot;);
        }
        // 缓存仍未命中，查数据库
        Shop shop = getById(id);
        if (ObjectUtil.isEmpty(shop)) {
            // 缓存空值到redis,防止穿透
            redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, &quot;&quot;, 2L, TimeUnit.MINUTES);
            return Result.fail(&quot;店铺不存在&quot;);
        }
        redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSON.toJSONString(shop), 30L, TimeUnit.MINUTES);
        // 释放互斥锁
        unLock(lockKey);
        return Result.ok(shop);
    }


    private boolean tryLock(String key) {
        // 只有当 key 不存在时，才会设置 value，并返回 true; 如果 key 已经存在，则不会进行任何修改，并返回 false。
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, &quot;1&quot;, 10L, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);    // isTrue:只有当flag是true才是true，flag为false和null都返回false(避免拆箱操作报空指针)
    }

    private void unLock(String key) {
        redisTemplate.delete(key);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;strong&gt;为什么需要 double check？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当多个线程同时进入查询方法时，可能会发生：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;A 线程发现缓存没有，去尝试加锁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B 线程也发现缓存没有，但 A 拿到锁，B 没拿到锁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B 休眠后继续重试，但此时 A 已经把最新数据写入缓存了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果没有 double check，B 又会继续走完整流程 —— 白查数据库，造成竞争。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以加锁之后必须再查一次缓存（第二次检查），避免重复构建缓存，减少数据库压力。&lt;/p&gt;
&lt;p&gt;这样可以保证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果别人已经重建缓存，我们不用再查数据库&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;避免多线程重复重建缓存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;性能更优，更安全
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;基于逻辑过期方式解决缓存击穿问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251202210741564.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于热点key会提前添加到Redis缓存，不设置过期时间，而是设置逻辑过期时间，再次进行查询缓存时，一定会命中的。若未命中，则说明不是缓存预热的数据，直接返回空值即可。&lt;/p&gt;
&lt;p&gt;整体思路：用户开始查询商铺，查询Redis判断是否命中，未命中直接返回空值即可。若命中，再通过逻辑过期时间判断数据是否过期，若未过期，直接返回Redis中的数据；若过期，则尝试获取互斥锁进行缓存重建。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若互斥锁获取成功，再次检查Redis中的数据是否过期，做DoubleCheck。若未过期（已经有人重建完成），返回本次查询到的商铺数据；若仍过期（还没有人重建缓存），开启独立线程查询数据库进行缓存重建，自己返回过期数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若获取互斥锁失败，返回过期数据即可。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在redis中存储的数据需要带上过期时间，故重建一个实体类，避免对原来代码的修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;做缓存预热&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
class HmDianPingApplicationTests {

    @Autowired
    private ShopServiceImpl shopService;

    @Test
    void saveToRedis() {
        shopService.saveShopToRedis(1L, 10L);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    public void saveShopToRedis(Long id, Long expireSeconds) {
        // 查询店铺数据
        Shop shop = getById(id);
        // 封装逻辑过期
        RedisData redisData = RedisData.builder()
                .data(shop)
                .expireTime(LocalDateTime.now().plusSeconds(expireSeconds))
                .build();
        // 写入redis
        redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


 @Override
    public Result queryShopById(Long id) {
        String shopJson = (String) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        if (StrUtil.isBlank(shopJson)) {
            return Result.fail(&quot;不存在&quot;);
        }
        // 命中需要判断过期时间
        RedisData redisData = JSON.parseObject(shopJson, RedisData.class);
        Shop shop = JSON.parseObject(JSON.toJSONString(redisData.getData()), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        if (expireTime.isAfter(LocalDateTime.now())) {
            // 未过期 返回店铺信息
            return Result.ok(shop);
        }
        // 过期-重建缓存
        // 获取互斥锁
        String lockKey = &quot;lock:shop:&quot; + id;
        boolean isLocked = tryLock(lockKey);
        // 是否获取成功
        if (isLocked) {
            // 再次检测redis缓存是否过期，做DoubleCheck
            String doubleCheckCacheStr = (String) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
            RedisData parsed = JSON.parseObject(doubleCheckCacheStr, RedisData.class);
            Shop shopObj = JSON.parseObject(JSON.toJSONString(parsed.getData()), Shop.class);
            LocalDateTime newExpireTime = parsed.getExpireTime();
            // 缓存未过期（已经有线程重建完成了），则返回数据
            if (newExpireTime.isAfter(LocalDateTime.now())) {
                return Result.ok(shopObj);
            }
            // 缓存仍过期 （还没有其他的线程重建缓存），创建独立线程，重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -&amp;gt; {
                saveShopToRedis(id, 20000L);
                // 释放锁
                unLock(lockKey);
            });
        }
        // 返回过期的商铺信息
        return Result.ok(shop);
    }

    private boolean tryLock(String key) {
        // 只有当 key 不存在时，才会设置 value，并返回 true; 如果 key 已经存在，则不会进行任何修改，并返回 false。
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, &quot;1&quot;, 10L, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);    // isTrue:只有当flag是true才是true，flag为false和null都返回false(避免拆箱操作报空指针)
    }

    private void unLock(String key) {
        redisTemplate.delete(key);
    }

    public void saveShopToRedis(Long id, Long expireSeconds) {
        // 查询店铺数据
        Shop shop = getById(id);
        // 封装逻辑过期
        RedisData redisData = RedisData.builder()
                .data(shop)
                .expireTime(LocalDateTime.now().plusSeconds(expireSeconds))
                .build();
        // 写入redis
        redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;缓存工具封装&lt;/h3&gt;
&lt;p&gt;基于StringRedisTemplate封装一个缓存工具类，满足下列需求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方法1：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法2：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法3：根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法4：根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法5：根据指定的key查询缓存，并反序列化为指定类型，需要利用互斥锁解决缓存击穿问题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;


/**
 * @ClassName CacheClient
 * @Description 缓存工具类
 * @Author zzy
 */
@Component
public class CacheClient {

    //存入Reids中的空值的过期时间
    public static final Long CACHE_NULL_TTL = 2L;
    //存入Reids中的空值的过期时间的时间类型
    public static final TimeUnit CACHE_NULL_TIME_UNIT = TimeUnit.MINUTES;
    //互斥锁对应的key
    public static final String LOCK_KEY = &quot;lock:&quot;;
    //获取互斥锁失败后的等待时间（单位毫秒）
    public static final Long SPIN_WAIT_MILLISECOND = 50L;

    //使用构造函数注入StringRedisTemplate
    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //创建拥有十个线程的线程池，用来重建缓存，避免经常创建销毁线程
    private final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间
     *
     * @param key      String类型的Key
     * @param value    任意类型的对象
     * @param time     过期时间
     * @param timeUnit 时间单位
     */
    public void set(String key, Object value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题
     *
     * @param key      String类型的Key
     * @param value    任意类型的对象
     * @param time     逻辑过期时间
     * @param timeUnit 时间单位
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit) {
        //设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
        //写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题
     *
     * @param keyPrefix  key的前缀
     * @param id         id
     * @param type       需要返回的对象的Class类型
     * @param dbFallback 根据id进行数据库查询的函数
     * @param time       过期时间
     * @param timeUnit   时间单位
     * @param &amp;lt;R&amp;gt;        需要返回的对象类型的泛型
     * @param &amp;lt;ID&amp;gt;       id的泛型
     * @return
     */
    public &amp;lt;R, ID&amp;gt; R queryWithPassThrough(String keyPrefix,
                                          ID id,
                                          Class&amp;lt;R&amp;gt; type,
                                          Function&amp;lt;ID, R&amp;gt; dbFallback,
                                          Long time,
                                          TimeUnit timeUnit) {
        //1、从redis中根据id查询商铺
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在记录
        if (StrUtil.isNotBlank(json)) {
            //存在，返回数据
            R r = JSONUtil.toBean(json, type);
            return r;
        }

        //3、判断记录是否为空值
        if (json != null) {
            return null;
        }

        //4、查询数据库
        R r = dbFallback.apply(id);
        //5、数据库是否存在记录
        if (r == null) {
            //6、不存在，将空值写入redis
            stringRedisTemplate.opsForValue().set(key, &quot;&quot;, CACHE_NULL_TTL, CACHE_NULL_TIME_UNIT);
            return null;
        }
        //7、存在，保存数据到redis，返回数据
        this.set(key, r, time, timeUnit);
        return r;
    }


    /**
     * 根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题
     *
     * @param keyPrefix  key的前缀
     * @param id         id
     * @param type       需要返回对象的Class类型
     * @param dbFallback 根据id查询数据库
     * @param time       逻辑过期时间
     * @param timeUnit   时间单位
     * @param &amp;lt;R&amp;gt;        需要返回的对象类型的泛型
     * @param &amp;lt;ID&amp;gt;       id的泛型
     * @return
     */
    public &amp;lt;R, ID&amp;gt; R queryWithLogicalExpire(String keyPrefix,
                                            ID id,
                                            Class&amp;lt;R&amp;gt; type,
                                            Function&amp;lt;ID, R&amp;gt; dbFallback,
                                            Long time,
                                            TimeUnit timeUnit) {
        //1、从redis中根据id查询商铺
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //2、缓存未命中，返回空数据
        if (StrUtil.isBlank(json)) {
            return null;
        }
        //3、缓存命中
        RedisData cacheData = JSONUtil.toBean(json, RedisData.class);
        LocalDateTime expireTime = cacheData.getExpireTime();
        R r = JSONUtil.toBean((JSONObject) cacheData.getData(), type);

        //3.1、判断缓存是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //3.2、缓存未过期，直接返回数据
            return r;
        }

        //4、缓存过期，需要进行缓存重建
        //4.1、尝试获取互斥锁
        String lockKey = LOCK_KEY + id;
        boolean isLock = tryLock(lockKey);
        //4.2、互斥锁获取成功
        if (isLock) {
            //4.3、再次检测redis缓存是否过期，做DoubleCheck
            String doubleCheckCacheStr = stringRedisTemplate.opsForValue().get(key);
            RedisData redisData = JSONUtil.toBean(doubleCheckCacheStr, RedisData.class);
            LocalDateTime newExpireTime = redisData.getExpireTime();
            R newR = JSONUtil.toBean((JSONObject) redisData.getData(), type);
            //4.3、缓存未过期（已经有线程重建完成了），则返回数据
            if (newExpireTime.isAfter(LocalDateTime.now())) {
                return newR;
            }
            //4.4 缓存仍过期 （还没有其他的线程重建缓存），创建独立线程，重建缓存
            //将重建工作交给线程池完成
            CACHE_REBUILD_EXECUTOR.submit(() -&amp;gt; {
                try {
                    //查询数据库
                    R dbR = dbFallback.apply(id);
                    //重建缓存
                    this.setWithLogicalExpire(key, dbR, time, timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //4.5释放锁
                    unlock(lockKey);
                }
            });
        }
        //5、返回过期的商铺信息
        return r;
    }

    /**
     * 根据指定的key查询缓存，并反序列化为指定类型，利用互斥锁解决缓存击穿问题
     *
     * @param keyPrefix  key的前缀
     * @param id         id
     * @param type       需要返回对象的Class类型
     * @param dbFallback 根据id查询数据库
     * @param time       过期时间
     * @param timeUnit   时间单位
     * @param &amp;lt;R&amp;gt;        需要返回的对象类型的泛型
     * @param &amp;lt;ID&amp;gt;       id的泛型
     * @return
     */
    public &amp;lt;R, ID&amp;gt; R queryWithMutex(String keyPrefix,
                                    ID id,
                                    Class&amp;lt;R&amp;gt; type,
                                    Function&amp;lt;ID, R&amp;gt; dbFallback,
                                    Long time,
                                    TimeUnit timeUnit) {
        //1、从redis中根据id查询商铺
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在记录
        if (StrUtil.isNotBlank(json)) {
            //存在，返回数据
            R r = JSONUtil.toBean(json, type);
            return r;
        }

        //3、判断记录是否为空值
        if (json != null) {
            return null;
        }

        //4、redis 查询结果为null缓存失效，尝试重建缓存
        String lockKey = LOCK_KEY + id;
        R dbR = null;
        try {
            //自旋等待，尝试获取互斥锁
            while (!tryLock(lockKey)) {
                Thread.sleep(SPIN_WAIT_MILLISECOND);
            }

            //4.2、获取锁成功,再次查询缓存
            String newJson = stringRedisTemplate.opsForValue().get(key);
            //缓存有效，直接返回
            if (StrUtil.isNotBlank(newJson)) {
                //存在，返回数据
                return JSONUtil.toBean(newJson, type);
            }

            //4.3、缓存无效，查询数据库重建缓存
            dbR = dbFallback.apply(id);
            //数据库是否存在记录
            if (dbR == null) {
                //不存在，将空值写入redis
                stringRedisTemplate.opsForValue().set(key, &quot;&quot;, CACHE_NULL_TTL, CACHE_NULL_TIME_UNIT);
                return null;
            }
            //存在，保存数据到redis，返回数据
            this.set(key, dbR, time, timeUnit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //5、释放锁
            unlock(lockKey);
        }
        //返回数据
        return dbR;
    }

    /**
     * 获取互斥锁，利用 setnx设置互斥锁，并设置锁的过期时间
     *
     * @param key
     * @return
     */
    public boolean tryLock(String key) {
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(key, &quot;1&quot;, 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(isLock);
    }

    /**
     * 释放互斥锁
     *
     * @param key
     */
    public void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用工具类优化解决之前店铺详情查询的缓存击穿和缓存穿透问题的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Result queryShopById(Long id) {
        // 通过缓存空值，缓存穿透
//        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,
//                id, Shop.class,  this::getById, CACHE_SHOP_TTL,TimeUnit.MINUTES);

        // 互斥锁解决缓存击穿
//        Shop shop = cacheClient.queryWithMutex(CACHE_SHOP_KEY,id,Shop.class,
//                this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);

        // 逻辑过期解决缓存击穿
        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY,id,Shop.class,
                this::getById,10L,TimeUnit.SECONDS);

        if (shop == null) {
            return Result.fail(&quot;店铺不存在&quot;);
        }
        return Result.ok(shop);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;优惠券秒杀&lt;/h2&gt;
&lt;h3&gt;全局唯—ID&lt;/h3&gt;
&lt;p&gt;对于使用MySQL数据库的ID存在的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id的规律性太明显，可能泄露数据&lt;/li&gt;
&lt;li&gt;受单表数据量的限制，数量过大后需要进行分库分表，会出现ID重复的情况，无法保证同类数据ID的唯一性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;全局ID生成器：是一种在分布式系统下用来生成全局唯一ID的工具，一般要满足一下特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;唯一性&lt;/li&gt;
&lt;li&gt;高性能&lt;/li&gt;
&lt;li&gt;高可用&lt;/li&gt;
&lt;li&gt;安全性&lt;/li&gt;
&lt;li&gt;递增性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了增加ID的安全性，我们可以不直接使用Redis自增的数值，而是拼接一些其它信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251202232918860.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;符号位：1bit，为0&lt;/li&gt;
&lt;li&gt;时间戳：31bit，以秒为单位，可以使用69年&lt;/li&gt;
&lt;li&gt;序列号：32bit，秒内的计数器，支持每秒产生2^32个不同ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工具类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class RedisIdWorker {


    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    private static final long BEGIN_TIMESTAMP = 1640995200L;

    /**
     * 序列号位数
     */
    private static final int COUNT_BITS = 32;

    /**
     * 生成全局唯一ID
     *
     * @param keyPerfix 相关业务的ID前缀
     * @return
     */
    public long nextId(String keyPerfix) {
        // 生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;
        // 生成序列号
        String date = now.format(DateTimeFormatter.ofPattern(&quot;yyyy:MM:dd&quot;));
        Long count = redisTemplate.opsForValue().increment(&quot;icr:&quot; + keyPerfix + &quot;:&quot; + date); // redis自增长的值是有上限的，是2^64

        return timestamp &amp;lt;&amp;lt; COUNT_BITS | count;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;p&gt;创建含有300个线程的线程池，每个线程使用Reids的id生成器生成100个id，统计用时。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Resource
    private RedisIdWorker idWorker;

    private ExecutorService es = Executors.newFixedThreadPool(300);

    @Test
    void testIdWorker() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(300);

        Runnable task = () -&amp;gt; {
            for (int i = 0; i &amp;lt; 100; i++) {
                long id = idWorker.nextId(&quot;order&quot;);
                System.out.println(&quot;id = &quot; + id);
            }
            latch.countDown();
        };

        long begin = System.currentTimeMillis();
        for (int i = 0; i &amp;lt; 300; i++) {
            es.submit(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println(&quot;time = &quot; + (end - begin));
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251203222352286.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;耗时1秒多，与机器性能有关&lt;/p&gt;
&lt;h3&gt;实现优惠券秒杀下单&lt;/h3&gt;
&lt;p&gt;秒杀券下单时需要判断两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;秒杀是否开始或结束，如果尚未开始或已经结束则无法下单&lt;/li&gt;
&lt;li&gt;库存是否充足，不足则无法下单&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251203230558459.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下单功能&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class VoucherOrderServiceImpl extends ServiceImpl&amp;lt;VoucherOrderMapper, VoucherOrder&amp;gt; implements IVoucherOrderService {

    private final ISeckillVoucherService seckillVoucherService;

    private final RedisIdWorker redisIdWorker;

    @Override
    @Transactional(timeout = 5000, rollbackFor = Exception.class)
    public Result seckillVoucher(Long voucherId) {
        // 查优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = voucher.getBeginTime();
        if (beginTime.isAfter(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀尚未开始&quot;);
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀已经结束&quot;);
        }
        if (voucher.getStock() &amp;lt; 1) {
            return Result.fail(&quot;库存不足&quot;);
        }
        boolean flag = seckillVoucherService.update().setSql(&quot;stock = stock - 1&quot;)
                .eq(&quot;voucher_id&quot;, voucherId)
                .update();
        if (!flag) {
            return Result.fail(&quot;扣减失败&quot;);
        }
        // 创建订单
        long orderId = redisIdWorker.nextId(&quot;order&quot;);
        VoucherOrder voucherOrder = VoucherOrder.builder()
                .id(orderId)
                .userId(UserHolder.getUser().getId())
                .voucherId(voucherId)
                .build();
        save(voucherOrder);
        // 返回订单id
        return Result.ok(orderId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;超卖问题&lt;/h3&gt;
&lt;p&gt;理想情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251203233615194.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;错误分析：&lt;/p&gt;
&lt;p&gt;​ 假设线程1过来查询库存，判断出来库存大于1，正准备去扣减库存，但是还没有来得及去扣减，此时线程2过来，线程2也去查询库存，发现这个数量一定也大于1，那么这两个线程都会去扣减库存，最终多个线程相当于一起去扣减库存，此时就会出现库存的超卖问题。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251203233809995.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;超卖问题是典型的多线程安全问题，针对这一问题的常见解决方案就是加锁：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251203234120503.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;乐观锁的关键是判断之前查询得到的数据是否有被修改过，常见的方式有两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;版本号法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;给数据添加一个版本version字段，当数据修改时version加一，基于version字段判断有没有被修改过。每一次更新都必须满足&lt;strong&gt;更新前的 version == 当前数据库中的 version&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251203234513563.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CAS法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用&lt;strong&gt;数据本身是否发生变化&lt;/strong&gt;判断线程是否安全（比如库存数量）
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251203235054427.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;乐观锁解决超卖&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private final ISeckillVoucherService seckillVoucherService;

    private final RedisIdWorker redisIdWorker;

    @Override
    @Transactional(timeout = 5000, rollbackFor = Exception.class)
    public Result seckillVoucher(Long voucherId) {
        // 查优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = voucher.getBeginTime();
        if (beginTime.isAfter(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀尚未开始&quot;);
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀已经结束&quot;);
        }
        if (voucher.getStock() &amp;lt; 1) {
            return Result.fail(&quot;库存不足&quot;);
        }
        // 扣减库存
        boolean flag = seckillVoucherService.update().setSql(&quot;stock = stock - 1&quot;)   // set stock = stock - 1
                .eq(&quot;voucher_id&quot;, voucherId)
                .gt(&quot;stock&quot;, 0)  // where id = ? and stock &amp;gt; 0
                .update();
        if (!flag) {
            return Result.fail(&quot;扣减失败&quot;);
        }
        // 创建订单
        long orderId = redisIdWorker.nextId(&quot;order&quot;);
        VoucherOrder voucherOrder = VoucherOrder.builder()
                .id(orderId)
                .userId(UserHolder.getUser().getId())
                .voucherId(voucherId)
                .build();
        save(voucherOrder);
        // 返回订单id
        return Result.ok(orderId);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;超卖这样的线程安全问题，解决方案有哪些？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;悲观锁：添加同步锁，让线程串行执行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：简单粗暴&lt;/li&gt;
&lt;li&gt;缺点：性能一般&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;乐观锁：不加锁，在更新时判断是否有其它线程在修改&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：性能好&lt;/li&gt;
&lt;li&gt;缺点：存在成功率低的问题 (&lt;code&gt;where id = ? and stock &amp;gt; 0&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;一人一单&lt;/h3&gt;
&lt;p&gt;需求：修改秒杀业务，要求同一个优惠券，一个用户只能下一单&lt;/p&gt;
&lt;p&gt;具体逻辑如下：首先查询优惠券，判断当前时间是否处于秒杀阶段，再进一步判断库存是否足够，然后再根据优惠卷id和用户id查询是否已经下过这个订单，如果下过这个订单，则不再下单，否则进行下单。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251208220800540.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在JMeter多线程环境下测试，同样会出现线程安全的问题，多个线程第一次下单时，同时查询到当前不存在订单，然后各自去下单，还是会出现一个用户下了多个订单的情况。&lt;/p&gt;
&lt;p&gt;这里没法使用乐观锁，因为数据根本就不存在，没法判断是否被修改过，我们要判断的是&lt;em&gt;是否存在&lt;/em&gt;，只能用悲观锁方案；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;加锁的粒度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先把从验证一人一单 到 添加优惠券订单的逻辑抽取到一个方法createVoucherOrder中，在这个方法中进行查询订单、扣减库存并完成订单添加。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public Result seckillVoucher(Long voucherId) {
        // 查优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = voucher.getBeginTime();
        if (beginTime.isAfter(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀尚未开始&quot;);
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀已经结束&quot;);
        }
        if (voucher.getStock() &amp;lt; 1) {
            return Result.fail(&quot;库存不足&quot;);
        }
        Long userId = UserHolder.getUser().getId();
        return createVoucherOrder(voucherId, userId);
    }

    @Transactional(timeout = 5000, rollbackFor = Exception.class)
    public synchronized Result createVoucherOrder(Long voucherId, Long userId) {    // 锁的对象是this
        // 一人一单
        Integer count = lambdaQuery()
                .eq(VoucherOrder::getUserId, userId)
                .eq(VoucherOrder::getVoucherId, voucherId)
                .count();
        if (count &amp;gt; 0) {
            // 用户至少下过一单
            return Result.fail(&quot;已经购买过了&quot;);
        }
        // 扣减库存
        boolean flag = seckillVoucherService.update().setSql(&quot;stock = stock - 1&quot;)   // set stock = stock - 1
                .eq(&quot;voucher_id&quot;, voucherId)
                .gt(&quot;stock&quot;, 0)  // where id = ? and stock &amp;gt; 0
                .update();
        if (!flag) {
            return Result.fail(&quot;库存不足&quot;);
        }
        // 创建订单
        long orderId = redisIdWorker.nextId(&quot;order&quot;);
        VoucherOrder voucherOrder = VoucherOrder.builder()
                .id(orderId)
                .userId(userId)
                .voucherId(voucherId)
                .build();
        save(voucherOrder);
        // 返回订单id
        return Result.ok(orderId);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在这个方法上加锁，在同一时刻只有一个线程可以执行该方法，每个线程对这个方法的访问变成了串行方式，性能降低。
加锁的初衷是为了解决&lt;strong&gt;同一个用户&lt;/strong&gt;的线程安全问题，而不同用户应该互不受影响。因此需要降低锁的粒度，同一个用户加一把锁，不同用户加不同的锁，故可以对用户的id加锁。
（同一个用户才需要去判断并发安全问题，如果不是同一个用户不需要加锁）&lt;/p&gt;
&lt;p&gt;:::info
为什么要用 &lt;code&gt;synchronized (userId.toString().intern())&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;目的：让同一个 userId 的操作串行执行，不同 userId 并行执行 (单机环境下，以 userId 为粒度的本地同步锁，使同一个 userId 的操作串行化)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一个用户请求，只允许一个线程进入同步块&lt;/li&gt;
&lt;li&gt;不同用户之间互不影响，可以同时执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public Result seckillVoucher(Long voucherId) {
        // 查优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = voucher.getBeginTime();
        if (beginTime.isAfter(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀尚未开始&quot;);
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀已经结束&quot;);
        }
        if (voucher.getStock() &amp;lt; 1) {
            return Result.fail(&quot;库存不足&quot;);
        }
        return createVoucherOrder(voucherId);
    }

    @Transactional(timeout = 5000, rollbackFor = Exception.class)
    public Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            // 一人一单
            Integer count = lambdaQuery()
                    .eq(VoucherOrder::getUserId, userId)
                    .eq(VoucherOrder::getVoucherId, voucherId)
                    .count();
            if (count &amp;gt; 0) {
                // 用户至少下过一单
                return Result.fail(&quot;已经购买过了&quot;);
            }
            // 扣减库存
            boolean flag = seckillVoucherService.update().setSql(&quot;stock = stock - 1&quot;)   // set stock = stock - 1
                    .eq(&quot;voucher_id&quot;, voucherId)
                    .gt(&quot;stock&quot;, 0)  // where id = ? and stock &amp;gt; 0
                    .update();
            if (!flag) {
                return Result.fail(&quot;库存不足&quot;);
            }
            // 创建订单
            long orderId = redisIdWorker.nextId(&quot;order&quot;);
            VoucherOrder voucherOrder = VoucherOrder.builder()
                    .id(orderId)
                    .userId(userId)
                    .voucherId(voucherId)
                    .build();
            save(voucherOrder);
            // 返回订单id
            return Result.ok(orderId);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如果不加 intern()，会出什么问题？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;synchronized (userId.toString())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然两次生成的字符串内容相等，但不是同一个对象，因为是 new 出来的不同实例，这样锁根本不生效&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;intern()&lt;/code&gt; 返回的是字符串常量池的同一个对象，JVM 会检查 字符串常量池（String Pool）里是否已经存在一个与 &quot;xxx&quot; 内容相同的字符串，如果存在：返回池中该字符串的引用，如果不存在：将 “xxx” 的字符串内容放入池中，并返回池中的引用&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;现在的逻辑是，先释放锁，再提交事务，这个事务是被Spring管理的，锁释放后意味着其他线程可以进来了，如果此时事物尚未提交，如果有线程进来查询订单了，而上一个订单还未写入数据库，此时查询订单就会发现不存在，然后再去创建订单，这就导致了订单重复的问题，依然存在并发安全问题；&lt;/p&gt;
&lt;p&gt;所以我们应该是事物提交后再去释放锁，所以我们应该把锁放在这里;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class VoucherOrderServiceImpl extends ServiceImpl&amp;lt;VoucherOrderMapper, VoucherOrder&amp;gt; implements IVoucherOrderService {

    private final ISeckillVoucherService seckillVoucherService;

    private final RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 查优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = voucher.getBeginTime();
        if (beginTime.isAfter(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀尚未开始&quot;);
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀已经结束&quot;);
        }
        if (voucher.getStock() &amp;lt; 1) {
            return Result.fail(&quot;库存不足&quot;);
        }
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            // 获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }

    @Transactional(timeout = 5000, rollbackFor = Exception.class)
    public Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        // 一人一单
        Integer count = lambdaQuery()
                .eq(VoucherOrder::getUserId, userId)
                .eq(VoucherOrder::getVoucherId, voucherId)
                .count();
        if (count &amp;gt; 0) {
            // 用户至少下过一单
            return Result.fail(&quot;已经购买过了&quot;);
        }
        // 扣减库存
        boolean flag = seckillVoucherService.update().setSql(&quot;stock = stock - 1&quot;)   // set stock = stock - 1
                .eq(&quot;voucher_id&quot;, voucherId)
                .gt(&quot;stock&quot;, 0)  // where id = ? and stock &amp;gt; 0
                .update();
        if (!flag) {
            return Result.fail(&quot;库存不足&quot;);
        }
        // 创建订单
        long orderId = redisIdWorker.nextId(&quot;order&quot;);
        VoucherOrder voucherOrder = VoucherOrder.builder()
                .id(orderId)
                .userId(userId)
                .voucherId(voucherId)
                .build();
        save(voucherOrder);
        // 返回订单id
        return Result.ok(orderId);

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何让事务生效？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;添加依赖&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.aspectj&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;aspectjweaver&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;启动类&lt;/strong&gt;打上注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableAspectJAutoProxy(exposeProxy = true)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;接口加上&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Result createVoucherOrder(Long voucherId);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;        synchronized (userId.toString().intern()) {
            // 获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;一人一单的并发安全问题 (分布式失效)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过加锁可以解决在单机情况下的一人一单安全问题，但是在集群模式下就不行了。&lt;/p&gt;
&lt;p&gt;1．我们将服务启动两份，端口分别为8081和8082;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251208232217486.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;然后修改nginx的conf目录下的nginx.conf文件，配置反向代理和负载均衡;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/json;

    sendfile        on;
    
    keepalive_timeout  65;

    server {
        listen       8080;
        server_name  localhost;
        # 指定前端项目所在的位置
        location / {
            root   html/hmdp;
            index  index.html index.htm;
        }

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


        location /api {  
            default_type  application/json;
            #internal;  
            keepalive_timeout   30s;  
            keepalive_requests  1000;  
            #支持keep-alive  
            proxy_http_version 1.1;  
            rewrite /api(/.*) $1 break;  
            proxy_pass_request_headers on;
            #more_clear_input_headers Accept-Encoding;  
            proxy_next_upstream error timeout;  
            #proxy_pass http://127.0.0.1:8081;
            proxy_pass http://backend;
        }
    }

    upstream backend {
        server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;
        server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;
    }  
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# win
nginx.exe -s reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，用户请求会在这两个节点上负载均衡，再次测试下是否存在线程安全问题。&lt;/p&gt;
&lt;p&gt;启动两端口的服务，使用同一个用户下两次单，在锁内部打上断点，debug结果如下：同一个用户在不同端口的服务上都成功获取到了锁，都可以进行下单操作。&lt;/p&gt;
&lt;p&gt;正常理想情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251208233531987.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251208233659701.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;集群问题：&lt;/p&gt;
&lt;p&gt;在集群模式下或分布式系统，有多个JVM的存在，每个JVM内部都有自己的锁，导致每一个锁都有一个线程获取，就出现了并行运行；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251208234023072.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;分布式锁&lt;/h3&gt;
&lt;h4&gt;分布式锁的实现方案&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251209205026567.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;什么是分布式锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分布式锁：满足分布式系统或集群模式下多进程可见并且互斥的锁。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可见性：多个jvm都能看到相同的结果。&lt;/li&gt;
&lt;li&gt;互斥：不管谁来访问，只能有一个人能拿到。&lt;/li&gt;
&lt;li&gt;高可用：大多数情况来获取锁都是成功的，不能说获取锁的时候经常出现问题。&lt;/li&gt;
&lt;li&gt;高性能(高并发)：由于加锁本身就让性能降低，如果获取锁的动作又很慢，那么就太慢了，所以获取锁的动作必须高性能。&lt;/li&gt;
&lt;li&gt;安全性：考虑异常的情况，比如获取锁成功了，但是还没有释放锁，程序异常了怎么办（可能死锁）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;分布式锁的实现方案&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分布式锁的核心是实现多进程之间互斥，而满足这一点的方式有很多，常见的有三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mysql: 利用事务机制获取锁，如果有异常，事务就会回滚，则释放了锁；&lt;/li&gt;
&lt;li&gt;redis: 利用redis的setnx命令获取锁，如果setnx成功，则获取锁，否则获取锁失败；释放锁即使把这个key删掉，其他人就能继续获取锁了，为了在服务出现故障后仍能自动释放锁，需要在添加key的时候设置过期时间(需要考虑看门狗)；可用性也是很好既支持主从也支持集群；&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;th&gt;Zookeeper&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;互斥&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;利用mysql本身的互斥锁机制&lt;/td&gt;
&lt;td&gt;利用setnx这样的互斥命令&lt;/td&gt;
&lt;td&gt;利用节点的唯一性和有序性实现互斥&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;高可用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;高性能&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;安全性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;断开连接，自动释放锁&lt;/td&gt;
&lt;td&gt;利用锁超时时间，到期释放&lt;/td&gt;
&lt;td&gt;临时节点，断开连接自动释放&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;利用Redis实现分布式锁&lt;/h4&gt;
&lt;p&gt;实现分布式锁时需要实现的两个基本方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;获取锁&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥：确保只能有一个线程获取锁&lt;/li&gt;
&lt;li&gt;非阻塞：尝试一次，成功返回true，失败返回false&lt;pre&gt;&lt;code&gt;# 添加锁，利用setnx的互斥特性
SETNX lock thread1

# 添加锁过期时间，避免服务岩机引起的死锁（过期时间要比业务时间长，否则业务没执行完锁就自动释放了）
EXPIRE lock 10

# 并且我们需要保证setnx和expire是原子操作，要么都成功要么都失败；
# 不能分两步执行（先 SETNX 再 EXPIRE），因为在这两个命令之间如果发生进程崩溃、网络中断等故障，会导致锁没有设置过期时间，从而引发死锁。
# 命令格式：
SET key value NX EX seconds
# 比如：
SET lock:shop101 thread-001 NX EX 30

# NX → 仅当 key 不存在时才设置（相当于 SETNX）
# EX seconds → 设置 key 的过期时间（秒）
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;释放锁&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;手动释放&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;超时释放：获取锁时添加一个超时时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 释放锁，删除即可
DEL key
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;流程：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251209215146930.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;基于Redis实现分布式锁初级版本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需求：定义一个类，实现下面接口，利用Redis实现分布式锁功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec 锁持有的超时时间，过期后自动释放
     * @return true代表获取锁成功；false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SimpleRedisLock implements ILock {

    private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;
    private String name;

    private static final String KEY_PREFIX = &quot;lock:&quot;;

    public SimpleRedisLock(RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate, String name) {
        this.redisTemplate = redisTemplate;
        this.name = name;
    }


    @Override
    public boolean tryLock(long timeoutSec) {
        long threadId = Thread.currentThread().getId();
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + &quot;&quot;, timeoutSec, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    @Override
    public void unlock() {
        redisTemplate.delete(KEY_PREFIX + name);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;修改业务逻辑&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public Result seckillVoucher(Long voucherId) {
        // 查优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = voucher.getBeginTime();
        if (beginTime.isAfter(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀尚未开始&quot;);
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀已经结束&quot;);
        }
        if (voucher.getStock() &amp;lt; 1) {
            return Result.fail(&quot;库存不足&quot;);
        }
        Long userId = UserHolder.getUser().getId();
        // 创建锁对象
        SimpleRedisLock simpleRedisLock = new SimpleRedisLock(redisTemplate, &quot;order:&quot; + userId);
        boolean flag = simpleRedisLock.tryLock(1000);
        if (!flag) {
            // 获取锁失败
            return Result.fail(&quot;不允许重复下单&quot;);
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 释放锁
            simpleRedisLock.unlock();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Redis分布式锁误删问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;问题说明：&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;持有锁的线程1在锁的内部出现了阻塞，导致他的锁TTL到期，自动释放&lt;/li&gt;
&lt;li&gt;此时线程2也来尝试获取锁，由于线程1已经释放了锁，所以线程2可以拿到&lt;/li&gt;
&lt;li&gt;但是现在线程1阻塞完了，继续往下执行，要开始释放锁了&lt;/li&gt;
&lt;li&gt;那么此时就会将属于线程2的锁释放，这就是误删别人锁的情况
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251209232048463.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有一些问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务阻塞，导致锁提前释放&lt;/li&gt;
&lt;li&gt;释放锁时，把别人的锁释放了（锁误删）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;释放锁时应该做一个判断，解决方案就是在每个线程释放锁的时候，都判断一下这个锁是不是自己的，如果不属于自己，则不进行删除操作。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251209232431633.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以正确流程应该是：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251209232533374.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决Redis分布式锁误删问题&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在获取锁时存入线程标示（可以用UUID表示）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在释放锁时先获取锁中的线程标示，判断是否与当前线程标示一致&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果一致则释放锁&lt;/li&gt;
&lt;li&gt;如果不一致则不释放锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;完善后：&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;key：KEY_PREFIX + name，key的前缀KEY_PREFIX = &quot;lock:&quot;，再根据当前的业务名称传入name，二者拼接起来作为key，让不同的业务获取不同的锁。&lt;/p&gt;
&lt;p&gt;value：应设置成当前线程的唯一标识，防止因线程阻塞导致锁自动释放后再去执行释放锁操作，将别的线程锁设置的锁误删。此处的唯一标识设置为&lt;strong&gt;UUID + 线程id&lt;/strong&gt;的形式，当要主动释放锁时，先获取对应的value值，判断与自己的唯一标识是否相同，相同则删除该key释放锁，否则不用处理。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当前线程的id不能作为线程的唯一标识。每个JVM实例都会为在其内部创建的每个线程分配一个唯一的标识符（通常是一个递增的长整型数字）。当有多个JVM实例运行时，每个JVM实例的线程标识符空间是独立的，所以不同JVM实例中的线程可能会有相同的标识符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class SimpleRedisLock implements ILock {

    private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;
    private String name;

    private static final String KEY_PREFIX = &quot;lock:&quot;;
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + &quot;-&quot;;

    public SimpleRedisLock(RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate, String name) {
        this.redisTemplate = redisTemplate;
        this.name = name;
    }


    @Override
    public boolean tryLock(long timeoutSec) {
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    @Override
    public void unlock() {
        // 线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        String redisThreadId = (String) redisTemplate.opsForValue().get(&quot;KEY_PREFIX + name&quot;);
        // 标识是否一致
        if (threadId.equals(redisThreadId)) {
            redisTemplate.delete(KEY_PREFIX + name);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;分布式锁的原子性问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于&lt;em&gt;判断标识&lt;/em&gt;是否一致和&lt;em&gt;释放锁&lt;/em&gt;是两个操作，可能会在判断标识一致后发生线程阻塞并且阻塞时间过长导致锁超时释放，其他线程就会获取锁成功。而当阻塞完成时的线程会直接去释放锁（之前已经判断过是一致的），此时就会释放其他线程的锁(又发生了误删)，从而可能引发线程安全问题。因此需要一种机制保证这两个Redis操作的原子性 --- &lt;em&gt;Redis的Lua脚本&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JVM的垃圾回收机制在FULL GC时会暂时阻塞所有线程‌&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;阻塞导致误删情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251209234746171.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;逻辑解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设线程1已经获取了锁，在判断标识一致之后，准备释放锁的时候，又出现了阻塞（例如JVM垃圾回收机制）&lt;/li&gt;
&lt;li&gt;于是锁的TTL到期了，自动释放了&lt;/li&gt;
&lt;li&gt;那么现在线程2趁虚而入，拿到了一把锁&lt;/li&gt;
&lt;li&gt;但是线程1的逻辑还没执行完，那么线程1就会执行删除锁的逻辑&lt;/li&gt;
&lt;li&gt;但是在阻塞前线程1已经判断了标识一致，所以现在线程1把线程2的锁给删了&lt;/li&gt;
&lt;li&gt;那么就相当于判断标识那行代码没有起到作用&lt;/li&gt;
&lt;li&gt;这就是删锁时的原子性问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Redis调用Lua脚本&lt;/h4&gt;
&lt;p&gt;Redis提供了Lua脚本功能，在一个脚本中编写多条Redis命令，确保多条命令执行时的原子性。Lua是一种编
程语言，它的基本语法大家可以参考网站：https://www.runoob.com/lua/lua-tutorial.html&lt;/p&gt;
&lt;p&gt;Redis提供的调用函数，语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 执行 Redis 命令

# 执行redis命令
redis.call(&apos;命令名称&apos;, &apos;key&apos;, &apos;其它参数&apos;, ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，我们要执行 set name jack，则脚本是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 执行 set name jack
redis.call(&apos;set&apos;, &apos;name&apos;, &apos;jack&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，我们要先执行 set name Rose，再执行 get name，则脚本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 先执行 set name jack
redis.call(&apos;set&apos;, &apos;name&apos;, &apos;jack&apos;)
# 再执行 get name
local name = redis.call(&apos;get&apos;, &apos;name&apos;)
# 返回
return name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写好脚本以后，需要用Redis命令来调用脚本，调用脚本的常见命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; help @scripting

  EVAL script numkeys key [key ...] arg [arg ...]
  summary: Execute a Lua script server side
  since: 2.6.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，我们要执行redis.call(&apos;set&apos;，&apos;name&apos;，jack&apos;）这个脚本，语法如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251210212522030.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; EVAL &quot;return redis.call(&apos;set&apos;,&apos;name&apos;,&apos;Jack&apos;)&quot; 0
OK
127.0.0.1:6379&amp;gt; get name
&quot;Jack&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果脚本中的key、Value不想写死，可以作为参数传递。key类型参数会放入KEYS数组，其它参数会放入ARGV数组，在
脚本中可以从KEYS和ARGV数组获取这些参数：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251210213306702.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; EVAL &quot;return redis.call(&apos;set&apos;,KEYS[1],ARGV[1])&quot; 1 name Lucy
OK
127.0.0.1:6379&amp;gt; get name
&quot;Lucy&quot;
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
lua语言中数组角标从1开始
:::&lt;/p&gt;
&lt;p&gt;释放锁的业务流程是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取锁中的线程标示&lt;/li&gt;
&lt;li&gt;判断是否与指定的标示（当前线程标示）一致&lt;/li&gt;
&lt;li&gt;如果一致则释放锁（删除）&lt;/li&gt;
&lt;li&gt;如果不一致则什么都不做&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果用Lua脚本来表示则是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 获取锁中的线程标示
local id = redis.call(&apos;GET&apos;, KEYS[1])
-- 比较线程标示与锁中的标示是否一致
if(id == ARGV[1]) then
    return redis.call(&apos;DEL&apos;, KEYS[1])
end
return 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Java调用Lua脚本&lt;/h4&gt;
&lt;p&gt;RedisTemplate调用Lua脚本的API如下：&lt;/p&gt;
&lt;p&gt;其中，将KEY类型参数放在了一个List中，通过该List可以知道传入的KEY类型参数的个数，就不用原指令中的numkeys参数了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251210214932342.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先在classpath路径下建一个lua脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 获取锁中的线程标示
local id = redis.call(&apos;GET&apos;, KEYS[1])
-- 比较线程标示与锁中的标示是否一致
if(id == ARGV[1]) then
    return redis.call(&apos;DEL&apos;, KEYS[1])
end
return 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用redisTemplate或stringRedisTemplate调用lua脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;


public class SimpleRedisLock implements ILock {

    private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;
    private String name;

    private static final String KEY_PREFIX = &quot;lock:&quot;;
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + &quot;-&quot;;

    private static final DefaultRedisScript&amp;lt;Long&amp;gt; UNLOCK_SCRIPT;

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript&amp;lt;&amp;gt;();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource(&quot;unlock.lua&quot;)); // 指定classpath文件路径
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public SimpleRedisLock(RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate, String name) {
        this.redisTemplate = redisTemplate;
        this.name = name;
    }


    @Override
    public boolean tryLock(long timeoutSec) {
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    @Override
    public void unlock() {
        // 调用lua脚本
        redisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

//    @Override
//    public void unlock() {
//        // 线程标识
//        String threadId = ID_PREFIX + Thread.currentThread().getId();
//        String redisThreadId = (String) redisTemplate.opsForValue().get(&quot;KEY_PREFIX + name&quot;);
//        // 标识是否一致
//        if (threadId.equals(redisThreadId)) {
//            redisTemplate.delete(KEY_PREFIX + name);
//        }
//    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public interface ILock {

    /**
     *尝试获取锁
     * @param timeout 锁持有的超时时间，过期后自动释放
     * @return true代表获取锁成功；false代表获取锁失败
     */
    boolean tryLock(long timeout);

    /**
     * 释放锁
     */
    void unlock();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决了阻塞情况下锁误删情况；&lt;/p&gt;
&lt;p&gt;基于Redis的分布式锁实现思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用 &lt;code&gt;set nx ex&lt;/code&gt; 获取锁，并设置过期时间，保存线程标示&lt;/li&gt;
&lt;li&gt;释放锁时先判断线程标示是否与自己一致，一致则删除锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用 &lt;code&gt;set nx&lt;/code&gt; 满足互斥性&lt;/li&gt;
&lt;li&gt;利用 &lt;code&gt;set ex&lt;/code&gt; 保证故障时锁依然能释放，避免死锁，提高安全性&lt;/li&gt;
&lt;li&gt;利用 Redis 集群保证高可用和高并发特性&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Redisson&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;基于setnx实现的分布式锁存在下面的问题：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不可重入
同一个线程无法多次获取同一把锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可重入锁是指&lt;strong&gt;同一个线程可以多次获取同一把锁&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如，我有一个方法a，需要先获取锁，然后执行业务并调用方法b，而方法b里也要获取同一把锁。如果锁是不可重入的，那么在方法a中获取锁后，调用方法b时再次尝试获取这把锁就会失败，此时会等待锁的释放。但由于方法a还未执行完，仍在调用b，所以会出现死锁。因此，在这种场景下，要求锁必须是可重入的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不可重试
获取锁只尝试一次就返回
false，没有重试机制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们之前所实现的锁是非阻塞的，如果失败，则立即返回&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超时释放
锁超时释放虽然可以避免死
锁，但如果是业务执行耗时
较长，也会导致锁释放，存
在安全隐患&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果时间太短，业务还没执行完，锁就释放了，就有可能发生我的业务还在执行，其他线程就获取到锁了；如果设置时间太长，将来如果我这个业务出现故障，那么很长一段时间其他线程都不能获取到锁，都在等待锁超时释放；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主从一致性
如果Redis提供了主从集群，主从同步存在延迟，当主宕机时，如果从并同步主中的
锁数据，则会出现锁实现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在某些情况下，比如现在有一个线程在主节点获取了锁。由于加锁是 set 操作，是一个写操作，这个写操作在主节点完成后，如果还没有同步到从节点，因为存在延迟，主节点突然宕机了。此时会选一个新的从节点作为主节点，而这个从节点因为没有完成同步，所以没有锁的标识。这样其他线程可能会趁机获取到锁，导致出现多个线程持有锁的情况。在极端情况下可能会出现安全问题。当然，这种情况发生的概率很低，因为主从同步的延迟通常非常低，往往在毫秒级别甚至更低。&lt;/p&gt;
&lt;p&gt;因此，前面提到的这4个问题，要么发生概率极低，要么并不是所有业务都有这样的需求，有些业务需要，有些业务不需要。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Redisson&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Redisson是一个在Redis的基础上实现的Java驻内存数据网格（In-Memory Data Grid）。它不仅提供了一系列的分布式
的Java常用对象，还提供了许多分布式服务，其中就包含了各种分布式锁的实现。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;是在Redis基础上实现的分布式工具的集合，在分布式系统下用的工具，它都有。（分布式锁就是它的一个子集）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Redis提供了分布式锁的多种多样功能&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可重入锁(Reentrant Lock)&lt;/li&gt;
&lt;li&gt;公平锁(Fair Lock)&lt;/li&gt;
&lt;li&gt;联锁(MultiLock)&lt;/li&gt;
&lt;li&gt;红锁(RedLock)&lt;/li&gt;
&lt;li&gt;读写锁(ReadWriteLock)&lt;/li&gt;
&lt;li&gt;信号量(Semaphore)&lt;/li&gt;
&lt;li&gt;可过期性信号量(PermitExpirableSemaphore)&lt;/li&gt;
&lt;li&gt;闭锁(CountDownLatch)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;官网地址：https://redisson.org&lt;/p&gt;
&lt;p&gt;GitHub地址：https://github.com/redisson/redisson&lt;/p&gt;
&lt;h4&gt;Redisson快速入门&lt;/h4&gt;
&lt;p&gt;引入依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.redisson&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;redisson&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.13.6&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置Redisson客户端&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class RedisConfig {

    @Bean
    public RedissonClient redissonClient() {
        // 配置类
        Config config = new Config();
        // 添加 redis 地址，这里添加了单点的地址，也可以使用 config.useClusterServers() 添加集群地址
        config.useSingleServer().setAddress(&quot;redis://192.168.150.101:6379&quot;).setPassword(&quot;123321&quot;);
        // 创建客户端
        return Redisson.create(config);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Resource
private RedissonClient redissonClient;

@Test
void testRedisson() throws InterruptedException {
    // 获取锁（可重入），指定锁的名称
    RLock lock = redissonClient.getLock(&quot;anyLock&quot;);
    // 尝试获取锁，参数分别是：获取锁的最大等待时间（期间会重试），锁自动释放时间，时间单位
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
    // 判断释放获取成功
    if(isLock){
        try {
            System.out.println(&quot;执行业务&quot;);
        }finally {
            // 释放锁
            lock.unlock();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;业务改进：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final RedissonClient redissonClient;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 查优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = voucher.getBeginTime();
        if (beginTime.isAfter(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀尚未开始&quot;);
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 未开始
            return Result.fail(&quot;秒杀已经结束&quot;);
        }
        if (voucher.getStock() &amp;lt; 1) {
            return Result.fail(&quot;库存不足&quot;);
        }
        Long userId = UserHolder.getUser().getId();
        // 创建锁对象
//        SimpleRedisLock simpleRedisLock = new SimpleRedisLock(redisTemplate, &quot;order:&quot; + userId);
        RLock lock = redissonClient.getLock(&quot;order:&quot; + userId);
        boolean flag = lock.tryLock();  // 立即尝试获取锁，获取不到直接返回 false，且不重试
        if (!flag) {
            // 获取锁失败
            return Result.fail(&quot;不允许重复下单&quot;);
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 释放锁
            lock.unlock();
//            simpleRedisLock.unlock();
        }
    }


    @Transactional(timeout = 5000, rollbackFor = Exception.class)
    public Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        // 一人一单
        Integer count = lambdaQuery()
                .eq(VoucherOrder::getUserId, userId)
                .eq(VoucherOrder::getVoucherId, voucherId)
                .count();
        if (count &amp;gt; 0) {
            // 用户至少下过一单
            return Result.fail(&quot;已经购买过了&quot;);
        }
        // 扣减库存
        boolean flag = seckillVoucherService.update().setSql(&quot;stock = stock - 1&quot;)   // set stock = stock - 1
                .eq(&quot;voucher_id&quot;, voucherId)
                .gt(&quot;stock&quot;, 0)  // where id = ? and stock &amp;gt; 0
                .update();
        if (!flag) {
            return Result.fail(&quot;库存不足&quot;);
        }
        // 创建订单
        long orderId = redisIdWorker.nextId(&quot;order&quot;);
        VoucherOrder voucherOrder = VoucherOrder.builder()
                .id(orderId)
                .userId(userId)
                .voucherId(voucherId)
                .build();
        save(voucherOrder);
        // 返回订单id
        return Result.ok(orderId);

    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Redisson可重入锁原理&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 创建锁对象
RLock lock = redissonClient.getLock(&quot;lock&quot;);

@Test
void method1() {
    boolean isLock = lock.tryLock();
    if(!isLock){
        log.error(&quot;获取锁失败, 1&quot;);
        return;
    }
    try {
        log.info(&quot;获取锁成功, 1&quot;);
        method2();
    } finally {
        log.info(&quot;释放锁, 1&quot;);
        lock.unlock();
    }
}

void method2(){
    boolean isLock = lock.tryLock();
    if(!isLock){
        log.error(&quot;获取锁失败, 2&quot;);
        return;
    }
    try {
        log.info(&quot;获取锁成功, 2&quot;);
    } finally {
        log.info(&quot;释放锁, 2&quot;);
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;method1() 成功加锁（第一次 tryLock() 返回 true）。&lt;/li&gt;
&lt;li&gt;method1() 调用 method2()，此时当前线程已持有锁。&lt;/li&gt;
&lt;li&gt;method2() 再次调用 lock.tryLock()：
&lt;ul&gt;
&lt;li&gt;由于是同一个线程，Redisson 允许重入，tryLock() 返回 true。&lt;/li&gt;
&lt;li&gt;锁的内部重入计数（hold count）变为 2。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;method2() 执行完，调用 lock.unlock()：
&lt;ul&gt;
&lt;li&gt;重入计数减为 1。&lt;/li&gt;
&lt;li&gt;锁并未真正释放（因为计数 &amp;gt; 0）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;回到 method1() 的 finally 块，再次 lock.unlock()：
&lt;ul&gt;
&lt;li&gt;重入计数减为 0，锁真正释放。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果是不可重入的：method1在方法内部调用method2(method1和method2出于同一个线程)，那么method1已经拿到一把锁了，想进入method2中拿另外一把锁，必然是拿不到的，于是就出现了死锁;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redisson可重入锁原理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;利用Hash结构记录线程ID和重入次数‌，使用计数器维护锁状态&lt;/p&gt;
&lt;p&gt;当同一个线程第一次获取锁时，Redis会记录下这个线程的ID，并将锁的持有次数设置为1。如果这个线程再次请求锁（即可重入操作），Redisson会检测到当前持有锁的线程ID与当前线程相同，于是不会重新设置锁，而是增加计数器，表示这个线程再次持有了锁‌。&lt;/p&gt;
&lt;p&gt;并且同一线程可以多次获取同一个锁，且只有当所有锁释放操作都完成后，锁才会真正释放。Redisson通过维护一个计数器来实现这一特性。每次释放锁时，Redisson会减少计数器，只有当计数器减为0时，锁才会真正释放‌。&lt;/p&gt;
&lt;p&gt;步骤：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251210233216892.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;获取锁的Lua脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;local key = KEYS[1];  -- 锁的key
local threadId = ARGV[1];  -- 线程的唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间

-- 判断锁是否存在
if (redis.call(&apos;exists&apos;,key) == 0) then
    -- 锁不存在，获取锁
    redis.call(&apos;hset&apos;, key, threadId, 1);
    --设置有效期
    redis.call(&apos;expire&apos;, key, releaseTime);
    -- 返回结果
    return 1;
end

--锁已存在，判断锁是否是自己的
if (redis.call(&apos;hexists&apos;,key,threadId) == 1) then
    --锁是自己的，重置过期时间，计数器加一
    redis.call(&apos;hincrby&apos;,key,threadId,1);
    redis.call(&apos;expire&apos;,key,releaseTime);
    return 1;
end
return 0; -- 锁不是自己的，获取锁失败
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;释放锁的Lua脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;local key = KEYS[1];  -- 锁的key
local threadId = ARGV[1];  -- 线程的唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间

-- 判断锁是否是自己的
if (redis.call(&apos;hexists&apos;,key,threadId) == 0) then
    --锁不是自己的，直接返回
    return nil;
end
-- 是自己的锁，则重入次数减一
local count = redis.call(&apos;hincrby&apos;,key,threadId,-1);

-- 判断计数器是否已减为0
if (count &amp;gt; 0) then
    -- 计数器不为0，不能释放锁，重置锁有效期后返回
    redis.call(&apos;expire&apos;,key,releaseTime);
    return nil;
else
    -- 计数器减为零，释放锁
    redis.call(&apos;del&apos;,key);
    return nil;
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Redisson的锁重试和WatchDog机制&lt;/h4&gt;
&lt;p&gt;参数解释：&lt;/p&gt;
&lt;p&gt;tryLock方法会接受&lt;code&gt;等待时间&lt;/code&gt;、&lt;code&gt;超时时间&lt;/code&gt;和&lt;code&gt;时间单位&lt;/code&gt;三个参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
private final RedissonClient redissonClient;

RLock lock = redissonClient.getLock(&quot;order:&quot; + userId);
boolean flag = lock.tryLock(); // 不等待，立即尝试获取锁，获取不到直接返回 false，且不重试
if (!flag) {
    // 获取锁失败
}


 // 最多等待10秒获取锁，成功后锁自动30秒过期
boolean flag = lock.tryLock(10, 30, TimeUnit.SECONDS);

// 最多等待10s，如果10s内获取不到锁就失败
flag = lock.tryLock(10, TimeUnit.SECONDS); // 如果不传leaseTime参数，默认是30秒(看门狗的默认时间)，锁过期
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;锁重试是利用消息订阅和信号量机制，来实现锁重试的，它不是无休止的盲等机制；&lt;/p&gt;
&lt;p&gt;在tryLock的源码中可以看到，释放锁的操作(lua脚本)，在执行del语句后通过publish来发送释放锁的信号&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;看门狗机制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WatchDog机制，也称为看门狗机制，通过动态续约锁的过期时间，确保了分布式锁在持有者未主动释放之前不会被其他线程获取，从而有效防止了因锁超时而导致的线程安全问题、数据一致性问题。&lt;/p&gt;
&lt;p&gt;分布式锁通常带有一个过期时间(TTL)来防止因锁未释放而导致的死锁问题。然而，业务逻辑执行时间可能超过锁的默认过期时间，如果没有扩展锁的时间，锁会自动过期并释放，导致其他线程获得锁，进而引发数据一致性问题。WatchDog机制的作用就是动态续约锁的过期时间，确保锁在持有者未主动释放之前不会被其他线程获取‌。&lt;/p&gt;
&lt;p&gt;若获取锁时没有指定锁的自动释放时间，leaseTime参数默认为-1，在异步获取锁时会将锁的自动释放时间设置为WatchdogTimeout，默认为30s。一旦锁被获取，Redisson会启动一个WatchDog定时任务。这个定时任务每隔一段时间（通常是10秒）会检查锁的状态，如果锁仍然有效，此时这个timeTask 就触发了,它会自动将锁的持有时间再延长30秒，如果操作成功，那么此时就会递归调用自己，再重新设置一个&lt;code&gt;timeTask()&lt;/code&gt;，于是再过10s后又再设置一个timeTask，完成不停的续约。这样，即使业务逻辑执行时间超过了锁的初始过期时间，锁也不会被自动释放‌。&lt;/p&gt;
&lt;p&gt;当客户端完成需要锁定的操作后，会手动释放锁，并删除定时任务。如果客户端在操作过程中发生异常或崩溃，WatchDog也会在锁的持有时间结束后自动释放锁，以避免死锁的发生‌。&lt;/p&gt;
&lt;p&gt;获取锁释放锁逻辑：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251215221556231.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Redisson 分布式锁原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可重入&lt;/strong&gt;：利用 hash 结构记录线程 id 和重入次数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可重试&lt;/strong&gt;：利用信号量和 PubSub 功能实现等待、唤醒，获取锁失败的重试机制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时续约&lt;/strong&gt;：利用 watchDog，每隔一段时间（&lt;code&gt;releaseTime / 3&lt;/code&gt;），重置超时时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说解决了不可重入、不可重试、超时释放的问题&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Redisson锁的MultiLock原理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了提高Redis的可用性，我们会搭建集群或者主从，以主从为例，主节点负责增删改，从节点负责读，主机会将数据同步给从机，但在主从同步完成之前，如果主节点宕机，Redis的哨兵机制会选择一个新的从节点作为主节点。然而，这个新的主节点上并没有之前的锁信息，导致锁失效。这样，当新的线程发来请求时，又可以获取到锁，从而出现两个线程并发访问安全问题。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，Redisson提出了‌MultiLock‌锁（联锁）。MultiLock锁不使用主从关系，而是将每个Redis节点都视为独立的节点，都可以进行读写操作。在获取锁时，需要在所有的Redis服务器上都要获取锁，只有所有的服务器都写入成功，才算是加锁成功，假设现在某个节点挂了，那么他去获取锁的时候，只要有一个节点拿不到，都不能算是加锁成功。这样，即使某个节点宕机 ，由于其他节点上仍然保留有锁的标识，因此新的线程无法在所有节点上都获取到锁，从而保证了锁的一致性和安全性‌。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251218000658790.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们先使用虚拟机额外搭建两个Redis节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress(&quot;redis://192.168.137.130:6379&quot;)
                .setPassword(&quot;root&quot;);
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient2() {
        Config config = new Config();
        config.useSingleServer().setAddress(&quot;redis://92.168.137.131:6380&quot;)
                .setPassword(&quot;root&quot;);
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient3() {
        Config config = new Config();
        config.useSingleServer().setAddress(&quot;redis://92.168.137.132:6381&quot;)
                .setPassword(&quot;root&quot;);
        return Redisson.create(config);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用联锁，我们首先要注入三个RedissonClient对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;

private RLock lock;

@BeforeEach
void setUp() {
    RLock lock1 = redissonClient.getLock(&quot;lock&quot;);
    RLock lock2 = redissonClient2.getLock(&quot;lock&quot;);
    RLock lock3 = redissonClient3.getLock(&quot;lock&quot;);
    lock = redissonClient.getMultiLock(lock1, lock2, lock3);    // 用哪一client调用都没有问题，都是一样的；并且这里的每一个节点的锁都是可重入锁
}

@Test
void method1() {
    boolean success = lock.tryLock();
    redissonClient.getMultiLock();
    if (!success) {
        log.error(&quot;获取锁失败，1&quot;);
        return;
    }
    try {
        log.info(&quot;获取锁成功&quot;);
        method2();
    } finally {
        log.info(&quot;释放锁，1&quot;);
        lock.unlock();
    }
}

void method2() {
    RLock lock = redissonClient.getLock(&quot;lock&quot;);
    boolean success = lock.tryLock();
    if (!success) {
        log.error(&quot;获取锁失败，2&quot;);
        return;
    }
    try {
        log.info(&quot;获取锁成功，2&quot;);
    } finally {
        log.info(&quot;释放锁，2&quot;);
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所谓的联锁，就是多个独立的锁，而每一个独立的锁，跟之前的原理是一样的；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1）不可重入Redis分布式锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原理：利用 &lt;code&gt;setnx&lt;/code&gt; 的互斥性；利用 &lt;code&gt;ex&lt;/code&gt; 避免死锁；释放锁时判断线程标示&lt;/li&gt;
&lt;li&gt;缺陷：不可重入、无法重试、锁超时失效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2）可重入的Redis分布式锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原理：利用 hash 结构，记录线程标示和重入次数；利用 &lt;code&gt;watchDog&lt;/code&gt; 延续锁时间；利用信号量和消息订阅控制锁重试等待&lt;/li&gt;
&lt;li&gt;缺陷：redis宕机引起锁失效问题、主从延迟或宕机导致节点未同步锁问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3）Redisson的multiLock：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原理：多个独立的Redis节点，必须在所有节点都获取重入锁，才算获取锁成功&lt;/li&gt;
&lt;li&gt;缺陷：运维成本高、实现复杂&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
&lt;code&gt;EX&lt;/code&gt; 如何避免死锁？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EX&lt;/code&gt; 是 Redis &lt;code&gt;SET&lt;/code&gt; 命令的一个选项，用于 &lt;strong&gt;设置 key 的过期时间（单位：秒）&lt;/strong&gt;。&lt;br /&gt;
即使持有锁的客户端异常退出、或忘记调用 unlock()、业务异常无法释放锁，Redis 也会在设定的 EX 时间后 自动删除该锁 key，从而释放锁，让其他客户端有机会重新获取。利用 EX 设置锁的自动过期时间，相当于给分布式锁加了一个“安全熔断机制”——即使程序出错，锁也能在有限时间内自动释放，从而有效避免死锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;利用信号量控制锁重试等待&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在分布式系统中，当多个客户端竞争同一把锁时，&lt;strong&gt;未抢到锁的客户端通常需要“重试”&lt;/strong&gt;（即过一会儿再尝试获取）。但如果每个客户端都无限制地、高频地轮询重试，会导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis 服务器压力剧增（大量无效请求）&lt;/li&gt;
&lt;li&gt;客户端 CPU 空转&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;信号量作为协调器&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;当客户端首次 &lt;code&gt;tryLock()&lt;/code&gt; 失败时，&lt;strong&gt;不立即返回失败&lt;/strong&gt;，而是向 Redis 注册一个“等待许可”请求。&lt;/li&gt;
&lt;li&gt;Redisson 在 Redis 中维护一个与锁关联的 &lt;strong&gt;信号量&lt;/strong&gt;，记录有多少客户端在排队等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放锁时唤醒等待者&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;当持有锁的客户端调用 &lt;code&gt;unlock()&lt;/code&gt; 时，除了删除锁 key，还会 &lt;strong&gt;释放一个信号量许可&lt;/strong&gt;（&lt;code&gt;release()&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;此操作会 &lt;strong&gt;通知（或唤醒）一个正在等待的客户端&lt;/strong&gt;，让它立即重试获取锁，而不是被动轮询。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持超时与公平性&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;客户端可设置最大等待时间（如 &lt;code&gt;tryLock(10, 30, TimeUnit.SECONDS)&lt;/code&gt;：最多等 10 秒，持锁 30 秒）。&lt;/li&gt;
&lt;li&gt;Redisson 默认按 &lt;strong&gt;FIFO（先到先得）&lt;/strong&gt; 唤醒等待者，保证公平性。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Redis优化秒杀&lt;/h3&gt;
&lt;h3&gt;Redis消息队列实现异步秒杀&lt;/h3&gt;
&lt;h2&gt;达人探店&lt;/h2&gt;
&lt;h2&gt;好友关注&lt;/h2&gt;
&lt;h2&gt;附近的商户&lt;/h2&gt;
&lt;h2&gt;用户签到&lt;/h2&gt;
&lt;h2&gt;UV统计&lt;/h2&gt;
</content:encoded></item><item><title>Redis - 基础</title><link>https://zzyang.top/posts/redis-basic/</link><guid isPermaLink="true">https://zzyang.top/posts/redis-basic/</guid><pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;初识Redis&lt;/h2&gt;
&lt;p&gt;Redis是一种键值型的NoSQL数据库，这里有两个关键字&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;键值型&lt;/li&gt;
&lt;li&gt;NoSQL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中键值型是指Redis中存储的数据都是以Key-Value键值对的形式存储，而Value的形式多种多样，可以使字符串、数值甚至Json&lt;/p&gt;
&lt;p&gt;而NoSQL则是相对于传统关系型数据库而言，有很大差异的一种数据库&lt;/p&gt;
&lt;h3&gt;认识NoSQL&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;NoSql&lt;/code&gt;可以翻译做Not Only Sql（不仅仅是SQL），或者是No Sql（非Sql的）数据库。是相对于传统关系型数据库而言，有很大差异的一种特殊的数据库，因此也称之为&lt;code&gt;非关系型数据库&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;结构化与非结构化&lt;/h4&gt;
&lt;p&gt;传统关系型数据库是结构化数据，每张表在创建的时候都有严格的约束信息，如字段名、字段数据类型、字段约束等，插入的数据必须遵循这些约束&lt;/p&gt;
&lt;p&gt;而NoSQL则对数据库格式没有约束，可以是键值型，也可以是文档型，甚至是图格式&lt;/p&gt;
&lt;h4&gt;关联与非关联&lt;/h4&gt;
&lt;p&gt;传统数据库的表与表之间往往存在关联，例如外键约束&lt;/p&gt;
&lt;p&gt;而非关系型数据库不存在关联关系，要维护关系要么靠代码中的业务逻辑，要么靠数据之间的耦合&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  id: 1,
  name: &quot;张三&quot;,
  orders: [
    {
       id: 1,
       item: {
	 id: 10, title: &quot;荣耀6&quot;, price: 4999
       }
    },
    {
       id: 2,
       item: {
	 id: 20, title: &quot;小米11&quot;, price: 3999
       }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如此处要维护张三与两个手机订单的关系，不得不冗余的将这两个商品保存在张三的订单文档中，不够优雅，所以建议使用业务逻辑来维护关联关系&lt;/p&gt;
&lt;h4&gt;查询方式&lt;/h4&gt;
&lt;p&gt;传统关系型数据库会基于Sql语句做查询，语法有统一的标准&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT id, age FROM tb_user WHERE id = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而不同的非关系型数据库查询语法差异极大&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Redis:  get user:1
MongoDB: db.user.find({_id: 1})
elasticsearch:  GET http://localhost:9200/users/1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;事务&lt;/h4&gt;
&lt;p&gt;传统关系型数据库能满足事务的ACID原则(原子性、一致性、独立性及持久性)&lt;/p&gt;
&lt;p&gt;而非关系型数据库汪汪不支持事务，或者不能要个保证ACID的特性，只能实现计本的一致性&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;NoSQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数据结构&lt;/td&gt;
&lt;td&gt;结构化 (Structured)&lt;/td&gt;
&lt;td&gt;非结构化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据关联&lt;/td&gt;
&lt;td&gt;关联的 (Relational)&lt;/td&gt;
&lt;td&gt;无关联的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询方式&lt;/td&gt;
&lt;td&gt;SQL 查询&lt;/td&gt;
&lt;td&gt;非SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事务特性&lt;/td&gt;
&lt;td&gt;ACID&lt;/td&gt;
&lt;td&gt;BASE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储方式&lt;/td&gt;
&lt;td&gt;磁盘&lt;/td&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;扩展性&lt;/td&gt;
&lt;td&gt;垂直&lt;/td&gt;
&lt;td&gt;水平&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;使用场景&lt;/td&gt;
&lt;td&gt;1) 数据结构固定&amp;lt;br&amp;gt;2) 对一致性、安全性要求不高&lt;/td&gt;
&lt;td&gt;1) 数据结构不固定&amp;lt;br&amp;gt;2) 相关业务对数据安全性、一致性要求较高&amp;lt;br&amp;gt;3) 对性能要求高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;存储方式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关系型数据库基于磁盘进行存储，会有大量的磁盘IO，对性能有一定影响&lt;/li&gt;
&lt;li&gt;非关系型数据库，他们的操作更多的是依赖于内存来操作，内存的读写速度会非常快，性能自然会好一些&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;扩展性&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关系型数据库集群模式一般是主从，主从数据一致，起到数据备份的作用，称为垂直扩展。&lt;/li&gt;
&lt;li&gt;非关系型数据库可以将数据拆分，存储在不同机器上，可以保存海量数据，解决内存大小有限的问题。称为水平扩展。&lt;/li&gt;
&lt;li&gt;关系型数据库因为表之间存在关联关系，如果做水平扩展会给数据查询带来很多麻烦&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;认识Redis&lt;/h3&gt;
&lt;p&gt;Redis诞生于2009年全称是Remote Dictionary Server，远程词典服务器，是一个基于内存的键值型NoSQL数据库(使用C语言编写)。&lt;/p&gt;
&lt;p&gt;特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;键值（key-value）型，value支持多种不同数据结构，功能丰富&lt;/li&gt;
&lt;li&gt;单线程，每个命令具备原子性&lt;/li&gt;
&lt;li&gt;低延迟，速度快(基于内存、IO多路复用、良好的编码)&lt;/li&gt;
&lt;li&gt;支持数据持久化&lt;/li&gt;
&lt;li&gt;支持主从集群、分片集群&lt;/li&gt;
&lt;li&gt;支持多语言客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作者：Antirez &lt;a href=&quot;http://oldblog.antirez.com/&quot;&gt;博客地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Redis官网：https://redis.io/&lt;/p&gt;
&lt;p&gt;:::info
&lt;strong&gt;Redis6.0已经变多线程了?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是因为Redis6.0的多线程仅仅是在对于网络请求处理这块，而核心的命令的执行这一部分依然是单线程，所以说Redis6.0它是单线程也是没有问题的。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;安装Redis&lt;/h3&gt;
&lt;p&gt;:::tip
Redis的作者根本就没有编写Windows版本的Redis，网上的win版本redis并不是官方提供的，而是微软自己编译的
:::&lt;/p&gt;
&lt;p&gt;先安装&lt;a href=&quot;https://zhuanlan.zhihu.com/p/19963662677&quot;&gt;VMware&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;再安装&lt;a href=&quot;https://www.cnblogs.com/tanghaorong/p/13210794.html&quot;&gt;镜像&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;单机安装Redis&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;安装Redis依赖&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y gcc tcl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果用不了，参考这个：&lt;a href=&quot;https://www.cnblogs.com/slgkaifa/p/19141048&quot;&gt;新装 CentOS 7 切换 yum 源&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上传安装包并解压&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;tar.gz包下载地址：https://redis.io/downloads/&lt;/p&gt;
&lt;p&gt;将该包上传到&lt;code&gt;/user/local/src&lt;/code&gt;目录&lt;/p&gt;
&lt;p&gt;解压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost /]# cd /usr/local/src
[root@localhost src]# ll
总用量 2440
-rw-r--r--. 1 root root 2496149 11月 11 22:56 redis-6.2.14.tar.gz
[root@localhost src]# tar -zxvf redis-6.2.14.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入redis目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd redis-6.2.14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make &amp;amp;&amp;amp; make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有出错，应该就安装成功了。&lt;/p&gt;
&lt;p&gt;默认的安装路径是在/usr/Local/bin目录下：&lt;/p&gt;
&lt;p&gt;该目录以及默认配置到环境变量，因此可以在任意目录下运行这些命令。其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;redis-cli:是redis提供的命令行客户端&lt;/li&gt;
&lt;li&gt;redis-server：是redis的服务端启动脚本&lt;/li&gt;
&lt;li&gt;redis-sentinel：是redis的哨兵启动脚本&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;启动方式&lt;/h4&gt;
&lt;p&gt;redis的启动方式有很多种，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认启动&lt;/li&gt;
&lt;li&gt;指定配置启动&lt;/li&gt;
&lt;li&gt;开机自启&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;默认启动&lt;/h5&gt;
&lt;p&gt;进入redis安装目录，执行redis-server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd redis-6.2.14

redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是前台启动方式，如果退出，redis就停止了。&lt;/p&gt;
&lt;h5&gt;指定配置启动&lt;/h5&gt;
&lt;p&gt;如果要让Redis以后台方式启动，则必须修改Redis配置文件，就在我们之前解压的redis安装包下
（&lt;code&gt;/usr/local/src/redis-6.2.6&lt;/code&gt;），名字叫redis.conf:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp redis.conf redis.conf.bck
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后修改redis.conf文件中的一些配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#允许访问的地址，默认是127.0.0.1，会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问，生产环境不要设置为0.0.0.0
bind 0.0.0.0
# 守护进程，修改为yes后即可后台运行
daemonize yes

# 密码，设置后访可Redis必须输入密码
requirepass 123321
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis的其它常见配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#监听的端口
port 6379
#工作目录，默认是当前日录，也就是运行redis-server时的命令，日志、持久化等文件会保存在这个目录
dir .
# 数据库数量，设置为1，代表只使用1个库，默认有16个库，编号0~15
databases 1
#设置redis能够使用的最大内存
maxmemory 512mb
#日志文件，默认为空，不记录日志，可以指定日志文件名(日志的位置就在dir所指定的位置)
logfile &quot;redis.log&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动Redis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 进入redis安装目录
cd /usr/local/src/redis-6.2.14

# 启动redis
redis-server redis.conf

 # 查看redis进程是否启动成功
ps -ef|grep redis

# 杀掉redis进程
kill -9 pid


&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;开机自启&lt;/h5&gt;
&lt;p&gt;我们也可以通过配置来实现开机自启。&lt;/p&gt;
&lt;p&gt;首先，新建一个系统服务文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vi /etc/systemd/system/redis.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=redis-server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/bin/redis-server /usr/local/src/redis-6.2.14/redis.conf
PrivateTmp=true

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重载系统服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们可以用下面这组命令来操作redis了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动redis
systemctl start redis

# 查看运行状态
systemctl status redis

# 停止redis
systemctl stop redis

# 重启redis
systemctl restart redis

systemctl enable redis # 设置开机自启

# 关闭开机自启(disable 不会停止当前正在运行的服务，只影响下次开机。如果你想立即停止服务 + 禁用自启，需要两个命令一起用。)
systemctl disable redis

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是一个典型的 systemd 服务配置文件，用于在 Linux 系统上开机自启 Redis 服务。
其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ExecStart&lt;/code&gt; 指定了启动命令和配置文件路径；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;After=network.target&lt;/code&gt; 表示在网络启动后运行；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WantedBy=multi-user.target&lt;/code&gt; 表示开机时启用该服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Redis客户端&lt;/h4&gt;
&lt;p&gt;安装完成Redis，我们就可以操作Redis，实现数据的CRUD了。这需要用到Redis客户端，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令行客户端&lt;/li&gt;
&lt;li&gt;图形化桌面客户端&lt;/li&gt;
&lt;li&gt;编程客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Redis命令行客户端&lt;/h5&gt;
&lt;p&gt;Redis安装完成后就自带了命令行客户端：redis-cli，使用方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli [options] [command]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见的options有&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-h 127.0.0.1&lt;/code&gt;: 指定要连接的redis节点的IP地址，默认是&lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p 6379&lt;/code&gt;: 指定要连接的redis节点的端口，默认是&lt;code&gt;6379&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-a 123123&lt;/code&gt;: 指定要连接的redis节点的密码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中的commonds就是Redis的操作命令，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ping&lt;/code&gt;：与redis服务端做心跳测试，服务端正常会返回&lt;code&gt;pong&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不指定commond时，会进入&lt;code&gt;redis-cLi&lt;/code&gt;的交互控制台：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli

redis-cli -a 密码

127.0.0.1:6379&amp;gt; ping
PONG

127.0.0.1:6379&amp;gt; set name jack
OK
127.0.0.1:6379&amp;gt; get name
&quot;jack&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@192 redis-6.2.14]# redis-cli
127.0.0.1:6379&amp;gt; get name
(error) NOAUTH Authentication required.
127.0.0.1:6379&amp;gt; Auth 密码
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;图形化桌面客户端&lt;/h5&gt;
&lt;p&gt;GitHub上的大神编写了Redis的图形化桌面客户端，地址：https://github.com/uglide/RedisDesktopManager&lt;/p&gt;
&lt;p&gt;不过该仓库提供的是RedisDesktopManager的源码，并未提供windows安装包。&lt;/p&gt;
&lt;p&gt;在下面这个仓库可以找到安装包：https://github.com/lework/RedisDesktopManager-Windows/releases&lt;/p&gt;
&lt;p&gt;或者使用这个&lt;a href=&quot;https://redis.tinycraft.cc/zh/&quot;&gt;现代化Redis桌面客户端&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果连不上，设置防火墙&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 永久放行 Redis 端口（6379）
firewall-cmd --permanent --add-port=6379/tcp

# 重载防火墙规则
firewall-cmd --reload

# 验证端口是否已开放
firewall-cmd --list-ports | grep 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Redis命令&lt;/h2&gt;
&lt;h3&gt;Redis数据结构介绍&lt;/h3&gt;
&lt;p&gt;Redis是一个key-value的数据库，key一般是String类型，不过value的类型多种多样：&lt;/p&gt;
&lt;h1&gt;Redis 数据类型分类&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;示例值&lt;/th&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hello world&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{name: &quot;Jack&quot;, age: 21}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[A -&amp;gt; B -&amp;gt; C -&amp;gt; C]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{A, B, C}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SortedSet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{A: 1, B: 2, C: 3}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GEO&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{A: (120.3, 30.5)}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;特殊类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BitMap&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0110110101110101011&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;特殊类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HyperLog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0110110101110101011&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;特殊类型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基本类型&lt;/strong&gt;：String、Hash、List、Set、SortedSet 是 Redis 最常用的五种数据结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特殊类型&lt;/strong&gt;：GEO（地理位置）、BitMap（位图）、HyperLog（基数估算）是基于基础结构封装的高级功能，用于特定场景优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Redis为了方便我们学习，将操作不同数据类型的命令也做了分组，在官网  https://redis.io/commands  可以查看
到不同的命令&lt;/p&gt;
&lt;h3&gt;Redis通用命令&lt;/h3&gt;
&lt;p&gt;通用指令是部分数据类型的，都可以使用的指令，常见的有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;KEYS&lt;/strong&gt;：查看符合模板的所有 key，&amp;lt;span style=&quot;color: red;&quot;&amp;gt;不建议在生产环境设备上使用&amp;lt;/span&amp;gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DEL&lt;/strong&gt;：删除一个指定的 key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EXISTS&lt;/strong&gt;：判断 key 是否存在&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EXPIRE&lt;/strong&gt;：给一个 key 设置有效期，有效期到期时该 key 会被自动删除&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL&lt;/strong&gt;：查看一个 KEY 的剩余有效期&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过 &lt;code&gt;help [command]&lt;/code&gt; 可以查看一个命令的具体用法，例如：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KEYS命令：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; help KEYS

  KEYS pattern
  summary: Find all keys matching the given pattern
  since: 1.0.0
  group: generic

127.0.0.1:6379&amp;gt; 

# 列出所有key
127.0.0.1:6379&amp;gt; KEYS *
1) &quot;name&quot;

# 模糊查询key 以n开头的key
127.0.0.1:6379&amp;gt; KEYS n*
1) &quot;name&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;DEL命令：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; KEYS *
1) &quot;age&quot;
2) &quot;name&quot;
127.0.0.1:6379&amp;gt; DEL age name
(integer) 2
127.0.0.1:6379&amp;gt; KEYS *
(empty array)
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;EXISTS命令：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; help EXISTS

  EXISTS key [key ...]
  summary: Determine if a key exists
  since: 1.0.0
  group: generic

127.0.0.1:6379&amp;gt; EXISTS name
(integer) 0
127.0.0.1:6379&amp;gt; set age 18
OK
127.0.0.1:6379&amp;gt; EXISTS age
(integer) 1
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;EXPIRE命令 TTL命令：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; help EXPIRE

  EXPIRE key seconds
  summary: Set a key&apos;s time to live in seconds
  since: 1.0.0
  group: generic

127.0.0.1:6379&amp;gt; EXPIRE age 20
(integer) 1
127.0.0.1:6379&amp;gt; TTL age
(integer) 16
127.0.0.1:6379&amp;gt; TTL age
(integer) -2
127.0.0.1:6379&amp;gt; 


# -2 表示key者已经过期
# -1 表示永久有效
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;String类型&lt;/h3&gt;
&lt;p&gt;String类型，也就是字符串类型，是Redis中最简单的存储类型&lt;/p&gt;
&lt;p&gt;其value是字符串，不过根据字符串的格式不同，又可以分为3类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;string：普通字符串&lt;/li&gt;
&lt;li&gt;int：整数类型，可以做自增、自减操作&lt;/li&gt;
&lt;li&gt;float：浮点类型，可以做自增、自减操作
不管是哪种格式，底层都是字节数组形式存储，只不过是编码方式不同，字符串类型的最大空间不能超过512M&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;String类型常见命令：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SET：添加或者修改已经存在的一个String类型的键值对&lt;/li&gt;
&lt;li&gt;GET：根据key获取String类型的value&lt;/li&gt;
&lt;li&gt;MSET：批量添加多个String类型的键值对&lt;/li&gt;
&lt;li&gt;MGET：根据多个key获取多个String类型的value&lt;/li&gt;
&lt;li&gt;INCR：让一个整型的key自增1。&lt;/li&gt;
&lt;li&gt;INCRBY:让一个整型的key自增并指定步长，例如：incrby num 2 让num值自增2&lt;/li&gt;
&lt;li&gt;INCRBYFLOAT：让一个浮点类型的数字自增并指定步长&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; MSET k1 v1 k2 v2
OK
127.0.0.1:6379&amp;gt; MGET k1 k2
1) &quot;v1&quot;
2) &quot;v2&quot;
127.0.0.1:6379&amp;gt; 


127.0.0.1:6379&amp;gt; set age 18
OK
127.0.0.1:6379&amp;gt; INCR age
(integer) 19
127.0.0.1:6379&amp;gt; get age
&quot;19&quot;


127.0.0.1:6379&amp;gt; set num 10.1
OK
127.0.0.1:6379&amp;gt; INCRBYFLOAT num 0.5
&quot;10.6&quot;
127.0.0.1:6379&amp;gt; INCRBYFLOAT num 0.5
&quot;11.1&quot;
127.0.0.1:6379&amp;gt; INCRBYFLOAT num 0.5
&quot;11.6&quot;
127.0.0.1:6379&amp;gt; get num
&quot;11.6&quot;
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Key的层级格式&lt;/h4&gt;
&lt;p&gt;Redis没有类似MySQL中Table的概念，那么我们该如何区分不同类型的Key呢？&lt;/p&gt;
&lt;p&gt;例如：需要存储用户、商品信息到Redis，有一个用户的id是1，有一个商品的id恰好也是1，如果此时使用id作为key，那么就会发生冲突，该怎么办？&lt;/p&gt;
&lt;p&gt;我们可以通过给key添加前缀加以区分，不过这个前缀不是随便加的，有一定的规范&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis的key允许有多个单词形成层级结构，多个单词之间用:隔开，格式如下&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;项目名:业务名:类型:id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个格式也并非是固定的，可以根据自己的需求来删除/添加词条，这样我们就可以把不同数据类型的数据区分开了，从而避免了key的冲突问题&lt;/p&gt;
&lt;p&gt;例如我们的项目名称叫 &lt;code&gt;codevision&lt;/code&gt;，有 &lt;code&gt;user&lt;/code&gt; 和 &lt;code&gt;product&lt;/code&gt; 两种不同类型的数据，我们可以这样定义 key：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;user 相关的 key：&lt;code&gt;codevision:user:1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;product 相关的 key：&lt;code&gt;codevision:product:1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果 Value 是一个 Java 对象，例如一个 User 对象，则可以将对象序列化为 JSON 字符串后存储：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KEY&lt;/th&gt;
&lt;th&gt;VALUE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codevision:user:1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{&quot;id&quot;:1, &quot;name&quot;: &quot;Jack&quot;, &quot;age&quot;: 21}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codevision:product:1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{&quot;id&quot;:1, &quot;name&quot;: &quot;小米11&quot;, &quot;price&quot;: 4999}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一旦我们向redis采用这样的方式存储，那么在可视化界面中，redis会以层级结构来进行存储，形成类似于这样的结构，更加方便Redis获取数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;String 类型的三种格式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字符串&lt;/li&gt;
&lt;li&gt;int&lt;/li&gt;
&lt;li&gt;float&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Redis 的 key 格式规范&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[项目名]:[业务名]:[类型]:[id]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Hash类型&lt;/h3&gt;
&lt;p&gt;Hash类型，也叫散列，其value是一个无序字典，类似于Java中的HashMap结构。&lt;/p&gt;
&lt;p&gt;String 类型存储对象的问题:&lt;/p&gt;
&lt;p&gt;当使用 String 类型存储对象时，通常会将对象序列化为 JSON 字符串后存储。这种方式在需要修改对象某个字段时非常不便：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KEY&lt;/th&gt;
&lt;th&gt;VALUE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heima:user:1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{&quot;name&quot;:&quot;Jack&quot;, &quot;age&quot;:21}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heima:user:2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{&quot;name&quot;:&quot;Rose&quot;, &quot;age&quot;:18}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Hash 类型的优势：支持字段级 CRUD&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Hash 结构可以将对象中的每个字段独立存储，允许对单个字段进行增删改查（CRUD），无需操作整个对象：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KEY&lt;/th&gt;
&lt;th&gt;FIELD&lt;/th&gt;
&lt;th&gt;VALUE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heima:user:1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;Jack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heima:user:1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;age&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heima:user:2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;Rose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heima:user:2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;age&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; HSET codevision:user:3 name zhangSan
(integer) 1

127.0.0.1:6379&amp;gt; HGET codevision:user:3 name
&quot;zhangSan&quot;

127.0.0.1:6379&amp;gt; HMSET codevision:user:4 name Lucy sex man age 40
OK
127.0.0.1:6379&amp;gt; HMGET codevision:user:4 name age
1) &quot;Lucy&quot;
2) &quot;40&quot;
127.0.0.1:6379&amp;gt; 

127.0.0.1:6379&amp;gt; HGETALL codevision:user:3
1) &quot;name&quot;
2) &quot;zhangSan&quot;
3) &quot;age&quot;
4) &quot;17&quot;
127.0.0.1:6379&amp;gt; 

127.0.0.1:6379&amp;gt; HKEYS codevision:user:3
1) &quot;name&quot;
2) &quot;age&quot;
127.0.0.1:6379&amp;gt; 

127.0.0.1:6379&amp;gt; HVALS codevision:user:3
1) &quot;zhangSan&quot;
2) &quot;17&quot;
127.0.0.1:6379&amp;gt; 


127.0.0.1:6379&amp;gt; HINCRBY codevision:user:3 age 2
(integer) 19
127.0.0.1:6379&amp;gt; HINCRBY codevision:user:3 age 2
(integer) 21
127.0.0.1:6379&amp;gt; HINCRBY codevision:user:3 age 2
(integer) 23
127.0.0.1:6379&amp;gt; 


127.0.0.1:6379&amp;gt; HSETNX codevision:user:3 age 45  # 没有成功，已经存在
(integer) 0
127.0.0.1:6379&amp;gt; HSETNX codevision:user:3 sex man
(integer) 1
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hash的常见命令有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HSET key field value：添加或者修改hash类型key的field的值&lt;/li&gt;
&lt;li&gt;HGET key field：获取一个hash类型key的field的值&lt;/li&gt;
&lt;li&gt;HMSET：批量添加多个hash类型key的field的值&lt;/li&gt;
&lt;li&gt;HMGET：批量获取多个hash类型key的field的值&lt;/li&gt;
&lt;li&gt;HGETALL：获取一个hash类型的key中的所有的field和value&lt;/li&gt;
&lt;li&gt;HKEYS：获取一个hash类型的key中的所有的field&lt;/li&gt;
&lt;li&gt;HVALS：获取一个hash类型的key中的所有的value&lt;/li&gt;
&lt;li&gt;HINCRBY:让一个hash类型key的字段值自增并指定步长&lt;/li&gt;
&lt;li&gt;HSETNX：添加一个hash类型的key的field值，前提是个这个field不存在，否则不执行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;List类型&lt;/h3&gt;
&lt;p&gt;Redis中的List类型与Java中的LinkedList类似，可以看做一个双向链表结构。既可以支持正向检索和也可以支持反向检索。&lt;/p&gt;
&lt;p&gt;特征也与LinkedList类似：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有序&lt;/li&gt;
&lt;li&gt;元素可以重复&lt;/li&gt;
&lt;li&gt;插入和删除快&lt;/li&gt;
&lt;li&gt;查询速度一般&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用来存储一个有序数据，例如：朋友圈点赞列表，评论列表等。&lt;/p&gt;
&lt;p&gt;List的常见命令有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LPUSH key element ...：向列表左侧插入一个或多个元素&lt;/li&gt;
&lt;li&gt;LPOP key：移除并返回列表左侧的第一个元素，没有则返回nil&lt;/li&gt;
&lt;li&gt;RPUSH key element ...：向列表右侧插入一个或多个元素&lt;/li&gt;
&lt;li&gt;RPOP key：移除并返回列表右侧的第一个元素&lt;/li&gt;
&lt;li&gt;LRANGE key start end：返回一段角标范围内的所有元素&lt;/li&gt;
&lt;li&gt;BLPOP和BRPOP：与LPOP和RPOP类似，只不过在没有元素时等待指定时间，而不是直接返回nil&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251119215647668.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; LPUSH users 1 2 3 
(integer) 3
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; RPUSH users 4 5 6
(integer) 6
127.0.0.1:6379&amp;gt; LPOP users 1
1) &quot;3&quot;
127.0.0.1:6379&amp;gt; RPOP users 1
1) &quot;6&quot;
127.0.0.1:6379&amp;gt; LRANGE users 1 4  # 下标从0开始计
1) &quot;1&quot;
2) &quot;4&quot;
3) &quot;5&quot;
127.0.0.1:6379&amp;gt; 


127.0.0.1:6379&amp;gt; BLPOP users2 10 # 如果在10s内没有元素被加入，则返回nil，如果有元素加入就会获取到
(nil)
(10.10s)
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如何利用List结构模拟一个栈？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;入口和出口在同一边。使用LPUSH/LPOP 或 RPUSH/RPOP进行元素操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如何利用List结构模拟一个队列？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;入口和出口在不同边。使用LPUSH/RPOP 或 RPUSH/LPOP进行元素操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如何利用List结构模拟一个阻塞队列？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;入口和出口在不同边，且出队时采用BLPOP或BRPOP&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;栈：先进后出（像一个人喝酒喝多了吐了）&lt;/p&gt;
&lt;p&gt;队列：先进先出（像一个人喝酒没有吐，从下面排放出去😂）&lt;/p&gt;
&lt;h3&gt;Set类型&lt;/h3&gt;
&lt;p&gt;Redis中的Set结构与Java中的HashSet类似，可以看做一个value为null的HashMap。因为也是一个hash表，因此具备与HashSet类似的特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无序&lt;/li&gt;
&lt;li&gt;元素不可重复&lt;/li&gt;
&lt;li&gt;查找快&lt;/li&gt;
&lt;li&gt;支持交集、并集、差集等功能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Set的常见命令有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SADD key member ...：向set中添加一个或多个元素&lt;/li&gt;
&lt;li&gt;SREM key member ...：移除set中的指定元素&lt;/li&gt;
&lt;li&gt;SCARD key：返回set中元素的个数&lt;/li&gt;
&lt;li&gt;SISMEMBER key member：判断一个元素是否存在于set中&lt;/li&gt;
&lt;li&gt;SMEMBERS：获取set中的所有元素&lt;/li&gt;
&lt;li&gt;SINTER key1 key2 ...：求key1与key2的交集&lt;/li&gt;
&lt;li&gt;SDIFF key1 key2 ...：求key1与key2的差集&lt;/li&gt;
&lt;li&gt;SUNION key1 key2 ...：求key1和key2的并集&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; SADD s1 1 2 3
(integer) 3
127.0.0.1:6379&amp;gt; SMEMBERS s1
1) &quot;1&quot;
2) &quot;2&quot;
3) &quot;3&quot;
127.0.0.1:6379&amp;gt; SREM s1 1
(integer) 1
127.0.0.1:6379&amp;gt; SISMEMBER s1 1
(integer) 0
127.0.0.1:6379&amp;gt; SISMEMBER s1 b
(integer) 0
127.0.0.1:6379&amp;gt; SISMEMBER s1 2
(integer) 1
127.0.0.1:6379&amp;gt; SCARD s1
(integer) 2
127.0.0.1:6379&amp;gt; 


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如图：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INTER交集BC

S1 DIFF S2差集A

并集ABCD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251120001443371.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Set命令的练习&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将下列数据用Redis的Set集合来存储：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;张三的好友有：李四、王五、赵六&lt;/li&gt;
&lt;li&gt;李四的好友有：王五、麻子、二狗&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; SADD zs lisi wangwu zhaoliu
(integer) 3
127.0.0.1:6379&amp;gt; SADD ls wangwu mazi ergou 
(integer) 3
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用Set的命令实现下列功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算张三的好友有几人&lt;/li&gt;
&lt;li&gt;计算张三和李四有哪些共同好友&lt;/li&gt;
&lt;li&gt;查询哪些人是张三的好友却不是李四的好友&lt;/li&gt;
&lt;li&gt;查询张三和李四的好友总共有哪些人&lt;/li&gt;
&lt;li&gt;判断李四是否是张三的好友&lt;/li&gt;
&lt;li&gt;判断张三是否是李四的好友&lt;/li&gt;
&lt;li&gt;将李四从张三的好友列表中移除&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; SCARD zs
(integer) 3
127.0.0.1:6379&amp;gt; 

127.0.0.1:6379&amp;gt; SINTER zs ls
1) &quot;wangwu&quot;
127.0.0.1:6379&amp;gt; 

127.0.0.1:6379&amp;gt; SDIFF zs ls
1) &quot;lisi&quot;
2) &quot;zhaoliu&quot;
127.0.0.1:6379&amp;gt; 

127.0.0.1:6379&amp;gt; SUNION zs ls
1) &quot;zhaoliu&quot;
2) &quot;lisi&quot;
3) &quot;ergou&quot;
4) &quot;wangwu&quot;
5) &quot;mazi&quot;
127.0.0.1:6379&amp;gt; 

127.0.0.1:6379&amp;gt; SISMEMBER zs lisi
(integer) 1

127.0.0.1:6379&amp;gt; SISMEMBER ls zhangsan
(integer) 0

127.0.0.1:6379&amp;gt; SREM zs lisi
(integer) 1
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SortedSet类型&lt;/h3&gt;
&lt;p&gt;Redis的SortedSet是一个可排序的set集合，与Java中的TreeSet有些类似，但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性，可以基于score属性对元素排序，底层的实现是一个跳表（SkipList）加 hash表。&lt;/p&gt;
&lt;p&gt;SortedSet具备下列特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可排序&lt;/li&gt;
&lt;li&gt;元素不重复&lt;/li&gt;
&lt;li&gt;查询速度快&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为SortedSet的可排序特性，经常被用来实现排行榜这样的功能。&lt;/p&gt;
&lt;p&gt;SortedSet的常见命令有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ZADD key score member：添加一个或多个元素到sorted set，如果已经存在则更新其score值&lt;/li&gt;
&lt;li&gt;ZREM key member：删除sorted set中的一个指定元素&lt;/li&gt;
&lt;li&gt;ZSCORE key member：获取sorted set中的指定元素的score值&lt;/li&gt;
&lt;li&gt;ZRANK key member：获取sorted set中的指定元素的排名&lt;/li&gt;
&lt;li&gt;ZCARD key：获取sorted set中的元素个数&lt;/li&gt;
&lt;li&gt;ZCOUNT key min max：统计score值在给定范围内的所有元素的个数&lt;/li&gt;
&lt;li&gt;ZINCRBY key increment member：让sorted set中的指定元素自增，步长为指定的increment值&lt;/li&gt;
&lt;li&gt;ZRANGE key min max：按照score排序后，获取指定排名范围内的元素&lt;/li&gt;
&lt;li&gt;ZRANGEBYSCORE key min max：按照score排序后，获取指定score范围内的元素&lt;/li&gt;
&lt;li&gt;ZDIFF、ZINTER、ZUNION：求差集、交集、并集&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：所有的排名默认都是升序，如果要降序则在命令的Z后面添加REV即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; help @sorted_set

  BZPOPMAX key [key ...] timeout
  summary: Remove and return the member with the highest score from one or more sorted sets, or block until one is available
  since: 5.0.0

  BZPOPMIN key [key ...] timeout
  summary: Remove and return the member with the lowest score from one or more sorted sets, or block until one is available
  since: 5.0.0

  ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]
  summary: Add one or more members to a sorted set, or update its score if it already exists
  since: 1.2.0

  ZCARD key
  summary: Get the number of members in a sorted set
  since: 1.2.0

  ZCOUNT key min max
  summary: Count the members in a sorted set with scores within the given values
  since: 2.0.0

  ZDIFF numkeys key [key ...] [WITHSCORES]
  summary: Subtract multiple sorted sets
  since: 6.2.0

  ZDIFFSTORE destination numkeys key [key ...]
  summary: Subtract multiple sorted sets and store the resulting sorted set in a new key
  since: 6.2.0

  ZINCRBY key increment member
  summary: Increment the score of a member in a sorted set
  since: 1.2.0

  ZINTER numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]
  summary: Intersect multiple sorted sets
  since: 6.2.0

  ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]
  summary: Intersect multiple sorted sets and store the resulting sorted set in a new key
  since: 2.0.0

  ZLEXCOUNT key min max
  summary: Count the number of members in a sorted set between a given lexicographical range
  since: 2.8.9

  ZMSCORE key member [member ...]
  summary: Get the score associated with the given members in a sorted set
  since: 6.2.0

  ZPOPMAX key [count]
  summary: Remove and return members with the highest scores in a sorted set
  since: 5.0.0

  ZPOPMIN key [count]
  summary: Remove and return members with the lowest scores in a sorted set
  since: 5.0.0

  ZRANDMEMBER key [count [WITHSCORES]]
  summary: Get one or multiple random elements from a sorted set
  since: 6.2.0

  ZRANGE key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
  summary: Return a range of members in a sorted set
  since: 1.2.0

  ZRANGEBYLEX key min max [LIMIT offset count]
  summary: Return a range of members in a sorted set, by lexicographical range
  since: 2.8.9

  ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
  summary: Return a range of members in a sorted set, by score
  since: 1.0.5

  ZRANGESTORE dst src min max [BYSCORE|BYLEX] [REV] [LIMIT offset count]
  summary: Store a range of members from sorted set into another key
  since: 6.2.0

  ZRANK key member
  summary: Determine the index of a member in a sorted set
  since: 2.0.0

  ZREM key member [member ...]
  summary: Remove one or more members from a sorted set
  since: 1.2.0

  ZREMRANGEBYLEX key min max
  summary: Remove all members in a sorted set between the given lexicographical range
  since: 2.8.9

  ZREMRANGEBYRANK key start stop
  summary: Remove all members in a sorted set within the given indexes
  since: 2.0.0

  ZREMRANGEBYSCORE key min max
  summary: Remove all members in a sorted set within the given scores
  since: 1.2.0

  ZREVRANGE key start stop [WITHSCORES]
  summary: Return a range of members in a sorted set, by index, with scores ordered from high to low
  since: 1.2.0

  ZREVRANGEBYLEX key max min [LIMIT offset count]
  summary: Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.
  since: 2.8.9

  ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
  summary: Return a range of members in a sorted set, by score, with scores ordered from high to low
  since: 2.2.0

  ZREVRANK key member
  summary: Determine the index of a member in a sorted set, with scores ordered from high to low
  since: 2.0.0

  ZSCAN key cursor [MATCH pattern] [COUNT count]
  summary: Incrementally iterate sorted sets elements and associated scores
  since: 2.8.0

  ZSCORE key member
  summary: Get the score associated with the given member in a sorted set
  since: 1.2.0

  ZUNION numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]
  summary: Add multiple sorted sets
  since: 6.2.0

  ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]
  summary: Add multiple sorted sets and store the resulting sorted set in a new key
  since: 2.0.0

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;SortedSet命令练习&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将班级的下列学生得分存入Redis的SortedSet中：
Jack 85, Lucy 89, Rose 82, Tom 95, Jerry 78, Amy 92, Miles 76&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并实现下列功能：&lt;/li&gt;
&lt;li&gt;删除Tom同学&lt;/li&gt;
&lt;li&gt;获取Amy同学的分数&lt;/li&gt;
&lt;li&gt;获取Rose同学的排名&lt;/li&gt;
&lt;li&gt;查询80分以下有几个学生&lt;/li&gt;
&lt;li&gt;给Amy同学加2分&lt;/li&gt;
&lt;li&gt;查出成绩前3名的同学&lt;/li&gt;
&lt;li&gt;查出成绩80分以下的所有同学&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; ZADD stus 85 Jack 89 Lucy 82 Rose 95 Tom 78 Jerry 92 Amy 76 Miles
(integer) 7
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; ZREM stus Tom
(integer) 1

127.0.0.1:6379&amp;gt; ZRANK stus Rose # 注意返回的排名是从0开始的，(ZRANK升序)
(integer) 2
127.0.0.1:6379&amp;gt; ZREVRANK stus Rose
(integer) 3
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; ZCOUNT stus 0 80
(integer) 2
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; ZINCRBY stus 2 Amy
&quot;94&quot;
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; ZREVRANGE stus 0 2 # 注意这个命令是角标
1) &quot;Amy&quot;
2) &quot;Lucy&quot;
3) &quot;Jack&quot;
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; ZRANGEBYSCORE stus 0 80
1) &quot;Miles&quot;
2) &quot;Jerry&quot;
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Redis的Java客户端&lt;/h2&gt;
&lt;h3&gt;客户端对比&lt;/h3&gt;
&lt;p&gt;在Redis官网中提供了各种语言的客户端，地址：https://redis.io/clients&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251120010555030.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Jedis&lt;/h3&gt;
&lt;h4&gt;Jedis快速入门&lt;/h4&gt;
&lt;p&gt;Jedis的官网地址： &lt;a href=&quot;https://github.com/redis/jedis&quot;&gt;https://github.com/redis/jedis&lt;/a&gt;，我们先来个快速入门&lt;/p&gt;
&lt;p&gt;Jedis使用的基本步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;引入依赖&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建jedis对象，建立连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用jedis，方法名与Redis命令一致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;释放资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入依赖：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;redis.clients&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jedis&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.7.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private Jedis jedis;

@BeforeEach
void setUp() {
    // 建立连接
    jedis = new Jedis(&quot;192.168.150.101&quot;, 6379);
    // 设置密码
    jedis.auth(&quot;123321&quot;);
    // 选择库
    jedis.select(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;测试string&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Test
void testString() {
    // 插入数据，方法名称就是redis命令名称，非常简单
    String result = jedis.set(&quot;name&quot;, &quot;张三&quot;);
    System.out.println(&quot;result = &quot; + result);
    // 获取数据
    String name = jedis.get(&quot;name&quot;);
    System.out.println(&quot;name = &quot; + name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;释放资源&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@AfterEach
void tearDown() {
    // 释放资源
    if (jedis != null) {
        jedis.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.zzyang.jedis;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;

public class JedisTest {

    private Jedis jedis;

    @BeforeEach
    void setUp() {
        jedis = new Jedis(&quot;192.168.9.128&quot;, 6379);
        jedis.auth(&quot;Zzy20020913.&quot;);
        jedis.select(0);
    }

    @Test
    void testString() {
        String result = jedis.set(&quot;name&quot;, &quot;zzy&quot;);
        System.out.println(&quot;result = &quot; + result);

        String name = jedis.get(&quot;name&quot;);
        System.out.println(&quot;name = &quot; + name);
    }

    @Test
    void testHash() {
        // 插入hash数据
        jedis.hmset(&quot;user:5&quot;, Map.of(&quot;name&quot;, &quot;Jack&quot;));
        jedis.hmset(&quot;user:5&quot;, Map.of(&quot;age&quot;, &quot;21&quot;));
        Map&amp;lt;String, String&amp;gt; map = jedis.hgetAll(&quot;user:5&quot;);
        System.out.println(&quot;map = &quot; + map);
    }

    @AfterEach
    void tearDown() {
        if (jedis != null) {
            jedis.close();
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Jedis连接池&lt;/h4&gt;
&lt;p&gt;Jedis本身是线程不安全的，并且频繁的创建和销毁连接会有性能损耗，因此我们推荐大家使用Jedis连接池代替Jedis的
直连方式。&lt;/p&gt;
&lt;p&gt;创建一个工具类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.zzyang.jedis.utils;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisConnectionFactory {

    private static final JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(8); // 最大连接数
        jedisPoolConfig.setMaxIdle(8);  // 最大空闲连接数
        jedisPoolConfig.setMinIdle(0);   // 最小空闲连接数
        jedisPoolConfig.setMaxWaitMillis(1000); // 设置获取连接的最大等待时间，单位毫秒
        jedisPool = new JedisPool(jedisPoolConfig, &quot;192.168.9.128&quot;, 6379, 1000, &quot;密码&quot;);
    }

    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @BeforeEach
    void setUp() {
//        jedis = new Jedis(&quot;192.168.9.128&quot;, 6379);
        jedis = JedisConnectionFactory.getJedis();
        jedis.auth(&quot;Zzy20020913.&quot;);
        jedis.select(0);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Redis的Java客户端&lt;/h3&gt;
&lt;h4&gt;认识SpringDataRedis&lt;/h4&gt;
&lt;p&gt;SpringData是Spring中数据操作的模块，包含对各种数据库的集成，其中对Redis的集成模块就叫做SpringDataRedis，官网地址：https://spring.io/projects/spring-data-redis&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供了对不同Redis客户端的整合（Lettuce和Jedis）&lt;/li&gt;
&lt;li&gt;提供了RedisTemplate统一API来操作Redis&lt;/li&gt;
&lt;li&gt;支持Redis的发布订阅模型&lt;/li&gt;
&lt;li&gt;支持Redis哨兵和Redis集群&lt;/li&gt;
&lt;li&gt;支持基于Lettuce的响应式编程&lt;/li&gt;
&lt;li&gt;支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化（方便数据的存储和读取）&lt;/li&gt;
&lt;li&gt;支持基于Redis的JDKCollection实现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SpringDataRedis中提供了RedisTemplate工具类，其中封装了各种对Redis的操作。像redis一样，对不同数据类型做了分组，将不同数据类型的操作API封装到了不同的类型中：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;返回值类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redisTemplate.opsForValue()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ValueOperations&lt;/td&gt;
&lt;td&gt;操作String类型数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redisTemplate.opsForHash()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HashOperations&lt;/td&gt;
&lt;td&gt;操作Hash类型数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redisTemplate.opsForList()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ListOperations&lt;/td&gt;
&lt;td&gt;操作List类型数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redisTemplate.opsForSet()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SetOperations&lt;/td&gt;
&lt;td&gt;操作Set类型数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redisTemplate.opsForZSet()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ZSetOperations&lt;/td&gt;
&lt;td&gt;操作SortedSet类型数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redisTemplate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;通用的命令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;RedisTemplate快速入门&lt;/h4&gt;
&lt;p&gt;SpringBoot已经提供了对SpringDataRedis的支持，使用非常简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--Redis依赖--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!--连接池依赖--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.commons&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;commons-pool2&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;spring:
  redis:
    host: 192.168.150.101
    port: 6379
    password: 123321
    lettuce:
      pool:
        max-active: 8  # 最大连接
        max-idle: 8    # 最大空闲连接
        min-idle: 0    # 最小空闲连接
        max-wait: 100  # 连接等待时间
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;注入RedisTemplate&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
private RedisTemplate redisTemplate;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
public class RedisTest {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void testString() {
        // 插入一条string类型数据
        redisTemplate.opsForValue().set(&quot;name&quot;, &quot;李四&quot;);
        // 读取一条string类型数据
        Object name = redisTemplate.opsForValue().get(&quot;name&quot;);
        System.out.println(&quot;name = &quot; + name);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;RedisTemplate的RedisSerializer&lt;/h4&gt;
&lt;p&gt;RedisTemplate可以接收任意Object作为值写入Redis，只不过写入前会把Object序列化为字节形式，默认是采用JDK
序列化，得到的结果是这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251124220452322.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可读性差&lt;/li&gt;
&lt;li&gt;内存占用较大&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建Redis配置文件，添加序列化器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class RedisConfiguration {
    @Bean
    @SuppressWarnings(&quot;all&quot;)
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {

        // 我们为了自己开发方便，一般直接使用 &amp;lt;String, Object&amp;gt;
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;String, Object&amp;gt;();
        template.setConnectionFactory(factory);

        // Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);

        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);

        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);

        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个Redis工具类，对RedisTemplate进行封装，像使用原生Redis指令那样在java中使用对应API&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Redis工具类
 */
@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    /****************** common start ****************/
    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time &amp;gt; 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings(&quot;unchecked&quot;)
    public void del(String... key) {
        if (key != null &amp;amp;&amp;amp; key.length &amp;gt; 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection&amp;lt;String&amp;gt;) CollectionUtils.arrayToList(key));
            }
        }
    }
    /****************** common end ****************/


    /****************** String start ****************/

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time &amp;gt; 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta &amp;lt; 0) {
            throw new RuntimeException(&quot;递增因子必须大于0&quot;);
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta &amp;lt; 0) {
            throw new RuntimeException(&quot;递减因子必须大于0&quot;);
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
    /****************** String end ****************/


    /****************** Map start ****************/

    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }
    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map&amp;lt;Object, Object&amp;gt; hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map&amp;lt;String, Object&amp;gt; map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map&amp;lt;String, Object&amp;gt; map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time &amp;gt; 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time &amp;gt; 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, long by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, long by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    /****************** Map end ****************/



    /****************** Set start ****************/

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set&amp;lt;Object&amp;gt; sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time &amp;gt; 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /****************** Set end ****************/

    /****************** List start ****************/

    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1代表所有值
     * @return
     */
    public List&amp;lt;Object&amp;gt; lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引 index&amp;gt;=0时， 0 表头，1 第二个元素，依次类推；index&amp;lt;0时，-1，表尾，-2倒数第二个元素，依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time &amp;gt; 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List&amp;lt;Object&amp;gt; value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List&amp;lt;Object&amp;gt; value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time &amp;gt; 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /****************** List end ****************/

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;StringRedisTemplate&lt;/h4&gt;
&lt;p&gt;尽管JSON的序列化方式可以满足我们的需求，但依然存在一些问题，如图：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testSer() {
        redisTemplate.opsForValue().set(&quot;user:36&quot;, new User(20, &quot;Jack&quot;));
        User user = (User) redisTemplate.opsForValue().get(&quot;user:36&quot;);
        System.out.println(&quot;user = &quot; + user);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251124223049400.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了在反序列化时知道对象的类型，JSON序列化器会将类的class类型写入json结果中，存入Redis，会带来额外的内存
开销。&lt;/p&gt;
&lt;p&gt;为了节省内存空间，我们并不会使用JSON序列化器来处理value，而是统一使用String序列化器，要求只能存储String
类型的key和value。当需要存储Java对象时，手动完成对象的序列化和反序列化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251124223327537.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring默认提供了一个StringRedisTemplate类，它的key和value的序列化方式默认就是String方式。省去了我们自定
义RedisTemplate的过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用StringRedisTemplate&lt;/li&gt;
&lt;li&gt;写入Redis时，手动把对象序列化为JSON&lt;/li&gt;
&lt;li&gt;读取Redis时，手动把读取到的JSON反序列化为对象&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;直接注入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Autowired
    private StringRedisTemplate stringRedisTemplate;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testStringTemplate() {
        User user = new User(22, &quot;Lucy&quot;);
        stringRedisTemplate.opsForValue().set(&quot;user:37&quot;, JSON.toJSONString(user));
        String resUser = stringRedisTemplate.opsForValue().get(&quot;user:37&quot;);
        User userObj = JSON.parseObject(resUser, User.class);
        System.out.println(&quot;userObj = &quot; + userObj);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时存储的内容没有之前的class信息，节约了存储空间
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20251124224450996.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;RedisTemplate操作Hash&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testHash() {
        redisTemplate.opsForHash().put(&quot;user:300&quot;, &quot;age&quot;, 20);
        redisTemplate.opsForHash().put(&quot;user:300&quot;, &quot;name&quot;, &quot;wangWu&quot;);
        Map&amp;lt;Object, Object&amp;gt; entries = redisTemplate.opsForHash().entries(&quot;user:300&quot;);
        System.out.println(&quot;entries = &quot; + entries);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info
参考&lt;/p&gt;
&lt;p&gt;https://catpaws.top/e0606bbf/&lt;/p&gt;
&lt;p&gt;https://cyborg2077.github.io/2022/10/21/RedisBasic/#List%E7%B1%BB%E5%9E%8B&lt;/p&gt;
&lt;p&gt;https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA?pwd=eh11#list/path=%2F&amp;amp;parentPath=%2Fsharelink3232509500-235828228909890&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>MySQL - 主从复制</title><link>https://zzyang.top/posts/mysql-masterslave/</link><guid isPermaLink="true">https://zzyang.top/posts/mysql-masterslave/</guid><pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;主从复制概述&lt;/h2&gt;
&lt;h3&gt;如何提升数据库并发能力&lt;/h3&gt;
&lt;p&gt;在实际工作中，我们常常将 &lt;code&gt;Redis&lt;/code&gt; 作为缓存与 &lt;code&gt;MySQL&lt;/code&gt; 配合来使用，当有请求的时候，首先会从缓存中进行查找，如果存在就直接取出。如果不存在再访问数据库，这样就&lt;code&gt;提升了读取的效率&lt;/code&gt;，也减少了对后端数据库的&lt;code&gt;访问压力&lt;/code&gt;。Redis 的缓存架构是&lt;code&gt;高并发架构&lt;/code&gt;中非常重要的一环。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_22-29-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此外，一般应用对数据库而言都是“&lt;code&gt;读多写少&lt;/code&gt;”，也就说对数据库读取数据的压力比较大，有一个思路就是采用数据库集群的方案，做&lt;code&gt;主从架构&lt;/code&gt;、进行&lt;code&gt;读写分离&lt;/code&gt;，这样同样可以提升数据库的并发处理能力。但并不是所有的应用都需要对数据库进行主从架构的设置，毕竟设置架构本身是有成本的。&lt;/p&gt;
&lt;p&gt;如果我们的目的在于提升数据库高并发访问的效率，那么首先考虑的是如何&lt;code&gt;优化SQL和索引&lt;/code&gt;，这种方式简单有效；其次才是采用&lt;code&gt;缓存的策略&lt;/code&gt;，比如使用Redis将热点数据保存在内存数据库中，提升读取的效率；最后才是对数据库采用&lt;code&gt;主从架构&lt;/code&gt;，进行读写分离。&lt;/p&gt;
&lt;p&gt;按照上面的方式进行优化，使用和维护的成本是由低到高的。&lt;/p&gt;
&lt;h3&gt;主从复制的作用&lt;/h3&gt;
&lt;p&gt;主从同步设计不仅可以提高数据库的吞吐量，还有以下3个方面的作用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第1个作用：读写分离&lt;/strong&gt;。我们可以通过主从复制的方式来&lt;code&gt;同步数据&lt;/code&gt;，然后通过读写分离提高数据库并发处理能力。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_22-40-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中一个是Master主库，负责写入数据，我们称之为：写库。&lt;/p&gt;
&lt;p&gt;其它都是Slave从库，负责读取数据，我们称之为：读库。&lt;/p&gt;
&lt;p&gt;当主库进行更新的时候，会自动将数据复制到从库中，而我们在客户端读取数据的时候，会从从库中进行读取。&lt;/p&gt;
&lt;p&gt;面对“&lt;code&gt;读多写少&lt;/code&gt;”的需求，采用读写分离的方式，可以实现&lt;code&gt;更高的并发访问&lt;/code&gt;。同时，我们还能对从服务器进行&lt;code&gt;负载均衡&lt;/code&gt;，让不同的读请求按照策略均匀地分发到不同的从服务器上，让&lt;code&gt;读取更加顺畅&lt;/code&gt;。读取顺畅的另一个原因，就是&lt;code&gt;减少了锁表&lt;/code&gt;的影响，比如我们让主库负责写，当主库出现写锁的时候，不会影响到从库进行 SELECT 的读取。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第2个作用就是数据备份&lt;/strong&gt;。我们通过主从复制将主库上的数据复制到了从库上，相当于是一种&lt;code&gt;热备份机制&lt;/code&gt;，也就是在主库正常运行的情况下进行的备份，不会影响到服务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第3个作用是具有高可用性&lt;/strong&gt;。数据备份实际上是一种冗余的机制，通过这种冗余的方式可以换取数据库的高可用性，也就是当服务器出现&lt;code&gt;故障&lt;/code&gt;或&lt;code&gt;宕机&lt;/code&gt;的情况下，可以&lt;code&gt;切换&lt;/code&gt;到从服务器上，保证服务的正常运行。&lt;/p&gt;
&lt;p&gt;关于高可用性的程度，我们可以用一个指标衡量，即正常可用时间/全年时间。比如要达到全年99.999%的时间都可用，就意味着系统在一年中的不可用时间不得超过&lt;code&gt;365*24*60*（1-99.999%）=5.256&lt;/code&gt;分钟（含系统崩溃的时间、日常维护操作导致的停机时间等），其他时间都需要保持可用的状态。&lt;/p&gt;
&lt;p&gt;实际上，更高的高可用性，意味着需要付出更高的成本代价。在现实中我们需要结合业务需求和成本来进行选择。&lt;/p&gt;
&lt;h2&gt;主从复制的原理&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Slave&lt;/code&gt; 会从 &lt;code&gt;Master&lt;/code&gt; 读取 &lt;code&gt;binlog&lt;/code&gt; 来进行数据同步。&lt;/p&gt;
&lt;h3&gt;原理剖析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;三个线程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实际上主从同步的原理就是基于 &lt;code&gt;binlog&lt;/code&gt; 进行数据同步的。在主从复制过程中，会基于 &lt;code&gt;3 个线程&lt;/code&gt;来操作，一个主库线程，两个从库线程。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_23-05-00.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;二进制日志转储线程&lt;/code&gt;（Binlog dump thread）是一个主库线程。当从库线程连接的时候，主库可以将二进制日志发送给从库，当主库读取事件（Event）的时候，会在 Binlog 上&lt;code&gt;加锁&lt;/code&gt;，读取完成之后，再将锁释放掉。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;从库 I/O 线程&lt;/code&gt;会连接到主库，向主库发送请求更新 Binlog。这时从库的 I/O 线程就可以读取到主库的二进制日志转储线程发送的 Binlog 更新部分，并且拷贝到本地的中继日志（Relay log）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;从库 SQL 线程&lt;/code&gt;会读取从库中的中继日志，并且执行日志中的事件，将从库中的数据与主库保持同步。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_23-05-48.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::warning
注意：
不是所有版本的 MySQL 都默认开启服务器的二进制日志。在进行主从同步的时候，我们需要先检查服务器是否已经开启了二进制日志。&lt;/p&gt;
&lt;p&gt;除非特殊指定，默认情况下从服务器会执行所有主服务器中保存的事件。也可以通过配置，使从服务器执行特定的事件。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复制三步骤&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;步骤1：&lt;code&gt;Master&lt;/code&gt; 将写操作记录到二进制日志（&lt;code&gt;binlog&lt;/code&gt;）。这些记录叫做&lt;strong&gt;二进制日志事件&lt;/strong&gt;(binary log events)；&lt;/li&gt;
&lt;li&gt;步骤2：&lt;code&gt;Slave&lt;/code&gt; 将 &lt;code&gt;Master&lt;/code&gt; 的 binary log events 拷贝到它的中继日志（&lt;code&gt;relay log&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;步骤3：&lt;code&gt;Slave&lt;/code&gt; 重做中继日志中的事件，将改变应用到自己的数据库中。MySQL复制是异步的且串行化的，而且重启后从&lt;code&gt;接入点&lt;/code&gt;开始复制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;复制的问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;复制的最大问题：&lt;code&gt;延时&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;复制的基本原则&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个 &lt;code&gt;Slave&lt;/code&gt; 只有一个 &lt;code&gt;Master&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每个 &lt;code&gt;Slave&lt;/code&gt; 只能有一个唯一的服务器 ID&lt;/li&gt;
&lt;li&gt;每个 &lt;code&gt;Master&lt;/code&gt; 可以有多个 &lt;code&gt;Slave&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;一主一从架构搭建&lt;/h2&gt;
&lt;p&gt;一台主机用于处理所有写请求，一台从机负责所有读请求，架构图如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-30_21-02-17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;准备工作&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;准备 2 台 CentOS 虚拟机&lt;/li&gt;
&lt;li&gt;每台虚拟机上需要安装好MySQL (可以是MySQL8.0 )&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;说明: 前面我们讲过如何克隆一台CentOS。大家可以在一台CentOS上安装好MySQL, 进而通过克隆的方式复制出1台包含MySQL的虚拟机。&lt;/p&gt;
&lt;p&gt;注意: 克隆的方式需要修改新克隆出来主机的: ① MAC地址 ② hostname ③ IP 地址 ④ UUID 。&lt;/p&gt;
&lt;p&gt;此外, 克隆的方式生成的虚拟机 (包含MySQL Server), 则克隆的虚拟机MySQL Server的UUID相同, 必须修改, 否则在有些场景会报错。比如:
&lt;code&gt;show slave status\G&lt;/code&gt; , 报如下的错误:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Last_IO_Error: Fatal error: The slave I/O thread stops because master and slave have equal
MySQL server UUIDs; these UUIDs must be different for replication to work.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改MySQL Server的UUID方式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /var/lib/mysql/auto.cnf

# 重启mysql服务
systemctl restart mysqld
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;克隆虚拟机&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在如下界面中，先不要启动centos虚拟机，先点击克隆按钮；（前提我们已有的centos虚拟机都已经安装好MySQL）
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-30_21-43-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生成新的mac地址&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-30_21-30-16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;修改主机名&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;开机后修改主机名称(这步不用改也可以)&lt;/p&gt;
&lt;p&gt;修改主机名可能不同linux版本不同,修改方法也不同。centos就是&lt;code&gt;vim /etc/hostname&lt;/code&gt; 命令来编辑主机名。&lt;/p&gt;
&lt;p&gt;需要重启。(&lt;code&gt;reboot&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;修改IP地址&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;此处需要注意的是：如果虚拟机使用的是动态ip分配，那么不需要更改ip，如果想改为静态ip，请修改：
&lt;code&gt;vim /etc/sysconfig/network-scripts/ifcfg-ens33&lt;/code&gt;
在此文件中修改UUID和IP地址即可。(IPADDR和UUID)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;修改UUID&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同样在修改IP地址的路径中修改即可，重启我们的网络&lt;/p&gt;
&lt;p&gt;&lt;code&gt;systemctl restart network&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;主机配置文件&lt;/h3&gt;
&lt;p&gt;建议mysql版本一致且后台以服务运行，主从所有配置项都配置在 [mysqld] 节点下，且都是小写字母。
具体参数配置如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必选&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 主服务器唯一id
server-id=1

# 启用二进制日志，指明路径。比如：自己的本地路径/log/mysqlbin
log-bin=atguigu-bin # 二进制日志文件名
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;可选&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;0（默认）表示读写（主机），1表示只读（从机）
read-only=0

# 设置日志文件保留时长，单位是秒
binlog_expire_logs_seconds=6000

# 控制单个二进制日志大小。此参数的最大和默认值值1GB
max_binlog_size=200M

# [可选]设置不要复制的数据库，表示针对某些库的修改操作就不要记录到binlog文件里
binlog-ignore-db=test
# [可选]设置需要复制的数据库,默认全部记录。比如:binlog-do-db=atguigu_master_slave
binlog-do-db=需要复制的主数据库名字
# [可选]设置binlog格式
binlog_format=STATEMENT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启后台mysql服务，是配置生效&lt;/p&gt;
&lt;p&gt;配置完成后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:wq # 保存并退出
systemctl restart mysqld # 重启mysql服务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
注意：&lt;/p&gt;
&lt;p&gt;先搭建完主从复制，再创建数据库。&lt;/p&gt;
&lt;p&gt;MySQL主从复制起始时，从机不继承主机数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;binlog-do-db=需要复制的主数据库名字
注意这里配置的数据库，先不要创建，等从机配置完成后再执行create语句进行创建
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1iq4y1u7vj?spm_id_from=333.788.player.switch&amp;amp;vd_source=da7c7c4a886275716b7ca33f532f1905&amp;amp;p=193&quot;&gt;binlog格式设置&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;从机配置文件&lt;/h3&gt;
&lt;p&gt;要求主从所有配置项都配置在 &lt;code&gt;my.cnf&lt;/code&gt; 的 &lt;code&gt;[mysqld]&lt;/code&gt; 栏位下，且都是小写字母。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必选&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 从服务器唯一ID
server-id=2
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;可选&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 启用中继日志
relay-log=mysql-relay
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启后台mysql服务，使配置生效
:::warning
主从机都关闭防火墙，否则会导致复制失败。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service iptables stop # centos6

systemctl stop firewalld.service # centos7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;主机：建立账户并授权&lt;/h3&gt;
&lt;p&gt;如果是mysql5.5或者5.7直接执行一条指令即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#在主机MySQL里执行授权主从复制的命令
GRANT REPLICATION SLAVE ON *.* TO &apos;slave1&apos;@&apos;从机器数据库IP&apos; IDENTIFIED BY &apos;abc123&apos;; #5.5,5.7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：如果使用的是MySQL8，需要如下的方式建立账户，并授权slave:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE USER &apos;slave1&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;

GRANT REPLICATION SLAVE ON *.* TO &apos;slave1&apos;@&apos;%&apos;;

#此语句必须执行。否则见下面。(这里的密码别写错了要与上面的一致)
ALTER USER &apos;slave1&apos;@&apos;%&apos; IDENTIFIED WITH mysql_native_password BY &apos;123456&apos;;

# 刷新权限
flush privileges;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
注意：在从机执行show slave status\G时报错：&lt;/p&gt;
&lt;p&gt;Last_IO_Error: error connecting to master &apos;slave1@192.168.1.150:3306&apos; - retry-time: 60 retries: 1 message:&lt;/p&gt;
&lt;p&gt;Authentication plugin &apos;caching_sha2_password&apos; reported error: Authentication requires secure connection.
:::
查询Master的状态，并记录下File和Position的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 获取主从复制的起点
show master status;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;记录下File和Position的值&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：执行完此步骤后不要再操作主服务器MySQL，防止主服务器状态值变化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;从机：配置需要复制的主机&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;步骤1：&lt;/strong&gt; 从机上复制主机的命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CHANGE MASTER TO
MASTER_HOST=&apos;主机的IP地址&apos;,
MASTER_USER=&apos;主机用户名&apos;,
MASTER_PASSWORD=&apos;主机用户名的密码&apos;,
MASTER_LOG_FILE=&apos;mysql-bin.具体数字&apos;,
MASTER_LOG_POS=具体值;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在mysql中执行：
CHANGE MASTER TO
MASTER_HOST=&apos;192.168.1.150&apos;,MASTER_USER=&apos;slave1&apos;,MASTER_PASSWORD=&apos;123456&apos;,MASTER_LOG_FILE=&apos;atguigu-bin.000007&apos;,MASTER_LOG_POS=154;

# MASTER_LOG_FILE和MASTER_LOG_FILE均为上面记录的值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-30_22-24-36.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip
如果之前做过主从的配置，而且从机还开着呢，那么执行该命令会报错，如果要重新配置的话一定要先进行stop
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#启动slave同步
START SLAVE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果报错：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-30_22-30-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以执行如下操作，删除之前的relay_log信息。然后重新执行 CHANGE MASTER TO... 语句即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reset slave; # 删除SLAVE数据库的relaylog日志文件，并重新启用新的relaylog文件

# 如果没有报错就不用执行这条命令了。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着查看同步状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show slave status\G;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-30_22-33-19.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;上面两个参数都是Yes，则说明主从配置成功！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时就搭建完成了，我们可以在主机执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create database xxx;

# 这里配置的数据库名：binlog-do-db=需要复制的主数据库名字
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显式如下的情况，就是不正确的。可能错误的原因有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;网络不通&lt;/li&gt;
&lt;li&gt;账户密码错误&lt;/li&gt;
&lt;li&gt;防火墙&lt;/li&gt;
&lt;li&gt;mysql配置文件问题&lt;/li&gt;
&lt;li&gt;连接服务器时语法&lt;/li&gt;
&lt;li&gt;主服务器mysql权限&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-30_22-34-37.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;建完数据库后我们在主服务器上插入数据，然后在从机上查看是否同步。&lt;/p&gt;
&lt;h3&gt;停止主从同步&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;停止主从同步命令&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 从机上执行
stop slave;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如何重新配置主从&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;start slave;

show slave status\G;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果停止从服务器复制功能，再使用。需要重新配置主从。否则会报错如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ERROR 3021 (HY000): This operation cannot be performed with a running slave io thread; run STOP SLAVE IO THREAD FOR CHANNEL &apos;&apos; first.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新配置主从，需要在从机上执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stop slave;
reset master; #删除Master中所有的binglog文件，并将日志索引文件清空，重新开始所有新的日志文件(慎用)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;同步数据一致性问题&lt;/h2&gt;
&lt;p&gt;主从同步的要求:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读库和写库的数据一致(最终一致);&lt;/li&gt;
&lt;li&gt;写数据必须写到写库;&lt;/li&gt;
&lt;li&gt;读数据必须到读库(不一定);&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;主从延迟问题&lt;/h3&gt;
&lt;p&gt;进行主从同步的内容是二进制日志，它是一个文件，在进行网络传输的过程中就一定会存在主从延迟（比如500ms），这样就可能造成用户在从库上读取的数据不是最新的数据，也就是主从同步中的数据不一致性问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;举例：导致主从延迟的时间点主要包括以下三个：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;主库A执行完成一个事务，写入binlog，我们把这个时刻记为T1；&lt;/li&gt;
&lt;li&gt;之后传给从库B，我们把从库B接收完这个binlog的时刻记为T2；&lt;/li&gt;
&lt;li&gt;从库B执行完成这个事务，我们把这个时刻记为T3。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;主从延迟问题原因&lt;/h3&gt;
&lt;p&gt;在网络正常的时候，日志从主库传给从库所需的时间是很短的，即T2-T1的值是非常小的。即，网络正常情况下，主备延迟的主要来源是备库接收完binlog和执行完这个事务之间的时间差。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主备延迟最直接的表现是，从库消费中继日志（relay log）的速度，比主库生产binlog的速度要慢。&lt;/strong&gt; 造成原因：&lt;/p&gt;
&lt;p&gt;1、从库的机器性能比主库要差&lt;/p&gt;
&lt;p&gt;2、从库的压力大&lt;/p&gt;
&lt;p&gt;3、大事务的执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例1：&lt;/strong&gt; 一次性用delete语句删除太多数据&lt;/p&gt;
&lt;p&gt;结论：后续再删除数据的时候，要控制每个事务删除的数据量，分成多次删除。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例2：&lt;/strong&gt; 一次性用insert...select插入太多数据&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例3：&lt;/strong&gt; 大表DDL&lt;/p&gt;
&lt;p&gt;比如在主库对一张500W的表添加一个字段耗费了10分钟，那么从节点上也会耗费10分钟。&lt;/p&gt;
&lt;h3&gt;如何减少主从延迟&lt;/h3&gt;
&lt;p&gt;若想要减少主从延迟的时间，可以采取下面的办法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;降低多线程大事务并发的概率，优化业务逻辑&lt;/li&gt;
&lt;li&gt;优化SQL，避免慢SQL，&lt;code&gt;减少批量操作&lt;/code&gt;，建议写脚本以update-sleep这样的形式完成。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;提高从库机器的配置&lt;/code&gt;，减少主库写binlog和从库读binlog的效率差。&lt;/li&gt;
&lt;li&gt;尽量采用&lt;code&gt;短的链路&lt;/code&gt;，也就是主库和从库服务器的距离尽量要短，提升端口带宽，减少binlog传输的网络延时。&lt;/li&gt;
&lt;li&gt;实时性要求的业务读强制走主库，从库只做备份，备份。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;如何解决一致性问题&lt;/h3&gt;
&lt;p&gt;如果操作的数据存储在同一个数据库中，那么对数据进行更新的时候，可以对记录加写锁，这样在读取的时候就不会发生数据不一致的情况。但这时从库的作用就是&lt;code&gt;备份&lt;/code&gt;，并没有起到&lt;code&gt;读写分离&lt;/code&gt;，分担主库&lt;code&gt;读压力&lt;/code&gt;的作用。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-11-03_22-19-26.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;读写分离情况下，解决主从同步中数据不一致的问题， 就是解决主从之间 &lt;code&gt;数据复制方式&lt;/code&gt; 的问题，如果按照数据一致性 &lt;code&gt;从弱到强&lt;/code&gt; 来进行划分，有以下 3 种复制方式。&lt;/p&gt;
&lt;h4&gt;异步复制&lt;/h4&gt;
&lt;p&gt;异步模式就是客户端提交 COMMIT 之后不需要等从库返回任何结果，而是直接将结果返回给客户端，这样做的好处是不会影响主库写的效率，但可能会存在主库宕机，而Binlog还没有同步到从库的情况，也就是此时的主库和从库数据不一致。这时候从从库中选择一个作为新主，那么新主则可能缺少原来主服务器中已提交的事务。所以，这种复制模式下的数据一致性是最弱的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-11-03_22-23-00.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;半同步复制&lt;/h4&gt;
&lt;p&gt;MySQL5.5版本之后开始支持半同步复制的方式。原理是在客户端提交 COMMIT 之后不直接将结果返回给客户端，而是等待至少有一个从库接收到了 Binlog，并且写入到中继日志中，再返回给客户端。&lt;/p&gt;
&lt;p&gt;这样做的好处就是提高了数据的一致性，当然相比于异步复制来说，至少多增加了一个网络连接的延迟，降低了主库写的效率。&lt;/p&gt;
&lt;p&gt;在 MySQL5.7 版本中还增加了一个 &lt;code&gt;rpl_semi_sync_master_wait_for_slave_count&lt;/code&gt; 参数，可以对应答的从库数量进行设置，默认为 &lt;code&gt;1&lt;/code&gt;，也就是说只要有 1 个从库进行了响应，就可以返回给客户端。如果将这个参数调大，可以提升数据一致性的强度，但也会增加主库等待从库响应的时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-11-03_22-33-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;组复制&lt;/h4&gt;
&lt;p&gt;异步复制和半同步复制都无法最终保证数据的一致性问题，半同步复制是通过判断从库响应的个数来决定是否返回给客户端，虽然数据一致性相比于异步复制有提升，但仍然无法满足对数据一致性要求高的场景，比如金融领域。MGR 很好地弥补了这两种复制模式的不足。&lt;/p&gt;
&lt;p&gt;组复制技术，简称 MGR（MySQL Group Replication）。是 MySQL 在 5.7.17 版本中推出的一种新的数据复制技术，这种复制技术是基于 Paxos 协议的状态机复制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MGR 是如何工作的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先我们将多个节点共同组成一个复制组，在 &lt;strong&gt;执行读写（RW）事务&lt;/strong&gt; 的时候，需要通过一致性协议层 （Consensus 层）的同意，也就是读写事务想要进行提交，必须要经过组里“大多数人”（对应 Node 节 点）的同意，大多数指的是同意的节点数量需要大于 （N/2+1），这样才可以进行提交，而不是原发起方一个说了算。而针对 &lt;strong&gt;只读（RO）事务&lt;/strong&gt; 则不需要经过组内同意，直接 COMMIT 即可。&lt;/p&gt;
&lt;p&gt;在一个复制组内有多个节点组成，它们各自维护了自己的数据副本，并且在一致性协议层实现了原子消 息和全局有序消息，从而保证组内数据的一致性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-11-03_22-45-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;MGR 将 MySQL 带入了数据强一致性的时代，是一个划时代的创新，其中一个重要的原因就是 MGR 是基于 Paxos 协议的。Paxos 算法是由 2013 年的图灵奖获得者 Leslie Lamport 于 1990 年提出的，有关这个算法的决策机制可以搜一下。事实上，Paxos 算法提出来之后就作为&lt;code&gt;分布式一致性算法&lt;/code&gt;被广泛应用，比如 Apache 的 ZooKeeper 也是基于 Paxos 实现的。&lt;/p&gt;
</content:encoded></item><item><title>MySQL - 数据库备份与恢复</title><link>https://zzyang.top/posts/mysql-backup/</link><guid isPermaLink="true">https://zzyang.top/posts/mysql-backup/</guid><pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在任何数据库环境中，总会有&lt;code&gt;不确定的意外&lt;/code&gt;情况发生，比如例外的停电、计算机系统中的各种软硬件故障、人为破坏、管理员误操作等是不可避免的，这些情况可能会导致&lt;code&gt;数据的丢失&lt;/code&gt;、&lt;code&gt;服务器瘫痪&lt;/code&gt;等严重的后果。存在多个服务器时，会出现主从服务器之间的&lt;code&gt;数据同步问题&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为了有效防止数据丢失，并将损失降到最低，应&lt;code&gt;定期&lt;/code&gt;对MySQL数据库服务器做&lt;code&gt;备份&lt;/code&gt;。如果数据库中的数据丢失或者出现错误，可以使用备份的数据&lt;code&gt;进行恢复&lt;/code&gt;。主从服务器之间的数据同步问题可以通过复制功能实现。&lt;/p&gt;
&lt;h2&gt;物理备份与逻辑备份&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;物理备份：&lt;/strong&gt; 备份数据文件，转储数据库物理文件到某一目录。物理备份恢复速度比较快，但占用空间比较大，MySQL中可以用 &lt;code&gt;xtrabackup&lt;/code&gt; 工具来进行物理备份。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;逻辑备份：&lt;/strong&gt; 对数据库对象利用工具进行导出工作，汇总入备份文件内。逻辑备份恢复速度慢，但占用空间小，更灵活。MySQL 中常用的逻辑备份工具为 &lt;code&gt;mysqldump&lt;/code&gt; 。逻辑备份就是 &lt;code&gt;备份sql语句&lt;/code&gt; ，在恢复的 时候执行备份的sql语句实现数据库数据的重现。&lt;/p&gt;
&lt;h2&gt;mysqldump实现逻辑备份&lt;/h2&gt;
&lt;p&gt;mysqldump是MySQL提供的一个非常有用的数据库备份工具。&lt;/p&gt;
&lt;h3&gt;备份一个数据库&lt;/h3&gt;
&lt;p&gt;mysqldump命令执行时，可以将数据库备份成一个&lt;code&gt;文本文件&lt;/code&gt;，该文件中实际上包含多个&lt;code&gt;CREATE&lt;/code&gt;和&lt;code&gt;INSERT&lt;/code&gt;语句，使用这些语句可以重新创建表和插入数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查出需要备份的表的结构，在文本文件中生成一个CREATE语句&lt;/li&gt;
&lt;li&gt;将表中的所有记录转换成一条INSERT语句。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump –u 用户名称 –h 主机名称 –p密码 待备份的数据库名称[tbname, [tbname...]]&amp;gt; 备份文件名称.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明： 备份的文件并非一定要求后缀名为.sql，例如后缀名为.txt的文件也是可以的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;举例：使用root用户备份atguigu数据库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 若当前已经连接上主机了，即可省略-h参数
mysqldump -uroot -p atguigu&amp;gt;atguigu.sql #备份文件存储在当前目录下
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p atguigudb1 &amp;gt; /var/lib/mysql/atguigu.sql #备份文件存储在指定目录下
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;备份文件剖析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- MySQL dump 10.13 Distrib 8.0.26, for Linux (x86_64)
--
-- Host: localhost Database: atguigu
-- ------------------------------------------------------
-- Server version 8.0.26

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE=&apos;+00:00&apos; */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=&apos;NO_AUTO_VALUE_ON_ZERO&apos; */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Current Database: `atguigu`
--

CREATE DATABASE /*!32312 IF NOT EXISTS*/ `atguigu` /*!40100 DEFAULT CHARACTER SET
utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION=&apos;N&apos; */;

USE `atguigu`;

--
-- Table structure for table `student`
--

DROP TABLE IF EXISTS `student`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `student` (
`studentno` int NOT NULL,
`name` varchar(20) DEFAULT NULL,
`class` varchar(20) DEFAULT NULL,
PRIMARY KEY (`studentno`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
/*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO `student` VALUES (1,&apos;张三_back&apos;,&apos;一班&apos;),(3,&apos;李四&apos;,&apos;一班&apos;),(8,&apos;王五&apos;,&apos;二班&apos;),
(15,&apos;赵六&apos;,&apos;二班&apos;),(20,&apos;钱七&apos;,&apos;&amp;gt;三班&apos;),(22,&apos;zhang3_update&apos;,&apos;1ban&apos;),(24,&apos;wang5&apos;,&apos;2ban&apos;);
/*!40000 ALTER TABLE `student` ENABLE KEYS */;
UNLOCK TABLES;
        .
        .
        .
        .
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2022-01-07 9:58:23
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;备份全部数据库&lt;/h3&gt;
&lt;p&gt;若想用mysqldump备份整个实例，可以使用 --all-databases 或 -A 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p --all-databases &amp;gt; all_database.sql
mysqldump -uroot -p -A &amp;gt; all_database.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;备份部分数据库&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;--databases&lt;/code&gt; 或 &lt;code&gt;-B&lt;/code&gt; 参数了，该参数后面跟数据库名称，多个数据库间用空格隔开。如果指定 databases参数，备份文件中会存在创建数据库的语句，如果不指定参数，则不存在。语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump –u user –h host –p --databases [数据库的名称1 [数据库的名称2...]] &amp;gt; 备份文件名称.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p --databases atguigu atguigu12 &amp;gt; part_database.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p -B atguigu atguigu12 &amp;gt; part_database.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;备份部分表&lt;/h3&gt;
&lt;p&gt;比如，在表变更前做个备份。语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump –u user –h host –p 数据库的名称 [表名1 [表名2...]] &amp;gt; 备份文件名称.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;举例：备份atguigu数据库下的book表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p atguigu book &amp;gt; book_backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;book.sql文件内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p atguigu book&amp;gt; book.sql^C
[root@node1 ~]# ls
kk kubekey kubekey-v1.1.1-linux-amd64.tar.gz README.md test1.sql two_database.sql
[root@node1 ~]# mysqldump -uroot -p atguigu book&amp;gt; book.sql
Enter password:
[root@node1 ~]# ls
book.sql kk kubekey kubekey-v1.1.1-linux-amd64.tar.gz README.md test1.sql
two_database.sql
[root@node1 ~]# vi book.sql
-- MySQL dump 10.13 Distrib 8.0.26, for Linux (x86_64)
--
-- Host: localhost Database: atguigu
-- ------------------------------------------------------
-- Server version 8.0.26

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE=&apos;+00:00&apos; */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=&apos;NO_AUTO_VALUE_ON_ZERO&apos; */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `book`
--

DROP TABLE IF EXISTS `book`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `book` (
`bookid` int unsigned NOT NULL AUTO_INCREMENT,
`card` int unsigned NOT NULL,
`test` varchar(255) COLLATE utf8_bin DEFAULT NULL,
PRIMARY KEY (`bookid`),
KEY `Y` (`card`)
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `book`
--

LOCK TABLES `book` WRITE;
/*!40000 ALTER TABLE `book` DISABLE KEYS */;
INSERT INTO `book` VALUES (1,9,NULL),(2,10,NULL),(3,4,NULL),(4,8,NULL),(5,7,NULL),
(6,10,NULL),(7,11,NULL),(8,3,NULL),(9,1,NULL),(10,17,NULL),(11,19,NULL),(12,4,NULL),
(13,1,NULL),(14,14,NULL),(15,5,NULL),(16,5,NULL),(17,8,NULL),(18,3,NULL),(19,12,NULL),
(20,11,NULL),(21,9,NULL),(22,20,NULL),(23,13,NULL),(24,3,NULL),(25,18,NULL),
(26,20,NULL),(27,5,NULL),(28,6,NULL),(29,15,NULL),(30,15,NULL),(31,12,NULL),
(32,11,NULL),(33,20,NULL),(34,5,NULL),(35,4,NULL),(36,6,NULL),(37,17,NULL),
(38,5,NULL),(39,16,NULL),(40,6,NULL),(41,18,NULL),(42,12,NULL),(43,6,NULL),
(44,12,NULL),(45,2,NULL),(46,12,NULL),(47,15,NULL),(48,17,NULL),(49,2,NULL),
(50,16,NULL),(51,13,NULL),(52,17,NULL),(53,7,NULL),(54,2,NULL),(55,9,NULL),
(56,1,NULL),(57,14,NULL),(58,7,NULL),(59,15,NULL),(60,12,NULL),(61,13,NULL),
(62,8,NULL),(63,2,NULL),(64,6,NULL),(65,2,NULL),(66,12,NULL),(67,12,NULL),(68,4,NULL),
(69,5,NULL),(70,10,NULL),(71,16,NULL),(72,8,NULL),(73,14,NULL),(74,5,NULL),
(75,4,NULL),(76,3,NULL),(77,2,NULL),(78,2,NULL),(79,2,NULL),(80,3,NULL),(81,8,NULL),
(82,14,NULL),(83,5,NULL),(84,4,NULL),(85,2,NULL),(86,20,NULL),(87,12,NULL),
(88,1,NULL),(89,8,NULL),(90,18,NULL),(91,3,NULL),(92,3,NULL),(93,6,NULL),(94,1,NULL),
(95,4,NULL),(96,17,NULL),(97,15,NULL),(98,1,NULL),(99,20,NULL),(100,15,NULL);
/*!40000 ALTER TABLE `book` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，book文件和备份的库文件类似。不同的是，book文件只包含book表的DROP、CREATE和 INSERT语句。&lt;/p&gt;
&lt;p&gt;备份多张表使用下面的命令，比如备份book和account表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#备份多张表
mysqldump -uroot -p atguigu book account &amp;gt; 2_tables_bak.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;备份单表的部分数据&lt;/h3&gt;
&lt;p&gt;有些时候一张表的数据量很大，我们只需要部分数据。这时就可以使用 &lt;code&gt;--where&lt;/code&gt; 选项了。where后面附带需要满足的条件。&lt;/p&gt;
&lt;p&gt;举例：备份student表中id小于10的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p atguigu student --where=&quot;id &amp;lt; 10 &quot; &amp;gt; student_part_id10_bak.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内容如下所示，insert语句只有id小于10的部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOCK TABLES `student` WRITE;
/*!40000 ALTER TABLE `student` DISABLE KEYS */;
INSERT INTO `student` VALUES (1,100002,&apos;JugxTY&apos;,157,280),(2,100003,&apos;QyUcCJ&apos;,251,277),
(3,100004,&apos;lATUPp&apos;,80,404),(4,100005,&apos;BmFsXI&apos;,240,171),(5,100006,&apos;mkpSwJ&apos;,388,476),
(6,100007,&apos;ujMgwN&apos;,259,124),(7,100008,&apos;HBJTqX&apos;,429,168),(8,100009,&apos;dvQSQA&apos;,61,504),
(9,100010,&apos;HljpVJ&apos;,234,185);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;排除某些表的备份&lt;/h3&gt;
&lt;p&gt;如果我们想备份某个库，但是某些表数据量很大或者与业务关联不大，这个时候可以考虑排除掉这些表，同样的，选项 &lt;code&gt;--ignore-table&lt;/code&gt; 可以完成这个功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p atguigu --ignore-table=atguigu.student &amp;gt; no_stu_bak.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过如下指定判定文件中没有student表结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep &quot;student&quot; no_stu_bak.sql
# 我们发现并没有该表结构，因为我们使用了 --ignore-table 选项排除掉了。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;只备份结构或只备份数据&lt;/h3&gt;
&lt;p&gt;只备份结构的话可以使用 &lt;code&gt;--no-data&lt;/code&gt; 简写为 &lt;code&gt;-d&lt;/code&gt; 选项；只备份数据可以使用 &lt;code&gt;--no-create-info&lt;/code&gt; 简写为 &lt;code&gt;-t&lt;/code&gt;选项。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只备份结构&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p atguigu --no-data &amp;gt; atguigu_no_data_bak.sql
#使用grep命令，没有找到insert相关语句，表示没有数据备份。
[root@node1 ~]# grep &quot;INSERT&quot; atguigu_no_data_bak.sql
[root@node1 ~]#
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;只备份数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p atguigu --no-create-info &amp;gt; atguigu_no_create_info_bak.sql
#使用grep命令，没有找到create相关语句，表示没有数据结构。
[root@node1 ~]# grep &quot;CREATE&quot; atguigu_no_create_info_bak.sql
[root@node1 ~]#
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;备份中包含存储过程、函数、事件&lt;/h3&gt;
&lt;p&gt;mysqldump备份默认是不包含存储过程，自定义函数及事件的。可以使用 &lt;code&gt;--routines&lt;/code&gt; 或 &lt;code&gt;-R&lt;/code&gt; 选项来备份存储过程及函数，使用 &lt;code&gt;--events&lt;/code&gt; 或 &lt;code&gt;-E&lt;/code&gt; 参数来备份事件。&lt;/p&gt;
&lt;p&gt;举例：备份整个atguigu库，包含存储过程及事件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用下面的SQL可以查看当前库有哪些存储过程或者函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SELECT SPECIFIC_NAME,ROUTINE_TYPE ,ROUTINE_SCHEMA FROM
information_schema.Routines WHERE ROUTINE_SCHEMA=&quot;atguigu&quot;;
+---------------+--------------+----------------+
| SPECIFIC_NAME | ROUTINE_TYPE | ROUTINE_SCHEMA |
+---------------+--------------+----------------+
| rand_num      | FUNCTION     | atguigu        |
| rand_string   | FUNCTION     | atguigu        |
| BatchInsert   | PROCEDURE    | atguigu        |
| insert_class  | PROCEDURE    | atguigu        |
| insert_order  | PROCEDURE    | atguigu        |
| insert_stu    | PROCEDURE    | atguigu        |
| insert_user   | PROCEDURE    | atguigu        |
| ts_insert     | PROCEDURE    | atguigu        |
+---------------+--------------+----------------+
9 rows in set (0.02 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面备份atguigu库的数据，函数以及存储过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p -R -E --databases atguigu &amp;gt; fun_atguigu_bak.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询备份文件中是否存在函数，如下所示，可以看到确实包含了函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep -C 5 &quot;rand_num&quot; fun_atguigu_bak.sql
--
--
-- Dumping routines for database &apos;atguigu&apos;
--
/*!50003 DROP FUNCTION IF EXISTS `rand_num` */;
/*!50003 SET @saved_cs_client = @@character_set_client */ ;
/*!50003 SET @saved_cs_results = @@character_set_results */ ;
/*!50003 SET @saved_col_connection = @@collation_connection */ ;
/*!50003 SET character_set_client = utf8mb3 */ ;
/*!50003 SET character_set_results = utf8mb3 */ ;
/*!50003 SET collation_connection = utf8_general_ci */ ;
/*!50003 SET @saved_sql_mode = @@sql_mode */ ;
/*!50003 SET sql_mode =
&apos;ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISIO
N_BY_ZERO,NO_ENGINE_SUBSTITUTION&apos; */ ;
DELIMITER ;;
CREATE DEFINER=`root`@`%` FUNCTION `rand_num`(from_num BIGINT ,to_num BIGINT) RETURNS
bigint
BEGIN
DECLARE i BIGINT DEFAULT 0;
SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ;
RETURN i;
END ;;
--
BEGIN
DECLARE i INT DEFAULT 0;
    SET autocommit = 0;
    REPEAT
    SET i = i + 1;
    INSERT INTO class ( classname,address,monitor ) VALUES
    (rand_string(8),rand_string(10),rand_num());
    UNTIL i = max_num
    END REPEAT;
    COMMIT;
END ;;
DELIMITER ;
--
BEGIN
DECLARE i INT DEFAULT 0;
    SET autocommit = 0; #设置手动提交事务
    REPEAT #循环
    SET i = i + 1; #赋值
    INSERT INTO order_test (order_id, trans_id ) VALUES
    (rand_num(1,7000000),rand_num(100000000000000000,700000000000000000));
    UNTIL i = max_num
    END REPEAT;
    COMMIT; #提交事务
END ;;
DELIMITER ;
--
BEGIN
DECLARE i INT DEFAULT 0;
    SET autocommit = 0; #设置手动提交事务
    REPEAT #循环
    SET i = i + 1; #赋值
    INSERT INTO student (stuno, name ,age ,classId ) VALUES
    ((START+i),rand_string(6),rand_num(),rand_num());
    UNTIL i = max_num
    END REPEAT;
    COMMIT; #提交事务
END ;;
DELIMITER ;
--
BEGIN
DECLARE i INT DEFAULT 0;
    SET autocommit = 0;
    REPEAT
    SET i = i + 1;
    INSERT INTO `user` ( name,age,sex ) VALUES (&quot;atguigu&quot;,rand_num(1,20),&quot;male&quot;);
    UNTIL i = max_num
    END REPEAT;
    COMMIT;
END ;;
DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;mysqldump常用选项&lt;/h3&gt;
&lt;p&gt;mysqldump其他常用选项如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--add-drop-database：在每个CREATE DATABASE语句前添加DROP DATABASE语句。

--add-drop-tables：在每个CREATE TABLE语句前添加DROP TABLE语句。

--add-locking：用LOCK TABLES和UNLOCK TABLES语句引用每个表转储。重载转储文件时插入得更快。

--all-database, -A：转储所有数据库中的所有表。与使用--database选项相同，在命令行中命名所有数据库。

--comment[=0|1]：如果设置为0，禁止转储文件中的其他信息，例如程序版本、服务器版本和主机。--skip-comments与--comments=0的结果相同。默认值为1，即包括额外信息。

--compact：产生少量输出。该选项禁用注释并启用--skip-add-drop-tables、--no-set-names、--skip-disable-keys和--skip-add-locking选项。

--compatible=name：产生与其他数据库系统或旧的MySQL服务器更兼容的输出，值可以为ansi、MySQL323、MySQL40、postgresql、oracle、mssql、db2、maxdb、no_key_options、no_table_options或者no_field_options。

--complete_insert, -c：使用包括列名的完整的INSERT语句。

--debug[=debug_options], -#[debug_options]：写调试日志。

--delete，-D：导入文本文件前清空表。

--default-character-set=charset：使用charsets默认字符集。如果没有指定，就使用utf8。

--delete--master-logs：在主复制服务器上，完成转储操作后删除二进制日志。该选项自动启用-master-data。

--extended-insert，-e：使用包括几个VALUES列表的多行INSERT语法。这样使得转储文件更小，重载文件时可以加速插入。

--flush-logs，-F：开始转储前刷新MySQL服务器日志文件。该选项要求RELOAD权限。

--force，-f：在表转储过程中，即使出现SQL错误也继续。

--lock-all-tables，-x：对所有数据库中的所有表加锁。在整体转储过程中通过全局锁定来实现。该选项自动关闭--single-transaction和--lock-tables。

--lock-tables，-l：开始转储前锁定所有表。用READ LOCAL锁定表以允许并行插入MyISAM表。对于事务表（例如InnoDB和BDB），--single-transaction是一个更好的选项，因为它根本不需要锁定表。

--no-create-db，-n：该选项禁用CREATE DATABASE /*!32312 IF NOT EXIST*/db_name语句，如果给出--database或--all-database选项，就包含到输出中。

--no-create-info，-t：只导出数据，而不添加CREATE TABLE语句。

--no-data，-d：不写表的任何行信息，只转储表的结构。

--opt：该选项是速记，它可以快速进行转储操作并产生一个能很快装入MySQL服务器的转储文件。该选项默认开启，但可以用--skip-opt禁用。

--password[=password]，-p[password]：当连接服务器时使用的密码。

-port=port_num，-P port_num：用于连接的TCP/IP端口号。

--protocol={TCP|SOCKET|PIPE|MEMORY}：使用的连接协议。

--replace，-r –replace和--ignore：控制替换或复制唯一键值已有记录的输入记录的处理。如果指定--replace，新行替换有相同的唯一键值的已有行；如果指定--ignore，复制已有的唯一键值的输入行被跳过。如果不指定这两个选项，当发现一个复制键值时会出现一个错误，并且忽视文本文件的剩余部分。

--silent，-s：沉默模式。只有出现错误时才输出。

--socket=path，-S path：当连接localhost时使用的套接字文件（为默认主机）。

--user=user_name，-u user_name：当连接服务器时MySQL使用的用户名。

--verbose，-v：冗长模式，打印出程序操作的详细信息。

--xml，-X：产生XML输出。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行帮助命令 &lt;code&gt;mysqldump --help&lt;/code&gt; ，可以获得特定版本的完整选项列表。&lt;/p&gt;
&lt;p&gt;:::tip
提示 如果运行mysqldump没有--quick或--opt选项，mysqldump在转储结果前将整个结果集装入内存。如果转储大数据库可能会出现问题，该选项默认启用，但可以用--skip-opt禁用。如果使用最新版本的mysqldump程序备份数据，并用于恢复到比较旧版本的MySQL服务器中，则不要使用--opt 或-e选项。
:::&lt;/p&gt;
&lt;h2&gt;mysql命令恢复数据&lt;/h2&gt;
&lt;p&gt;使用mysqldump命令将数据库中的数据备份成一个文本文件。需要恢复时，可以使用&lt;code&gt;mysql命令&lt;/code&gt;来恢复备份的数据。&lt;/p&gt;
&lt;p&gt;mysql命令可以执行备份文件中的&lt;code&gt;CREATE语句&lt;/code&gt;和&lt;code&gt;INSERT语句&lt;/code&gt;。通过CREATE语句来创建数据库和表。通过INSERT语句来插入备份的数据。&lt;/p&gt;
&lt;p&gt;基本语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 这个指令就相当于读取mysqldump生成的sql文件
mysql -uroot -p [dbname] &amp;lt; backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，dbname参数表示数据库名称。该参数是可选参数，可以指定数据库名，也可以不指定。指定数据库名时，表示还原该数据库下的表。此时需要确保MySQL服务器中已经创建了该名的数据库。不指定数据库名，表示还原文件中所有的数据库。此时sql文件中包含有CREATE DATABASE语句，不需要MySQL服务器中已存在的这些数据库。&lt;/p&gt;
&lt;h3&gt;单库备份中恢复单库&lt;/h3&gt;
&lt;p&gt;使用root用户，将之前练习中备份的atguigu.sql文件中的备份导入数据库中，命令如下：&lt;/p&gt;
&lt;p&gt;如果备份文件中包含了创建数据库的语句，则恢复的时候不需要指定数据库名称，如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 如果已经存在数据库了，使用该命令即可
mysql -uroot -p &amp;lt; atguigu.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则需要指定数据库名称，如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 如果还没有数据库，使用该命令
mysql -uroot -p atguigu4 &amp;lt; atguigu.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;全量备份恢复&lt;/h3&gt;
&lt;p&gt;如果我们现在有昨天的全量备份，现在想整个恢复，则可以这样操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql –u root –p &amp;lt; all.sql

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完后，MySQL数据库中就已经恢复了all.sql文件中的所有数据库。&lt;/p&gt;
&lt;p&gt;:::tip
补充:&lt;/p&gt;
&lt;p&gt;如果使用&lt;code&gt;--all-databases&lt;/code&gt;参数备份了所有的数据库，那么恢复时不需要指定数据库。对应的sql文件包含有
CREATE DATABASE语句，可通过该语句创建数据库。创建数据库后，可以执行sql文件中的USE语句选择数据库，再创建表并插入记录。
:::&lt;/p&gt;
&lt;h3&gt;从全量备份中恢复单库&lt;/h3&gt;
&lt;p&gt;可能有这样的需求，比如说我们只想恢复某一个库，但是我们有的是整个实例的备份，这个时候我们可以从全量备份中分离出单个库的备份。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 表示从all_database.sql文件中分离出atguigu库的备份，并保存到atguigu.sql文件中
sed -n &apos;/^-- Current Database: `atguigu`/,/^-- Current Database: `/p&apos; all_database.sql &amp;gt; atguigu.sql
# 分离完成后我们再导入atguigu.sql即可恢复单个库
mysql -uroot -p &amp;lt; atguigu.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果没有看到恢复的数据库或者数据，我们需要重新登录&lt;code&gt;mysql -uroot -p&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;从单库备份中恢复单表&lt;/h3&gt;
&lt;p&gt;这个需求还是比较常见的。比如说我们知道哪个表误操作了，那么就可以用单表恢复的方式来恢复。&lt;/p&gt;
&lt;p&gt;举例：我们有atguigu整库的备份，但是由于class表误操作，需要单独恢复出这张表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 把atguigu.sql数据库文件中的class表的CREATE语句(表结构)分离出来，保存到class_structure.sql文件中
cat atguigu.sql | sed -e &apos;/./{H;$!d;}&apos; -e &apos;x;/CREATE TABLE `class`/!d;q&apos; &amp;gt; class_structure.sql
# 把atguigu.sql数据库文件中的class表的insert语句(表数据)分离出来，保存到class_data.sql文件中
cat atguigu.sql | grep --ignore-case &apos;insert into `class`&apos; &amp;gt; class_data.sql
#用shell语法分离出创建表的语句及插入数据的语句后 再依次导出即可完成恢复

use atguigu;
mysql&amp;gt; source class_structure.sql;
# 或
mysql&amp;gt; source /var/lib/mysql/backup/class_structure.sql
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql&amp;gt; source class_data.sql;
Query OK, 1 row affected (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;物理备份：直接复制整个数据库&lt;/h2&gt;
&lt;p&gt;直接将MySQL中的数据库文件复制出来。这种方法最简单，速度也最快。MySQL的数据库目录位置不一 定相同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在Windows平台下，MySQL 8.0存放数据库的目录通常默认为 “&lt;code&gt;C:\ProgramData\MySQL\MySQL Server 8.0\Data&lt;/code&gt;”或者其他用户自定义目录；&lt;/li&gt;
&lt;li&gt;在Linux平台下，数据库目录位置通常为&lt;code&gt;/var/lib/mysql/&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在MAC OSX平台下，数据库目录位置通常为“&lt;code&gt;/usr/local/mysql/data&lt;/code&gt;”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但为了保证备份的一致性。需要保证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方式1：备份前，将服务器停止。&lt;/li&gt;
&lt;li&gt;方式2：备份前，对相关表执行 &lt;code&gt;FLUSH TABLES WITH READ LOCK&lt;/code&gt; 操作。这样当复制数据库目录中 的文件时，允许其他客户继续查询表。同时，FLUSH TABLES语句来确保开始备份前将所有激活的索 引页写入硬盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种方式方便、快速，但不是最好的备份方法，因为实际情况可能 不允许停止MySQL服务器 或者 锁住表 ，而且这种方法 &lt;strong&gt;对InnoDB存储引擎的表不适用&lt;/strong&gt;。对于MyISAM存储引擎的表，这样备份和还原很方便，但是还原时最好是相同版本的MySQL数据库，否则可能会存在文件类型不同的情况。&lt;/p&gt;
&lt;p&gt;注意，物理备份完毕后，执行 UNLOCK TABLES 来结算其他客户对表的修改行为。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明： 在MySQL版本号中，第一个数字表示主版本号，主版本号相同的MySQL数据库文件格式相同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此外，还可以考虑使用相关工具实现备份。比如， &lt;code&gt;MySQLhotcopy&lt;/code&gt; 工具。MySQLhotcopy是一个Perl脚本，它使用LOCK TABLES、FLUSH TABLES和cp或scp来快速备份数据库。它是备份数据库或单个表最快的途径，但它只能运行在数据库目录所在的机器上，并且只能备份MyISAM类型的表。多用于mysql5.5之前。&lt;/p&gt;
&lt;h2&gt;物理备份：直接复制到数据库目录&lt;/h2&gt;
&lt;p&gt;步骤:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;演示删除备份的数据库中指定表的数据&lt;/li&gt;
&lt;li&gt;将备份的数据库数据拷贝到数据目录下,并重启MySQL服务器&lt;/li&gt;
&lt;li&gt;查询相关表的数据是否恢复。需要使用下面的 chown 操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要求:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须确保备份数据的数据库和待恢复的数据库服务器的主版本号相同。&lt;/li&gt;
&lt;li&gt;因为只有MySQL数据库主版本号相同时,才能保证这两个MySQL数据库文件类型是相同的。&lt;/li&gt;
&lt;li&gt;这种方式对 MyISAM类型的表比较有效 ,&lt;em&gt;对于InnoDB类型的表则不可用&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;因为InnoDB表的表空间不能直接复制。&lt;/li&gt;
&lt;li&gt;在Linux操作系统下,复制到数据库目录后,一定要将数据库的用户和组变成mysql,命令如下:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;chown -R mysql.mysql /var/lib/mysql/dbname
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，两个mysql分别表示组和用户；“-R”参数可以改变文件夹下的所有子文件的用户和组；“dbname”参数表示数据库目录。&lt;/p&gt;
&lt;p&gt;:::info
提示 Linux操作系统下的权限设置非常严格。通常情况下，MySQL数据库只有root用户和mysql用户 组下的mysql用户才可以访问，因此将数据库目录复制到指定文件夹后，一定要使用chown命令将 文件夹的用户组变为mysql，将用户变为mysql。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;物理备份步骤&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; create database atguigudb5;
Query OK, 1 row affected (0.01 sec)

mysql&amp;gt; use atguigudb5;
Database changed
mysql&amp;gt; create table test1(id int)engine=myisam;
Query OK, 0 rows affected (0.01 sec)

mysql&amp;gt; insert into test1 values(1),(2),(3);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql&amp;gt; select * from test1;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
+----+
3 rows in set (0.00 sec)

mysql&amp;gt; flush tables with read lock;
Query OK, 0 rows affected (0.00 sec)

-- 此时不可以删除，防止备份之前表数据的不一致性
mysql&amp;gt; delte from test1;
ERROR 1223 (HY000): Can&apos;t execute the query because you have a conflicting read lock

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在mysql外部操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在mysql目录下 var/lib/mysql/ 进行复制
cp -r atguigudb5 ./backup/

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入mysql中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; unlock tables;
Query OK, 0 rows affected (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此备份就完成了；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;恢复数据&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 模拟数据损毁了
mysql&amp;gt; delete from test1;
Query OK, 3 rows affected (0.00 sec)

mysql&amp;gt; select * from test1;
Empty set (0.00 sec)

mysql&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入到mysql目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 模拟真实删除
rm -rf atguigudb5
# 从备份的文件中移动到数据库目录下
mv  ./backup/atguigudb5 ./

# 重启mysql服务器
systemctl restart mysqld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新进入mysql&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; select * from test1;
ERROR 2013 (HY000): Lost connection to MySQL server during query
No connection. Trying to reconnect...
Connection id:    8
Current database: atguigudb5

ERROR 1036 (HY000): Table &apos;test1&apos; is read only
mysql&amp;gt; quit
Bye
[root@atguigu05 ~]# mysql -uroot -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.25 MySQL Community Server - GPL

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type &apos;help;&apos; or &apos;\h&apos; for help. Type &apos;\c&apos; to clear the current input statement.

mysql&amp;gt; use atguigudb5;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

mysql&amp;gt; select * from test1;
ERROR 1036 (HY000): Table &apos;test1&apos; is read only # 这里是说权限是不够的


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在linux下执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 因为这个文件目录不是之前那个了，这是新的文件目录，这个目录必须要让mysql的用户有权限访问
chown -R mysql.mysql /var/lib/mysql/atguigudb5

# 此时再回到mysql中就能执行了
mysql&amp;gt; select * from test1;
+----+
| id |
+----+  
|  1 |
|  2 |
|  3 |
+----+
3 rows in set (0.01 sec)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;物理层面的恢复就完成了。&lt;/p&gt;
&lt;h2&gt;表的导出与导入&lt;/h2&gt;
&lt;h3&gt;表的导出&lt;/h3&gt;
&lt;h4&gt;使用select...into outfile导出文本文件&lt;/h4&gt;
&lt;p&gt;在MySQL中，可以使用SELECT…INTO OUTFILE语句将表的内容导出成一个文本文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例：&lt;/strong&gt; 使用SELECT…INTO OUTFILE将atguigu数据库中account表中的记录导出到文本文件。&lt;/p&gt;
&lt;p&gt;（1）选择数据库atguigu，并查询account表，执行结果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use atguigu;
select * from account;
mysql&amp;gt; select * from account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 90 |
| 2 | 李四 | 100 |
| 3 | 王五 | 0 |
+----+--------+---------+
3 rows in set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（2）mysql默认对导出的目录有权限限制，也就是说使用命令行进行导出的时候，需要指定目录进行操作。&lt;/p&gt;
&lt;p&gt;查询secure_file_priv值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SHOW GLOBAL VARIABLES LIKE &apos;%secure%&apos;;
+--------------------------+-----------------------+
| Variable_name            | Value                 |
+--------------------------+-----------------------+
| require_secure_transport | OFF                   |
| secure_file_priv         | /var/lib/mysql-files/ |
+--------------------------+-----------------------+
2 rows in set (0.02 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数&lt;code&gt;secure_file_priv&lt;/code&gt;的可选值和作用分别是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果设置为&lt;code&gt;empty&lt;/code&gt;，表示不限制文件生成的位置，这是不安全的设置;&lt;/li&gt;
&lt;li&gt;如果设置为一个表示路径的字符串，就要求生成的文件只能放在这个指定的目录，或者它的子目录;&lt;/li&gt;
&lt;li&gt;如果设置为&lt;code&gt;NULL&lt;/code&gt;，就表示禁止在这个MySQL实例上执行&lt;code&gt;select...into outfile&lt;/code&gt;操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（3）上面结果中显示，secure_file_priv变量的值为/var/lib/mysql-files/，导出目录设置为该目录，SQL语句如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM account INTO OUTFILE &quot;/var/lib/mysql-files/account.txt&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（4）查看 &lt;code&gt;/var/lib/mysql-files/account.txt&lt;/code&gt;文件。&lt;/p&gt;
&lt;p&gt;这里就是实实在在的数据，表结构是没有的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /var/lib/mysql-files/account.txt

1 张三 90
2 李四 100
3 王五 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用mysqldump命令导出文本文件&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;举例1：&lt;/strong&gt; 使用mysqldump命令将将atguigu数据库中account表中的记录导出到文本文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p -T &quot;/var/lib/mysql-files/&quot; atguigu account
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mysqldump命令执行完毕后，在指定的目录/var/lib/mysql-files/下生成了account.sql和account.txt文件。&lt;/p&gt;
&lt;p&gt;打开account.sql文件，其内容包含创建account表的CREATE语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@node1 mysql-files]# cat account.sql
-- MySQL dump 10.13 Distrib 8.0.26, for Linux (x86_64)
--
-- Host: localhost Database: atguigu
-- ------------------------------------------------------
-- Server version 8.0.26
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE=&apos;+00:00&apos; */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=&apos;&apos; */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `account`
--

DROP TABLE IF EXISTS `account`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `account` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`balance` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
/*!40101 SET character_set_client = @saved_cs_client */;

/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2022-01-07 23:19:27
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开account.txt文件，其内容只包含account表中的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@node1 mysql-files]# cat account.txt
1 张三 90
2 李四 100
3 王五 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;举例2：&lt;/strong&gt; 使用mysqldump将atguigu数据库中的account表导出到文本文件，使用FIELDS选项，要求字段之间使用逗号“，”间隔，所有字符类型字段值用双引号括起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p -T &quot;/var/lib/mysql-files/&quot; atguigu account --fields-terminated-by=&apos;,&apos; --fields-optionally-enclosed-by=&apos;\&quot;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语句mysqldump语句执行成功之后，指定目录下会出现两个文件account.sql和account.txt。&lt;/p&gt;
&lt;p&gt;打开account.sql文件，其内容包含创建account表的CREATE语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@node1 mysql-files]# cat account.sql
-- MySQL dump 10.13 Distrib 8.0.26, for Linux (x86_64)
--
-- Host: localhost Database: atguigu
-- ------------------------------------------------------
-- Server version 8.0.26
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE=&apos;+00:00&apos; */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=&apos;&apos; */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `account`
--
DROP TABLE IF EXISTS `account`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `account` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`balance` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2022-01-07 23:36:39
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开account.txt文件，其内容包含创建account表的数据。从文件中可以看出，字段之间用逗号隔开，字 符类型的值被双引号括起来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@node1 mysql-files]# cat account.txt
1,&quot;张三&quot;,90
2,&quot;李四&quot;,100
3,&quot;王五&quot;,0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用mysql命令导出文本文件&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;举例1：&lt;/strong&gt; 使用mysql语句导出atguigu数据中account表中的记录到文本文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -uroot -p --execute=&quot;SELECT * FROM account;&quot; atguigu&amp;gt; &quot;/var/lib/mysql-files/account.txt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开account.txt文件，其内容包含创建account表的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@node1 mysql-files]# cat account.txt
id name balance
1 张三 90
2 李四 100
3 王五 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;举例2：&lt;/strong&gt; 将atguigu数据库account表中的记录导出到文本文件，使用--veritcal参数将该条件记录分为多行显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -uroot -p --vertical --execute=&quot;SELECT * FROM account;&quot; atguigu &amp;gt; &quot;/var/lib/mysql-files/account_1.txt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开account_1.txt文件，其内容包含创建account表的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@node1 mysql-files]# cat account_1.txt
*************************** 1. row ***************************
id: 1
name: 张三
balance: 90
*************************** 2. row ***************************
id: 2
name: 李四
balance: 100
*************************** 3. row ***************************
id: 3
name: 王五
balance: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;举例3：&lt;/strong&gt; 将atguigu数据库account表中的记录导出到xml文件，使用--xml参数，具体语句如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -uroot -p --xml --execute=&quot;SELECT * FROM account;&quot; atguigu&amp;gt;&quot;/var/lib/mysql-files/account_3.xml&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[root@node1 mysql-files]# cat account_3.xml
&amp;lt;?xml version=&quot;1.0&quot;?&amp;gt;
&amp;lt;resultset statement=&quot;SELECT * FROM account&quot;
xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&amp;gt;
&amp;lt;row&amp;gt;
    &amp;lt;field name=&quot;id&quot;&amp;gt;1&amp;lt;/field&amp;gt;
    &amp;lt;field name=&quot;name&quot;&amp;gt;张三&amp;lt;/field&amp;gt;
    &amp;lt;field name=&quot;balance&quot;&amp;gt;90&amp;lt;/field&amp;gt;
&amp;lt;/row&amp;gt;
&amp;lt;row&amp;gt;
    &amp;lt;field name=&quot;id&quot;&amp;gt;2&amp;lt;/field&amp;gt;
    &amp;lt;field name=&quot;name&quot;&amp;gt;李四&amp;lt;/field&amp;gt;
    &amp;lt;field name=&quot;balance&quot;&amp;gt;100&amp;lt;/field&amp;gt;
&amp;lt;/row&amp;gt;
&amp;lt;row&amp;gt;
    &amp;lt;field name=&quot;id&quot;&amp;gt;3&amp;lt;/field&amp;gt;
    &amp;lt;field name=&quot;name&quot;&amp;gt;王五&amp;lt;/field&amp;gt;
    &amp;lt;field name=&quot;balance&quot;&amp;gt;0&amp;lt;/field&amp;gt;
&amp;lt;/row&amp;gt;
&amp;lt;/resultset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：如果要将表数据导出到html文件中，可以使用 --html 选项。然后可以使用浏览器打开。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;表的导入&lt;/h3&gt;
&lt;h4&gt;使用load data infile导入文本文件&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;举例1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用SELECT...INTO OUTFILE将atguigu数据库中account表的记录导出到文本文件&lt;/p&gt;
&lt;p&gt;在mysql中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM atguigu.account INTO OUTFILE &apos;/var/lib/mysql-files/account_0.txt&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除account表中的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE FROM atguigu.account;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从文本文件account.txt中恢复数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOAD DATA INFILE &apos;/var/lib/mysql-files/account_0.txt&apos; INTO TABLE atguigu.account;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询account表中的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; select * from account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
| 1 | 张三     | 90      |
| 2 | 李四     | 100     |
| 3 | 王五     | 0       |
+----+--------+---------+
3 rows in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;举例2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;选择数据库atguigu，使用SELECT…INTO OUTFILE将atguigu数据库account表中的记录导出到文本文件，使用FIELDS选项和LINES选项，要求字段之间使用逗号&quot;，&quot;间隔，所有字段值用双引号括起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM atguigu.account INTO OUTFILE &apos;/var/lib/mysql-files/account_1.txt&apos; FIELDS TERMINATED BY &apos;,&apos; ENCLOSED BY &apos;\&quot;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除account表中的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE FROM atguigu.account;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从/var/lib/mysql-files/account.txt中导入数据到account表中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOAD DATA INFILE &apos;/var/lib/mysql-files/account_1.txt&apos; INTO TABLE atguigu.account FIELDS TERMINATED BY &apos;,&apos; ENCLOSED BY &apos;\&quot;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询account表中的数据，具体SQL如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from account;
mysql&amp;gt; select * from account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
| 1 | 张三     | 90      |
| 2 | 李四     | 100     |
| 3 | 王五     | 0       |
+----+--------+---------+
3 rows in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用mysqlimport方式导入文本文件&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;举例：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;导出文件account.txt，字段之间使用逗号&quot;，&quot;间隔，字段值用双引号括起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM atguigu.account INTO OUTFILE &apos;/var/lib/mysql-files/account.txt&apos; FIELDS TERMINATED BY &apos;,&apos; ENCLOSED BY &apos;\&quot;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除account表中的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE FROM atguigu.account;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用mysqlimport命令将account.txt文件内容导入到数据库atguigu的account表中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在linux中执行
mysqlimport -uroot -p atguigu &apos;/var/lib/mysql-files/account.txt&apos; --fields-terminated-by=&apos;,&apos; --fields-optionally-enclosed-by=&apos;\&quot;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询account表中的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from account;
mysql&amp;gt; select * from account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
| 1 | 张三     | 90      |
| 2 | 李四     | 100     |
| 3 | 王五     | 0       |
+----+--------+---------+
3 rows in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;--host=host_name，-h host host_name：将数据导入给定主机上的MySQL服务器，默认主机是localhost。&lt;/li&gt;
&lt;li&gt;--ignore，-i：参见--replace选项的描述。&lt;/li&gt;
&lt;li&gt;--ignore-lines=n：忽视数据文件的前n行。&lt;/li&gt;
&lt;li&gt;--local，-L：从本地客户端读入输入文件。&lt;/li&gt;
&lt;li&gt;--lock-tables，-l：处理文本文件前锁定所有表，以便写入。这样可以确保所有表在服务器上保持同步。&lt;/li&gt;
&lt;li&gt;--password[=password]，-p[password]：当连接服务器时使用的密码。如果使用短选项形式（-p），选项和密码之间不能有空格。如果在命令行中--password或-p选项后面没有密码值，就提示输入一个密码。&lt;/li&gt;
&lt;li&gt;--port=port_num，-P port_num：用户连接的TCP/IP端口号。&lt;/li&gt;
&lt;li&gt;--protocol={TCP|SOCKET|PIPE|MEMORY}：使用的连接协议。&lt;/li&gt;
&lt;li&gt;--replace，-r --replace和--ignore选项控制复制唯一键值已有记录的输入记录的处理。如果指定--replace，新行替换有相同唯一键值的已有行；如果指定--ignore，复制已有唯一键值的输入行被跳过；如果不指定这两个选项，当发现一个复制键值时会出现一个错误，并且忽视文本文件的剩余部分。&lt;/li&gt;
&lt;li&gt;--silent，-s：沉默模式。只有出现错误时才输出信息。&lt;/li&gt;
&lt;li&gt;--user=username，-u user_name：当连接服务器时MySQL使用的用户名。&lt;/li&gt;
&lt;li&gt;--verbose，-v：冗长模式。打印出程序操作的详细信息。&lt;/li&gt;
&lt;li&gt;--version，-V：显示版本信息并退出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数据库迁移&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;数据迁移（data migration）是指选择、准备、提取和转换数据，并将数据从一个计算机存储系统永久地传输到另一个计算机存储系统的过程。&lt;/p&gt;
&lt;p&gt;此外，验证迁移数据的完整性和退役原来旧的数据存储，也被认为是整个数据迁移过程的一部分。
数据库迁移的原因是多样的，包括服务器或存储设备更换、维护或升级，应用程序迁移，网站集成，灾难恢复和数据中心迁移。&lt;/p&gt;
&lt;p&gt;根据不同的需求可能要采取不同的迁移方案，但总体来讲，MySQL数据迁移方案大致可以分为物理迁移和逻辑迁移两类。通常以尽可能自动化的方式执行，从而将人力资源从繁琐的任务中解放出来。&lt;/p&gt;
&lt;h3&gt;迁移注意点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 相同版本的数据库之间迁移注意点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;指的是在主版本号相同的MySQL数据库之间进行数据库移动。&lt;/p&gt;
&lt;p&gt;方式1： 因为迁移前后MySQL数据库的 &lt;code&gt;主版本号相同&lt;/code&gt; ，所以可以通过复制数据库目录来实现数据库迁移，但是物理迁移方式只适用于MyISAM引擎的表。对于InnoDB表，不能用直接复制文件的方式备份数据库。&lt;/p&gt;
&lt;p&gt;方式2： 最常见和最安全的方式是使用 &lt;code&gt;mysqldump命令&lt;/code&gt; 导出数据，然后在目标数据库服务器中使用 MySQL命令导入。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#host1的机器中备份所有数据库,并将数据库迁移到名为host2的机器上
mysqldump –h host1 –uroot –p –-all-databases|
mysql –h host2 –uroot –p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述语句中，“|”符号表示管道，其作用是将mysqldump备份的文件给mysql命令；“--all-databases”表示要迁移所有的数据库。通过这种方式可以直接实现迁移。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 不同版本的数据库之间迁移注意点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如，原来很多服务器使用5.7版本的MySQL数据库，在8.0版本推出来以后，改进了5.7版本的很多缺陷， 因此需要把数据库升级到8.0版本&lt;/p&gt;
&lt;p&gt;旧版本与新版本的MySQL可能使用不同的默认字符集，例如有的旧版本中使用latin1作为默认字符集，而最新版本的MySQL默认字符集为utf8mb4。如果数据库中有中文数据，那么迁移过程中需要对 默认字符集 进行修改 ，不然可能无法正常显示数据。&lt;/p&gt;
&lt;p&gt;高版本的MySQL数据库通常都会 &lt;code&gt;兼容低版本&lt;/code&gt; ，因此可以从低版本的MySQL数据库迁移到高版本的MySQL数据库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 不同数据库之间迁移注意点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不同数据库之间迁移是指从其他类型的数据库迁移到MySQL数据库，或者从MySQL数据库迁移到其他类 型的数据库。这种迁移没有普适的解决方法。&lt;/p&gt;
&lt;p&gt;迁移之前，需要了解不同数据库的架构， 比较它们之间的差异 。不同数据库中定义相同类型的数据的 关键字可能会不同 。例如，MySQL中日期字段分为DATE和TIME两种，而ORACLE日期字段只有DATE；SQL Server数据库中有ntext、Image等数据类型，MySQL数据库没有这些数据类型；MySQL支持的ENUM和SET 类型，这些SQL Server数据库不支持。&lt;/p&gt;
&lt;p&gt;另外，数据库厂商并没有完全按照SQL标准来设计数据库系统，导致不同的数据库系统的 SQL语句 有差别。例如，微软的SQL Server软件使用的是T-SQL语句，T-SQL中包含了非标准的SQL语句，不能和MySQL的SQL语句兼容。&lt;/p&gt;
&lt;p&gt;不同类型数据库之间的差异造成了互相 &lt;code&gt;迁移的困难&lt;/code&gt; ，这些差异其实是商业公司故意造成的技术壁垒。但是不同类型的数据库之间的迁移并 &lt;code&gt;不是完全不可能&lt;/code&gt; 。例如，可以使用 &lt;code&gt;MyODBC&lt;/code&gt; 实现MySQL和SQL Server之 间的迁移。MySQL官方提供的工具 &lt;code&gt;MySQL Migration Toolkit&lt;/code&gt; 也可以在不同数据之间进行数据迁移。 MySQL迁移到Oracle时，需要使用mysqldump命令导出sql文件，然后， &lt;code&gt;手动更改&lt;/code&gt; sql文件中的CREATE语句。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;迁移小结&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-11-05_22-11-22.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;误删操作，怎么办？&lt;/h2&gt;
&lt;p&gt;传统的高可用架构是不能预防误删数据的，因为主库的一个drop table命令，会通过binlog传给所有从库和级联从库，进而导致整个集群的实例都会执行这个命令。
为了找到解决误删数据的更高效的方法，我们需要先对和MySQL相关的误删数据，做下分类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用delete语句误删数据行；&lt;/li&gt;
&lt;li&gt;使用drop table或者truncate table语句误删数据表；&lt;/li&gt;
&lt;li&gt;使用drop database语句误删数据库；&lt;/li&gt;
&lt;li&gt;使用rm命令误删整个MySQL实例。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;delete：误删行&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;处理措施1&lt;/strong&gt;：数据恢复
使用&lt;code&gt;Flashback工具&lt;/code&gt;恢复数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原理：&lt;code&gt;修改binlog&lt;/code&gt;内容，拿回原库重放。如果误删数据涉及到了多个事务的话，需要将事务的顺序调过来再执行。&lt;/p&gt;
&lt;p&gt;使用前提：binlog_format=row和binlog_row_image=FULL。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;处理措施2&lt;/strong&gt;：预防&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码上线前，必须SQL审查、审计。&lt;/p&gt;
&lt;p&gt;建议可以打开安全模式，把&lt;code&gt;sql_safe_updates&lt;/code&gt;参数设置为&lt;code&gt;on&lt;/code&gt;。强制要求加where条件且where后需要是索引字段，否则必须使用limit。否则就会报错。&lt;/p&gt;
&lt;h3&gt;truncate/drop ：误删库/表&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;背景:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;delete全表是很慢的,需要生成回滚日志、写redo、写binlog。所以,从性能角度考虑,优先考虑使用truncate table或者drop table命令。&lt;/p&gt;
&lt;p&gt;使用delete命令删除的数据,你还可以用Flashback来恢复。而使用truncate /drop table和drop database命令删除的数据,就没办法通过Flashback来恢复了。因为,即使我们配置了binlog_format=row,执行这三个命令时,记录的binlog还是statement格式。binlog里面就只有一个truncate/drop语句,这些信息是恢复不出数据的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方案:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种情况下恢复数据,需要使用&lt;code&gt;全量备份&lt;/code&gt;与&lt;code&gt;增量日志&lt;/code&gt;结合的方式。&lt;/p&gt;
&lt;p&gt;方案的前提:有定期的全量备份,并且实时备份binlog。&lt;/p&gt;
&lt;p&gt;举例:有人误删了一个库,时间为下午3点。步骤如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;取最近一次&lt;code&gt;全量备份&lt;/code&gt;。假设设置数据库库是一天一备,最近备份数据是当天&lt;code&gt;凌晨2点&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;用备份恢复出一个&lt;code&gt;临时库&lt;/code&gt;;(注意:这里选择临时库,而不是直接操作主库)&lt;/li&gt;
&lt;li&gt;取出凌晨2点之后的binlog日志;&lt;/li&gt;
&lt;li&gt;剔除误删除数据的语句外,其它语句全部应用到临时库。(前面讲过binlog的恢复)&lt;/li&gt;
&lt;li&gt;最后恢复到主库&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;预防使用truncate/drop误删库/表&lt;/h3&gt;
&lt;p&gt;上面我们说了使用 truncate /drop 语句误删库/表的恢复方案，在生产环境中可以通过下面建议的方案来尽量的避免类似的误操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（1）权限分离&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;限制帐户权限，核心的数据库，一般都&lt;code&gt;不能随便分配写权限&lt;/code&gt;，想要获取写权限需要&lt;code&gt;审批&lt;/code&gt;。比如只给业务开发人员 DML 权限，不给 truncate/drop 权限。即使是 DBA 团队成员，日常也都规定只使用&lt;code&gt;只读账号&lt;/code&gt;，必要的时候才使用有更新权限的账号。&lt;/li&gt;
&lt;li&gt;不同的账号，不同的数据之间要进行&lt;code&gt;权限分离&lt;/code&gt;，避免一个账号可以删除所有库。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;（2）制定操作规范&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如在删除数据表之前，必须先对表做改名操作（比如加 &lt;code&gt;_to_be_deleted&lt;/code&gt;）。然后，观察一段时间，确保对业务无影响以后再删除这张表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（3）设置延迟复制备库&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;简单的说延迟复制就是设置一个固定的延迟时间，比如 1 个小时，让从库落后主库一个小时。出现误删除操作 1 小时内，到这个备库上执行 &lt;code&gt;stop slave&lt;/code&gt;，再通过之前介绍的方法，跳过误操作命令，就可以恢复出需要的数据。这里通过 &lt;code&gt;CHANGE MASTER TO MASTER_DELAY = N &lt;/code&gt;命令，可以指定这个备库持续保持跟主库有 N 秒的延迟。比如把 N 设置为 3600，即代表 1 个小时。&lt;/p&gt;
&lt;p&gt;此外，延迟复制还可以用来解决以下问题：&lt;/p&gt;
&lt;p&gt;① 用来做&lt;code&gt;延迟测试&lt;/code&gt;，比如做好的数据库读写分离，把从库作为读库，那么想知道当数据产生延迟的时候到底会发生什么，就可以使用这个特性模拟延迟。&lt;/p&gt;
&lt;p&gt;② 用于&lt;code&gt;老数据的查询等需求&lt;/code&gt;，比如你经常需要查看某天前一个表或者字段的数值，你可能需要把备份恢复后进行查看，如果有延迟从库，比如延迟一周，那么就可以解决这样类似的需求。&lt;/p&gt;
&lt;h3&gt;rm：误删MySQL实例&lt;/h3&gt;
&lt;p&gt;对于一个有高可用机制的MySQL集群来说，不用担心 rm删除数据了。只是删掉了其中某一个节点的数据的话，HA系统就会开始工作，选出一个新的主库，从而保证整个集群的正常工作。我们要做的就是在这个节点上把数据恢复回来，再接入整个集群。&lt;/p&gt;
&lt;p&gt;但如果是恶意地把整个集群删除，那就需要考虑跨机房备份，跨城市备份。&lt;/p&gt;
</content:encoded></item><item><title>MySQL - 其他数据库日志</title><link>https://zzyang.top/posts/mysql-otherdblog/</link><guid isPermaLink="true">https://zzyang.top/posts/mysql-otherdblog/</guid><pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我们在讲解数据库事务时，讲过两种日志：重做日志、回滚日志。&lt;/p&gt;
&lt;p&gt;对于线上数据库应用系统，突然遭遇&lt;code&gt;数据库宕机&lt;/code&gt;怎么办？在这种情况下，&lt;code&gt;定位宕机的原因&lt;/code&gt;就非常关键。我们可以查看数据库的&lt;code&gt;错误日志&lt;/code&gt;。因为日志中记录了数据库运行中的诊断信息，包括了错误、警告和注释等信息。比如：从日志中发现某个连接中的 SQL 操作发生了死循环，导致内存不足，被系统强行终止了。明确了原因，处理起来也就轻松了，系统很快就恢复了运行。&lt;/p&gt;
&lt;p&gt;除了发现错误，日志在数据复制、数据恢复、操作审计，以及确保数据的永久性和一致性等方面，都有着不可替代的作用。&lt;/p&gt;
&lt;p&gt;千万不要小看日志。很多看似奇怪的问题，答案往往就藏在日志里。很多情况下，只有通过查看日志才能发现问题的原因，真正解决问题。所以，一定要学会查看日志，养成检查日志的习惯，对提升你的数据库应用开发能力至关重要。&lt;/p&gt;
&lt;p&gt;MySQL8.0 官网日志地址：https://dev.mysql.com/doc/refman/8.0/en/server-logs.html&lt;/p&gt;
&lt;h2&gt;MySQL支持的日志&lt;/h2&gt;
&lt;h3&gt;日志类型&lt;/h3&gt;
&lt;p&gt;MySQL有不同类型的日志文件，用来存储不同类型的日志，分为 &lt;code&gt;二进制日志&lt;/code&gt; 、 &lt;code&gt;错误日志&lt;/code&gt; 、 &lt;code&gt;通用查询日志&lt;/code&gt; 和 &lt;code&gt;慢查询日志&lt;/code&gt; ，这也是常用的4种。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MySQL 8&lt;/code&gt;又新增两种支持的日志： &lt;code&gt;中继日志&lt;/code&gt; 和 &lt;code&gt;数据定义语句日志&lt;/code&gt; 。使用这些日志文件，可以查看MySQL内部发生的事情。&lt;/p&gt;
&lt;p&gt;这6类日志分别为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;慢查询日志&lt;/strong&gt;:记录所有执行时间超过&lt;code&gt;long_query_time&lt;/code&gt;的所有查询,方便我们对查询进行优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用查询日志&lt;/strong&gt;:记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令,对我们复原操作的实际场景、发现问题,甚至是对数据库操作的审计都有很大的帮助。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;错误日志&lt;/strong&gt;:记录MySQL服务的启动、运行或停止MySQL服务时出现的问题,方便我们了解服务器的 状态,从而对服务器进行维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二进制日志&lt;/strong&gt;:记录所有更改数据的语句,可以用于主从服务器之间的数据同步,以及服务器遇到故 障时数据的无损失恢复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中继日志&lt;/strong&gt;:用于主从服务器架构中,从服务器用来存放主服务器二进制日志内容的一个中间文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据定义语句日志&lt;/strong&gt;:记录数据定义语句执行的元数据操作。
除二进制日志外,其他日志都是 &lt;code&gt;文本文件&lt;/code&gt; 。默认情况下,所有日志创建于 &lt;code&gt;MySQL数据目录&lt;/code&gt; 中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;日志的弊端&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;日志功能会 &lt;code&gt;降低MySQL数据库的性能&lt;/code&gt; 。例如，在查询非常频繁的MySQL数据库系统中，如果开启了通用查询日志和慢查询日志，MySQL数据库会花费很多时间记录日志。&lt;/li&gt;
&lt;li&gt;日志会 &lt;code&gt;占用大量的磁盘空间&lt;/code&gt; 。对于用户量非常大，操作非常频繁的数据库，日志文件需要的存储空间设置比数据库文件需要的存储空间还要大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;慢查询日志(slow query log)&lt;/h2&gt;
&lt;p&gt;前面《性能分析工具的使用》已经详细讲述。&lt;/p&gt;
&lt;h2&gt;通用查询日志(general query log)&lt;/h2&gt;
&lt;p&gt;通用查询日志用来 &lt;code&gt;记录用户的所有操作&lt;/code&gt; ，包括启动和关闭MySQL服务、所有用户的连接开始时间和截止 时间、发给 MySQL 数据库服务器的所有 SQL 指令等。当我们的数据发生异常时，&lt;strong&gt;查看通用查询日志， 还原操作时的具体场景&lt;/strong&gt;，可以帮助我们准确定位问题。&lt;/p&gt;
&lt;h3&gt;问题场景&lt;/h3&gt;
&lt;p&gt;在电商系统中，购买商品并且使用微信支付完成以后，却发现支付中心的记录并没有新增，此时用户再次使用支付宝支付，就会出现&lt;code&gt;重复支付&lt;/code&gt;的问题。但是当去数据库中查询数据的时候，会发现只有一条记录存在。那么此时给到的现象就是只有一条支付记录，但是用户却支付了两次。&lt;/p&gt;
&lt;p&gt;我们对系统进行了仔细检查，没有发现数据问题，因为用户编号和订单编号以及第三方流水号都是对的。可是用户确实支付了两次，这个时候，我们想到了检查通用查询日志，看看当天到底发生了什么。&lt;/p&gt;
&lt;p&gt;查看之后，发现：1月1日下午2点，用户使用微信支付完以后，但是由于网络故障，支付中心没有及时收到微信支付的回调通知，导致当时没有写入数据。1月1日下午2点30，用户又使用支付宝支付，此时记录更新到支付中心。1月1日晚上9点，微信的回调通知过来了，但是支付中心已经存在了支付宝的记录，所以只能覆盖记录了。&lt;/p&gt;
&lt;p&gt;由于网络的原因导致了重复支付。至于解决问题的方案就很多了，这里省略。&lt;/p&gt;
&lt;p&gt;可以看到&lt;strong&gt;通用查询日志&lt;/strong&gt;可以帮助我们了解操作发生的具体时间和操作的细节，对找出异常发生的原因极其关键。&lt;/p&gt;
&lt;h3&gt;查看当前状态&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 通用查询日志处于关闭状态
# 通用查询日志文件的名称是atguigu01.log


mysql&amp;gt; SHOW VARIABLES LIKE &apos;%general%&apos;;
+------------------+------------------------------+
| Variable_name    | Value                        |
+------------------+------------------------------+
| general_log      | OFF                          |
| general_log_file | /var/lib/mysql/atguigu01.log |
+------------------+------------------------------+
2 rows in set (0.03 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明1: 系统变量 &lt;code&gt;general_log&lt;/code&gt; 的值是 &lt;code&gt;OFF&lt;/code&gt;，即通用查询日志处于关闭状态。在 MySQL 中，&lt;strong&gt;这个参数的默认值是关闭的&lt;/strong&gt;。因为一旦开启记录通用查询日志，MySQL 会记录所有的连接起止和相关的 SQL 操作，这样会消耗系统资源并且占用磁盘空间。我们可以通过手动修改变量的值，&lt;strong&gt;在需要的时候开启日志&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;说明2: 通用查询日志文件的名称是 &lt;code&gt;atguigu01.log&lt;/code&gt;。存储路径是&lt;code&gt;/var/lib/mysql/&lt;/code&gt;，默认也是数据路径。这样我们就知道在哪里可以查看通用查询日志的内容了。&lt;/p&gt;
&lt;h3&gt;启动日志&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方式1：永久性方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;修改&lt;code&gt;my.cnf&lt;/code&gt;或者&lt;code&gt;my.ini&lt;/code&gt;配置文件来设置。在&lt;code&gt;[mysqld]&lt;/code&gt;组下加入log选项，并重启MySQL服务。格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
general_log=ON
general_log_file=[path[filename]] #日志文件所在目录路径，filename为日志文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不指定目录和文件名，通用查询日志将默认存储在MySQL数据目录中的&lt;code&gt;hostname.log&lt;/code&gt;文件中， &lt;code&gt;hostname&lt;/code&gt;表示主机名。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式2：临时性方式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET GLOBAL general_log=on; # 开启通用查询日志
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;SET GLOBAL general_log_file=&apos;path/filename&apos;; # 设置日志文件保存位置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的，关闭操作SQL命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET GLOBAL general_log=off; # 关闭通用查询日志
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看设置后情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;general_log%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查看日志&lt;/h3&gt;
&lt;p&gt;通用查询日志是以 &lt;code&gt;文本文件&lt;/code&gt; 的形式存储在文件系统中的，可以使用 &lt;code&gt;文本编辑器&lt;/code&gt; 直接打开日志文件。每台 MySQL 服务器的通用查询日志内容是不同的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Windows 操作系统中，使用&lt;code&gt;文本文件查看器&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在 Linux 系统中，可以使用 &lt;code&gt;vi&lt;/code&gt; 工具或者 &lt;code&gt;gedit&lt;/code&gt; 工具查看；&lt;/li&gt;
&lt;li&gt;在 Mac OSX 系统中，可以使用&lt;code&gt;文本文件查看器&lt;/code&gt;或者 &lt;code&gt;vi&lt;/code&gt; 等工具查看。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从 &lt;code&gt;SHOW VARIABLES LIKE &apos;general_log%&apos;&lt;/code&gt; 结果中可以看到通用查询日志的位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 进入到通用查询日志目录
cd /var/lib/mysql

# 查看通用查询日志，即可看到操作的任务记录
vi xxxx.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在通用查询日志里面，我们可以清楚地看到，什么时候开启了新的客户端登陆数据库，登录之后做了什么 SQL 操作，针对的是哪个数据表等信息。&lt;/p&gt;
&lt;h3&gt;停止日志&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方式1：永久性方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;修改 &lt;code&gt;my.cnf&lt;/code&gt; 或者 &lt;code&gt;my.ini&lt;/code&gt; 文件，把&lt;code&gt;[mysqld]&lt;/code&gt;组下的 &lt;code&gt;general_log&lt;/code&gt; 值设置为 &lt;code&gt;OFF&lt;/code&gt; 或者把 &lt;code&gt;general_log&lt;/code&gt; 一项注释掉。修改保存后，再重启MySQL服务，即可生效。&lt;/p&gt;
&lt;p&gt;举例1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
general_log=OFF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;举例2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
#general_log=OFF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式2：临时性方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用SET语句停止MySQL通用查询日志功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET GLOBAL general_log=off;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看设置后情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;general_log%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;删除\刷新日志&lt;/h3&gt;
&lt;p&gt;如果数据的使用非常频繁，那么通用查询日志会占用服务器非常大的磁盘空间。数据管理员可以删除很长时间之前的查询日志，以保证MySQL服务器上的硬盘空间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;手动删除文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;general_log%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，通用查询日志的目录默认为MySQL数据目录。在该目录下手动删除通用查询日志 atguigu01.log&lt;/p&gt;
&lt;p&gt;使用如下命令重新生成查询日志文件，具体命令如下。刷新MySQL数据目录，发现创建了新的日志文件。前提一定要开启通用日志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 刷新 MySQL 的日志文件
mysqladmin -uroot -p flush-logs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望备份旧的通用查询日志，就必须先将旧的日志文件复制出来或者改名，然后执行上面的mysqladmin命令。正确流程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd mysql-data-directory # 输入自己的通用日志文件所在目录
mv mysql.general.log mysql.general.log.old # 指定旧的文件名 以及 新的文件名
mysqladmin -uroot -p flush-logs  # 必须是在日志开启的情况下，才能刷新日志
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info
&lt;code&gt;flush_logs&lt;/code&gt; 会根据你启用的日志类型，触发不同的“滚动/刷新”动作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;错误日志 (error log)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;把缓存中的内容立即写到错误日志文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;二进制日志 (binlog)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;当前 binlog 文件会被关闭，生成一个新的 binlog 文件 (序号 +1)。&lt;/li&gt;
&lt;li&gt;常用于主从复制，或手动切分 binlog。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;中继日志 (relay log)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;在从库上，会生成新的 relay log 文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;通用查询日志 (general log) 和慢查询日志 (slow query log)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;如果开启了这些日志，也会刷新，并生成新的文件。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
mv 命令（全称 move）主要用于移动或重命名文件和目录。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重命名文件/目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当源路径和目标路径在同一目录下时，mv 会执行重命名操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv 旧文件名 新文件名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv oldname.txt newname.txt  # 重命名文件
mv dir1 dir2               # 重命名目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;移动文件/目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将文件或目录从源路径移动到目标路径（可跨目录）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv [选项] 源文件 目标文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv file.txt /home/user/documents/  # 将 file.txt 移动到 documents 目录
mv dir1 /backup/                  # 将 dir1 目录移动到 /backup/ 下
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;选项&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-i：交互模式，如果目标文件存在，会提示是否覆盖。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 移动目录并覆盖前确认
mv -i mydir/ /backup/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;docker容器中查看通用查询日志&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 先进入容器中
docker exec -it mysql-container bash
# 进入mysql中
mysql -uroot -p

# 在mysql中执行
SET GLOBAL general_log = &apos;ON&apos;;

# 查看文件位置
mysql&amp;gt; SHOW VARIABLES LIKE &apos;general_log%&apos;;
+------------------+---------------------------------+
| Variable_name    | Value                           |
+------------------+---------------------------------+
| general_log      | ON                              |
| general_log_file | /var/lib/mysql/ca39d70d7ced.log |
+------------------+---------------------------------+
2 rows in set (0.00 sec)


# 退出mysql客户端
\q

# 然后在容器里看日志文件
tail -n 50 -f /var/lib/mysql/ca39d70d7ced.log


root@ca39d70d7ced:/var/lib/mysql# cat /var/lib/mysql/ca39d70d7ced.log
/usr/sbin/mysqld, Version: 8.0.27 (MySQL Community Server - GPL). started with:
Tcp port: 3306  Unix socket: /var/run/mysqld/mysqld.sock
Time                 Id Command    Argument
2025-10-02T13:35:17.042065Z     41848 Query     SHOW VARIABLES LIKE &apos;%general%&apos;
2025-10-02T13:35:29.205100Z     41848 Query     show databases
2025-10-02T13:35:46.124200Z     41848 Query     SELECT DATABASE()
2025-10-02T13:35:46.124418Z     41848 Init DB   zy_uums
2025-10-02T13:35:46.125422Z     41848 Query     show databases
2025-10-02T13:35:46.126249Z     41848 Query     show tables
2025-10-02T13:35:46.127311Z     41848 Field List        log 

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;错误日志(error log)&lt;/h2&gt;
&lt;p&gt;错误日志记录了 MySQL 服务器启动、停止运行的时间，以及系统启动、运行和停止过程中的诊断信息，包括&lt;code&gt;错误&lt;/code&gt;、&lt;code&gt;警告&lt;/code&gt;和&lt;code&gt;提示&lt;/code&gt;等。&lt;/p&gt;
&lt;p&gt;通过错误日志可以查看系统的运行状态，便于即时发现故障、修复故障。如果 MySQL 服务&lt;code&gt;出现异常&lt;/code&gt;，错误日志是发现问题、解决故障的&lt;code&gt;首选&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;启动日志&lt;/h3&gt;
&lt;p&gt;在MySQL数据库中，错误日志功能是 &lt;strong&gt;默认开启&lt;/strong&gt; 的。而且，错误日志 &lt;strong&gt;无法被禁止&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;默认情况下，错误日志存储在MySQL数据库的数据文件夹下，名称默认为 &lt;code&gt;mysqld.log&lt;/code&gt; （Linux系统）或 &lt;code&gt;hostname.err&lt;/code&gt; （mac系统）。如果需要制定文件名，则需要在&lt;code&gt;my.cnf&lt;/code&gt;或者&lt;code&gt;my.ini&lt;/code&gt;中做如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
log-error=[path/[filename]] #path为日志文件所在的目录路径，filename为日志文件名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改配置项后，需要重启MySQL服务以生效。&lt;/p&gt;
&lt;h3&gt;查看日志&lt;/h3&gt;
&lt;p&gt;MySQL错误日志是以文本文件形式存储的，可以使用文本编辑器直接查看。&lt;/p&gt;
&lt;p&gt;查询错误日志的存储路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SHOW VARIABLES LIKE &apos;log_err%&apos;;
+----------------------------+----------------------------------------+
| Variable_name              | Value                                  |
+----------------------------+----------------------------------------+
| log_error                  | stderr                                 |
| log_error_services         | log_filter_internal; log_sink_internal |
| log_error_suppression_list |                                        |
| log_error_verbosity        | 2                                      |
+----------------------------+----------------------------------------+
4 rows in set (0.00 sec)

# MySQL 的错误日志没有写入文件，而是输出到标准错误（stderr）。
# 在 Docker 容器里，标准输出（stdout）和标准错误（stderr）都会被 Docker 捕获并写到容器日志里。
# 直接在宿主机执行：
docker logs -f mysql-container
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[root@iZbp143l1lire02d1p2giaZ ~]# docker logs mysql
2024-08-03 08:55:08+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.27-1debian10 started.
2024-08-03 08:55:08+00:00 [Note] [Entrypoint]: Switching to dedicated user &apos;mysql&apos;
2024-08-03 08:55:08+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.27-1debian10 started.
2024-08-03 08:55:08+00:00 [Note] [Entrypoint]: Initializing database files
2024-08-03T08:55:08.717819Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.27) initializing of server in progress as process 41
2024-08-03T08:55:08.726202Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2024-08-03T08:55:09.567909Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
.....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在正常情况下，执行结果中可以看到错误日志文件是mysqld.log，位于MySQL默认的数据目录下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SHOW VARIABLES LIKE &apos;log_err%&apos;;
+----------------------------+----------------------------------------+
| Variable_name              | Value                                  |
+----------------------------+----------------------------------------+
| log_error                  | /var/log/mysqld.log                    |
| log_error_services         | log_filter_internal; log_sink_internal |
| log_error_suppression_list |                                        |
| log_error_verbosity        | 2                                      |
+----------------------------+----------------------------------------+
4 rows in set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误日志文件中记录了服务器启动的时间，以及存储引擎 InnoDB 启动和停止的时间等。我们在做初始化时候生成的数据库初始密码也是记录在 error.log 中。&lt;/p&gt;
&lt;h3&gt;删除\刷新日志&lt;/h3&gt;
&lt;p&gt;对于很久以前的错误日志，数据库管理员查看这些错误日志的可能性不大，可以将这些错误日志删除， 以保证MySQL服务器上的 &lt;code&gt;硬盘空间&lt;/code&gt; 。MySQL的错误日志是以文本文件的形式存储在文件系统中的，可以 &lt;code&gt;直接删除&lt;/code&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一步（方式1）：删除操作&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;rm -f /var/lib/mysql/mysqld.log
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一步（方式2）：重命名文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mv /var/log/mysqld.log /var/log/mysqld.log.old
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第二步：重建日志&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mysqladmin -uroot -p flush-logs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能会报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@atguigu01 log]# mysqladmin -uroot -p flush-logs
Enter password:
mysqladmin: refresh failed; error: &apos;Could not open file &apos;/var/log/mysqld.log&apos; for
error logging.&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官网提示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-02_22-21-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们需要进行 补充操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;install -omysql -gmysql -m0644 /dev/null /var/log/mysqld.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;flush-logs&lt;/code&gt; 指令操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MySQL 5.5.7&lt;/code&gt;以前的版本，flush-logs将错误日志文件重命名为&lt;code&gt;filename.err_old&lt;/code&gt;，并创建新的日志文件。&lt;/li&gt;
&lt;li&gt;从&lt;code&gt;MySQL 5.5.7&lt;/code&gt;开始，flush-logs只是重新打开日志文件，并不做日志备份和创建的操作。&lt;/li&gt;
&lt;li&gt;如果日志文件不存在，MySQL启动或者执行flush-logs时会自动创建新的日志文件。重新创建错误日志，大小为0字节。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MySQL8.0新特性&lt;/h3&gt;
&lt;p&gt;MySQL8.0里对错误日志的改进。MySQL8.0的错误日志可以理解为一个全新的日志，在这个版本里，接受了来自社区的广泛批评意见，在这些意见和建议的基础上生成了新的日志。
下面这些是来自社区的意见：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认情况下内容过于冗长&lt;/li&gt;
&lt;li&gt;遗漏了有用的信息&lt;/li&gt;
&lt;li&gt;难以过滤某些信息&lt;/li&gt;
&lt;li&gt;没有标识错误信息的子系统源&lt;/li&gt;
&lt;li&gt;没有错误代码，解析消息需要识别错误&lt;/li&gt;
&lt;li&gt;引导消息可能会丢失&lt;/li&gt;
&lt;li&gt;固定格式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;针对这些意见，MySQL做了如下改变：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;采用组件架构，通过不同的组件执行日志的写入和过滤功能&lt;/li&gt;
&lt;li&gt;写入错误日志的全部信息都具有唯一的错误代码从10000开始&lt;/li&gt;
&lt;li&gt;增加了一个新的消息分类《system》用于在错误日志中始终可见的非错误但服务器状态更改事件的消息&lt;/li&gt;
&lt;li&gt;增加了额外的附加信息，例如关机时的版本信息，谁发起的关机等等&lt;/li&gt;
&lt;li&gt;两种过滤方式，Internal和Dragnet&lt;/li&gt;
&lt;li&gt;三种写入形式，经典、JSON和syseventlog&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;小结：&lt;/p&gt;
&lt;p&gt;通常情况下，管理员不需要查看错误日志。但是，MySQL服务器发生异常时，管理员可以从错误日志中找到发生异常的时间、原因，然后根据这些信息来解决异常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;二进制日志(bin log)&lt;/h2&gt;
&lt;p&gt;binlog可以说是MySQL中比较 &lt;code&gt;重要&lt;/code&gt; 的日志了，在日常开发及运维过程中，经常会遇到。&lt;/p&gt;
&lt;p&gt;binlog即binary log，二进制日志文件，也叫作变更日志（update log）。它记录了数据库所有执行的 DDL 和 DML 等数据库更新事件的语句，但是不包含没有修改任何数据的语句（如数据查询语句select、 show等）。&lt;/p&gt;
&lt;p&gt;它以&lt;code&gt;事件形式&lt;/code&gt;记录并保存在&lt;code&gt;二进制文件&lt;/code&gt;中。通过这些信息，我们可以再现数据更新操作的全过程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果想要记录所有语句（例如，为了识别有问题的查询），需要使用通用查询日志。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;binlog主要应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是用于&lt;strong&gt;数据恢复&lt;/strong&gt;，如果MySQL数据库意外停止，可以通过二进制日志文件来查看用户执行了哪些操作，对数据库服务器文件做了哪些修改，然后根据二进制日志文件中的记录来恢复数据库服务器。&lt;/li&gt;
&lt;li&gt;二是用于&lt;strong&gt;数据复制&lt;/strong&gt;，由于日志的延续性和时效性，master把它的二进制日志传递给slaves来达到master-slave数据一致的目的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以说MySQL数据库的&lt;strong&gt;数据备份&lt;/strong&gt;、&lt;strong&gt;主备&lt;/strong&gt;、&lt;strong&gt;主主&lt;/strong&gt;、&lt;strong&gt;主从&lt;/strong&gt;都离不开binlog，需要依靠binlog来同步数据，保证数据一致性。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-02_22-38-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;查看默认情况&lt;/h3&gt;
&lt;p&gt;查看记录二进制日志是否开启：在MySQL8中默认情况下，二进制文件是开启的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it mysql bash

mysql -uroot -p
Enter password: 

mysql&amp;gt; show variables like &apos;%log_bin%&apos;;
+---------------------------------+-----------------------------+
| Variable_name                   | Value                       |
+---------------------------------+-----------------------------+
| log_bin                         | ON                          |
| log_bin_basename                | /var/lib/mysql/binlog       |
| log_bin_index                   | /var/lib/mysql/binlog.index |
| log_bin_trust_function_creators | OFF                         |
| log_bin_use_v1_row_events       | OFF                         |
| sql_log_bin                     | ON                          |
+---------------------------------+-----------------------------+
6 rows in set (0.02 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;log_bin_basename&lt;/code&gt;：是binlog日志的基本文件名，后面会追加标识来表示每一个文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;log_bin_index&lt;/code&gt;：是binlog文件的索引文件，这个文件管理了所有的binlog文件的目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;log_bin_trust_function_creators&lt;/code&gt;：限制存储过程，前面我们已经讲过了，这是因为二进制日志的一个重要功能是用于主从复制，而存储函数有可能导致主从的数据不一致。所以当开启二进制日志后，需要限制存储函数的创建、修改、调用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;log_bin_use_v1_row_events&lt;/code&gt; 此只读系统变量已弃用。ON表示使用版本1二进制日志行，OFF表示使用版本2二进制日志行（MySQL 5.6 的默认值为2）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; \q
Bye

root@b5bc2ea3fb2c:/# cd /var/lib/mysql/   
root@b5bc2ea3fb2c:/var/lib/mysql# ls
&apos;#ib_16384_0.dblwr&apos;   binlog.index      hm@002ditem      ib_logfile0   nacos                server-key.pem
&apos;#ib_16384_1.dblwr&apos;   ca-key.pem        hm@002dpay       ib_logfile1   performance_schema   sys
&apos;#innodb_temp&apos;        ca.pem            hm@002dtrade     ibdata1       private_key.pem      undo_001
 auto.cnf             client-cert.pem   hm@002duser      ibtmp1        public_key.pem       undo_002
 binlog.000030        client-key.pem    hmall            mysql         seata
 binlog.000031        hm@002dcart       ib_buffer_pool   mysql.ibd     server-cert.pem
root@b5bc2ea3fb2c:/var/lib/mysql# 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们会发现有很多binlog文件，这是因为，每当mysql服务器重启的时候，都会帮我们创建一个新的binlog文件。&lt;/p&gt;
&lt;h3&gt;日志参数设置&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方式1：永久性方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;修改MySQL的 &lt;code&gt;/ect/my.cnf&lt;/code&gt; 或 &lt;code&gt;my.ini&lt;/code&gt; 文件可以设置二进制日志的相关参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
#启用二进制日志
log-bin=atguigu-bin
binlog_expire_logs_seconds=600
max_binlog_size=100M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若是docker环境，进入到我们挂载的目录，进入conf目录，编辑my.cnf文件，添加如上内容。&lt;/p&gt;
&lt;p&gt;提示:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;log-bin=mysql-bin #打开日志(主机需要打开)，这个mysql-bin也可以自定义，这里也可以加上路径，
如:/home/www/mysql_bin_log/mysql-bin&lt;/li&gt;
&lt;li&gt;binlog_expire_logs_seconds: 此参数控制二进制日志文件保留的时长，单位是&lt;strong&gt;秒&lt;/strong&gt;，默认2592000 30天
--14400 4小时;86400 1天;259200 3天;&lt;/li&gt;
&lt;li&gt;max_binlog_size: 控制单个二进制日志大小，当前日志文件大小超过此变量时，执行切换动作。此参数
的&lt;strong&gt;最大和默认值是1GB&lt;/strong&gt;，该设置并&lt;strong&gt;不能严格控制Binlog的大小&lt;/strong&gt;，尤其是Binlog比较靠近最大值而又遇到一
个比较大事务时，为了保证事务的完整性，可能不做切换日志的动作(一个事务还没结束，结果这个文件大小满了)，只能将该事务的所有SQL都记录
进当前日志，直到事务结束。一般情况下可采取默认值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;重新启动MySQL服务，查询二进制日志的信息，执行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart mysqld
# 或者
docker restart mysql

[root@192 conf]# docker exec -it mysql bash
root@b5bc2ea3fb2c:/# mysql -uroot -p
Enter password: 

mysql&amp;gt; show variables like &apos;%log_bin%&apos;;
+---------------------------------+----------------------------------+
| Variable_name                   | Value                            |
+---------------------------------+----------------------------------+
| log_bin                         | ON                               |
| log_bin_basename                | /var/lib/mysql/atguigu-bin       |
| log_bin_index                   | /var/lib/mysql/atguigu-bin.index |
| log_bin_trust_function_creators | OFF                              |
| log_bin_use_v1_row_events       | OFF                              |
| sql_log_bin                     | ON                               |
+---------------------------------+----------------------------------+
6 rows in set (0.01 sec)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入之后即可看到该目录&lt;code&gt;atguigu-bin.000001&lt;/code&gt;,&lt;code&gt;atguigu-bin.index&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@b5bc2ea3fb2c:/# cd /var
root@b5bc2ea3fb2c:/var# cd lib/
root@b5bc2ea3fb2c:/var/lib# cd mysql/
root@b5bc2ea3fb2c:/var/lib/mysql# ls
&apos;#ib_16384_0.dblwr&apos;   binlog.000031     client-key.pem   ib_buffer_pool   nacos                sys
&apos;#ib_16384_1.dblwr&apos;   binlog.000032     hm@002dcart      ib_logfile0      performance_schema   undo_001
&apos;#innodb_temp&apos;        binlog.000033     hm@002ditem      ib_logfile1      private_key.pem      undo_002
 atguigu-bin.000001   binlog.index      hm@002dpay       ibdata1          public_key.pem
 atguigu-bin.index    ca-key.pem        hm@002dtrade     ibtmp1           seata
 auto.cnf             ca.pem            hm@002duser      mysql            server-cert.pem
 binlog.000030        client-cert.pem   hmall            mysql.ibd        server-key.pem
root@b5bc2ea3fb2c:/var/lib/mysql# 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;设置带文件夹的bin-log日志存放目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果想改变日志文件的目录和名称，可以对my.cnf或my.ini中的log_bin参数修改如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
log-bin=&quot;/var/lib/mysql/binlog/atguigu-bin&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：新建的文件夹需要使用mysql用户，使用下面的命令即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chown -R -v mysql:mysql binlog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启MySQL服务之后，新的二进制日志文件将出现在/var/lib/mysql/binlog/文件夹下面:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show variables like &apos;%log_bin%&apos;;
+---------------------------------+--------------------------------------------+
| Variable_name                   | Value                                      |
+---------------------------------+--------------------------------------------+
| log_bin                         | ON                                         |
| log_bin_basename                | /var/lib/mysql/binlog/atguigu-bin          |
| log_bin_index                   | /var/lib/mysql/binlog/atguigu-bin.index    |
| log_bin_trust_function_creators | OFF                                        |
| log_bin_use_v1_row_events       | OFF                                        |
| sql_log_bin                     | ON                                         |
+---------------------------------+--------------------------------------------+
6 rows in set (0.00 sec)

[root@node1 binlog]# ls
atguigu-bin.000001  atguigu-bin.index
[root@node1 binlog]# pwd
/var/lib/mysql/binlog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;strong&gt;数据库文件最好不要与日志文件放在同一个磁盘上&lt;/strong&gt;！这样，当数据库文件所在的磁盘发生故障时，可以使用日志文件恢复数据。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式2：临时性方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果不希望通过修改配置文件并重启的方式设置二进制日志的话，还可以使用如下指令，需要注意的是 在mysql8中只有 会话级别 的设置，没有了global级别的设置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# global 级别
mysql&amp;gt; set global sql_log_bin=0;
ERROR 1228 (HY000): Variable &apos;sql_log_bin&apos; is a SESSION variable and can`t be used
with SET GLOBAL

# session级别，只能使用session级别
mysql&amp;gt; SET sql_log_bin=0;
Query OK, 0 rows affected (0.01 秒)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查看日志&lt;/h3&gt;
&lt;p&gt;当MySQL创建二进制日志文件时，先创建一个以“filename”为名称、以“.index”为后缀的文件，再创建一 个以“filename”为名称、以“.000001”为后缀的文件。&lt;/p&gt;
&lt;p&gt;MySQL服务 &lt;code&gt;重新启动一次&lt;/code&gt; ，以“.000001”为后缀的文件就会增加一个，并且后缀名按1递增。即日志文件的 个数与MySQL服务启动的次数相同；如果日志长度超过了 &lt;code&gt;max_binlog_size&lt;/code&gt; 的上限（默认是1GB），就会创建一个新的日志文件。&lt;/p&gt;
&lt;p&gt;查看当前的二进制日志文件列表及大小。指令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SHOW BINARY LOGS;
+--------------------+-----------+-----------+
| Log_name           | File_size | Encrypted |
+--------------------+-----------+-----------+
| atguigu-bin.000001 | 156       | No        |
+--------------------+-----------+-----------+
1 行于数据集 (0.02 秒)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有对数据库的修改都会记录在binlog中。但binlog是二进制文件，无法直接查看，想要更直观的观测它就要借助&lt;code&gt;mysqlbinlog&lt;/code&gt;命令工具了。指令如下：在查看执行，先执行一条SQL语句，如下&lt;/p&gt;
&lt;p&gt;随便做几个增删改操作，我们再执行&lt;code&gt;SHOW BINARY LOGS;&lt;/code&gt; , 我们会发现File_size变大了；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;update student set name=&apos;张三_back&apos; where id=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开始查看binlog&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 注意查看的这个文件一定要看最新的
root@b5bc2ea3fb2c:/var/lib/mysql# mysqlbinlog &quot;/var/lib/mysql/atguigu-bin.000001&quot;
# The proper term is pseudo_replica_mode, but we use this compatibility alias
# to make the statement usable on server versions 8.0.24 and older.
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#251008 23:58:55 server id 1  end_log_pos 125 CRC32 0x61e1c329  Start: binlog v 4, server v 8.0.27 created 251008 23:58:55 at startup
# Warning: this binlog is either in use or was not closed properly.
ROLLBACK/*!*/;
BINLOG &apos;
P4rmaA8BAAAAeQAAAH0AAAABAAQAOC4wLjI3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAA/iuZoEwANAAgAAAAABAAEAAAAYQAEGggAAAAICAgCAAAACgoKKioAEjQA
CigBKcPhYQ==
&apos;/*!*/;
# at 125
#251008 23:58:55 server id 1  end_log_pos 156 CRC32 0xaf26c362  Previous-GTIDs
# [empty]
# at 156
#251009  0:28:19 server id 1  end_log_pos 235 CRC32 0x603ebf33  Anonymous_GTID  last_committed=0        sequence_number=1     rbr_only=yes    original_committed_timestamp=1759940899272405   immediate_commit_timestamp=1759940899272405  transaction_length=339
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1759940899272405 (2025-10-09 00:28:19.272405 CST)
# immediate_commit_timestamp=1759940899272405 (2025-10-09 00:28:19.272405 CST)
/*!80001 SET @@session.original_commit_timestamp=1759940899272405*//*!*/;
/*!80014 SET @@session.original_server_version=80027*//*!*/;
/*!80014 SET @@session.immediate_server_version=80027*//*!*/;
SET @@SESSION.GTID_NEXT= &apos;ANONYMOUS&apos;/*!*/;
# at 235
#251009  0:28:19 server id 1  end_log_pos 322 CRC32 0x2e028b2c  Query   thread_id=12    exec_time=0     error_code=0
SET TIMESTAMP=1759940899/*!*/;
SET @@session.pseudo_thread_id=12/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1168113696/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8mb4 *//*!*/;
SET @@session.character_set_client=255,@@session.collation_connection=255,@@session.collation_server=224/*!*/;
SET @@session.time_zone=&apos;SYSTEM&apos;/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
/*!80011 SET @@session.default_collation_for_utf8mb4=255*//*!*/;
BEGIN
/*!*/;
# at 322
#251009  0:28:19 server id 1  end_log_pos 396 CRC32 0x1a3d78f4  Table_map: `hm-trade`.`order` mapped to number 98
# at 396
#251009  0:28:19 server id 1  end_log_pos 464 CRC32 0x64ee3309  Write_rows: table id 98 flags: STMT_END_F

BINLOG &apos;
I5HmaBMBAAAASgAAAIwBAAAAAGIAAAAAAAEACGhtLXRyYWRlAAVvcmRlcgAMCAMBCAERERERERER
BwAAAAAAAADwDwEBIPR4PRo=
I5HmaB4BAAAARAAAANABAAAAAGIAAAAAAAEAAgAM///ABwEAyE5nbcEb4FoBAAIDAAAAAAAAAAFo
5pEjaOaRIwkz7mQ=
&apos;/*!*/;
# at 464
#251009  0:28:19 server id 1  end_log_pos 495 CRC32 0x67f243ac  Xid = 1357
COMMIT/*!*/;
# at 495
#251009  0:28:42 server id 1  end_log_pos 574 CRC32 0xe1964ead  Anonymous_GTID  last_committed=1        sequence_number=2     rbr_only=yes    original_committed_timestamp=1759940922068152   immediate_commit_timestamp=1759940922068152  transaction_length=386
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1759940922068152 (2025-10-09 00:28:42.068152 CST)
# immediate_commit_timestamp=1759940922068152 (2025-10-09 00:28:42.068152 CST)
/*!80001 SET @@session.original_commit_timestamp=1759940922068152*//*!*/;
/*!80014 SET @@session.original_server_version=80027*//*!*/;
/*!80014 SET @@session.immediate_server_version=80027*//*!*/;
SET @@SESSION.GTID_NEXT= &apos;ANONYMOUS&apos;/*!*/;
# at 574
#251009  0:28:42 server id 1  end_log_pos 670 CRC32 0xdfe894fe  Query   thread_id=12    exec_time=0     error_code=0
SET TIMESTAMP=1759940922/*!*/;
BEGIN
/*!*/;
# at 670
#251009  0:28:42 server id 1  end_log_pos 744 CRC32 0x0860a7dd  Table_map: `hm-trade`.`order` mapped to number 98
# at 744
#251009  0:28:42 server id 1  end_log_pos 850 CRC32 0x81c6dd08  Update_rows: table id 98 flags: STMT_END_F

BINLOG &apos;
OpHmaBMBAAAASgAAAOgCAAAAAGIAAAAAAAEACGhtLXRyYWRlAAVvcmRlcgAMCAMBCAERERERERER
BwAAAAAAAADwDwEBIN2nYAg=
OpHmaB8BAAAAagAAAFIDAAAAAGIAAAAAAAEAAgAM/////8AHAQDITmdtwRvgWgEAAgMAAAAAAAAA
AWjmkSNo5pEjgAcBAMhOZ23BG+BaAQACAwAAAAAAAAACaOaRI2jmkTpo5pE6CN3GgQ==
&apos;/*!*/;
# at 850
#251009  0:28:42 server id 1  end_log_pos 881 CRC32 0xf11d6f94  Xid = 1381
COMMIT/*!*/;
SET @@SESSION.GTID_NEXT= &apos;AUTOMATIC&apos; /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
root@b5bc2ea3fb2c:/var/lib/mysql# 

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果可以看到，这是一个简单的日志文件，日志中记录了用户的一些操作，这里并没有出现具体的SQL语句，这是因为binlog关键字后面的内容是经过编码后的二进制日志。
这里一个update语句包含如下事件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Query事件 负责开始一个事务(BEGIN)&lt;/li&gt;
&lt;li&gt;Table_map事件 负责映射需要的表&lt;/li&gt;
&lt;li&gt;Update_rows事件 负责写入数据&lt;/li&gt;
&lt;li&gt;Xid事件 负责结束事务
下面命令将行事件以&lt;strong&gt;伪SQL的形式&lt;/strong&gt;表现出来&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用&lt;code&gt;mysqlbinlog -v&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@b5bc2ea3fb2c:/var/lib/mysql# mysqlbinlog -v &quot;/var/lib/mysql/atguigu-bin.000001&quot;
# The proper term is pseudo_replica_mode, but we use this compatibility alias
# to make the statement usable on server versions 8.0.24 and older.
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#251008 23:58:55 server id 1  end_log_pos 125 CRC32 0x61e1c329  Start: binlog v 4, server v 8.0.27 created 251008 23:58:55 at startup
# Warning: this binlog is either in use or was not closed properly.
ROLLBACK/*!*/;
BINLOG &apos;
P4rmaA8BAAAAeQAAAH0AAAABAAQAOC4wLjI3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAA/iuZoEwANAAgAAAAABAAEAAAAYQAEGggAAAAICAgCAAAACgoKKioAEjQA
CigBKcPhYQ==
&apos;/*!*/;
# at 125
#251008 23:58:55 server id 1  end_log_pos 156 CRC32 0xaf26c362  Previous-GTIDs
# [empty]
# at 156
#251009  0:28:19 server id 1  end_log_pos 235 CRC32 0x603ebf33  Anonymous_GTID  last_committed=0        sequence_number=1     rbr_only=yes    original_committed_timestamp=1759940899272405   immediate_commit_timestamp=1759940899272405  transaction_length=339
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1759940899272405 (2025-10-09 00:28:19.272405 CST)
# immediate_commit_timestamp=1759940899272405 (2025-10-09 00:28:19.272405 CST)
/*!80001 SET @@session.original_commit_timestamp=1759940899272405*//*!*/;
/*!80014 SET @@session.original_server_version=80027*//*!*/;
/*!80014 SET @@session.immediate_server_version=80027*//*!*/;
SET @@SESSION.GTID_NEXT= &apos;ANONYMOUS&apos;/*!*/;
# at 235
#251009  0:28:19 server id 1  end_log_pos 322 CRC32 0x2e028b2c  Query   thread_id=12    exec_time=0     error_code=0
SET TIMESTAMP=1759940899/*!*/;
SET @@session.pseudo_thread_id=12/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1168113696/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8mb4 *//*!*/;
SET @@session.character_set_client=255,@@session.collation_connection=255,@@session.collation_server=224/*!*/;
SET @@session.time_zone=&apos;SYSTEM&apos;/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
/*!80011 SET @@session.default_collation_for_utf8mb4=255*//*!*/;
BEGIN
/*!*/;
# at 322
#251009  0:28:19 server id 1  end_log_pos 396 CRC32 0x1a3d78f4  Table_map: `hm-trade`.`order` mapped to number 98
# at 396
#251009  0:28:19 server id 1  end_log_pos 464 CRC32 0x64ee3309  Write_rows: table id 98 flags: STMT_END_F

BINLOG &apos;
I5HmaBMBAAAASgAAAIwBAAAAAGIAAAAAAAEACGhtLXRyYWRlAAVvcmRlcgAMCAMBCAERERERERER
BwAAAAAAAADwDwEBIPR4PRo=
I5HmaB4BAAAARAAAANABAAAAAGIAAAAAAAEAAgAM///ABwEAyE5nbcEb4FoBAAIDAAAAAAAAAAFo
5pEjaOaRIwkz7mQ=
&apos;/*!*/;
### INSERT INTO `hm-trade`.`order`
### SET
###   @1=2000000000000000001
###   @2=88800
###   @3=2
###   @4=3
###   @5=1
###   @6=1759940899
###   @7=NULL
###   @8=NULL
###   @9=NULL
###   @10=NULL
###   @11=NULL
###   @12=1759940899
# at 464
#251009  0:28:19 server id 1  end_log_pos 495 CRC32 0x67f243ac  Xid = 1357
COMMIT/*!*/;
# at 495
#251009  0:28:42 server id 1  end_log_pos 574 CRC32 0xe1964ead  Anonymous_GTID  last_committed=1        sequence_number=2     rbr_only=yes    original_committed_timestamp=1759940922068152   immediate_commit_timestamp=1759940922068152  transaction_length=386
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1759940922068152 (2025-10-09 00:28:42.068152 CST)
# immediate_commit_timestamp=1759940922068152 (2025-10-09 00:28:42.068152 CST)
/*!80001 SET @@session.original_commit_timestamp=1759940922068152*//*!*/;
/*!80014 SET @@session.original_server_version=80027*//*!*/;
/*!80014 SET @@session.immediate_server_version=80027*//*!*/;
SET @@SESSION.GTID_NEXT= &apos;ANONYMOUS&apos;/*!*/;
# at 574
#251009  0:28:42 server id 1  end_log_pos 670 CRC32 0xdfe894fe  Query   thread_id=12    exec_time=0     error_code=0
SET TIMESTAMP=1759940922/*!*/;
BEGIN
/*!*/;
# at 670
#251009  0:28:42 server id 1  end_log_pos 744 CRC32 0x0860a7dd  Table_map: `hm-trade`.`order` mapped to number 98
# at 744
#251009  0:28:42 server id 1  end_log_pos 850 CRC32 0x81c6dd08  Update_rows: table id 98 flags: STMT_END_F

BINLOG &apos;
OpHmaBMBAAAASgAAAOgCAAAAAGIAAAAAAAEACGhtLXRyYWRlAAVvcmRlcgAMCAMBCAERERERERER
BwAAAAAAAADwDwEBIN2nYAg=
OpHmaB8BAAAAagAAAFIDAAAAAGIAAAAAAAEAAgAM/////8AHAQDITmdtwRvgWgEAAgMAAAAAAAAA
AWjmkSNo5pEjgAcBAMhOZ23BG+BaAQACAwAAAAAAAAACaOaRI2jmkTpo5pE6CN3GgQ==
&apos;/*!*/;
### UPDATE `hm-trade`.`order`
### WHERE
###   @1=2000000000000000001
###   @2=88800
###   @3=2
###   @4=3
###   @5=1
###   @6=1759940899
###   @7=NULL
###   @8=NULL
###   @9=NULL
###   @10=NULL
###   @11=NULL
###   @12=1759940899
### SET
###   @1=2000000000000000001
###   @2=88800
###   @3=2
###   @4=3
###   @5=2
###   @6=1759940899
###   @7=1759940922
###   @8=NULL
###   @9=NULL
###   @10=NULL
###   @11=NULL
###   @12=1759940922
# at 850
#251009  0:28:42 server id 1  end_log_pos 881 CRC32 0xf11d6f94  Xid = 1381
COMMIT/*!*/;
SET @@SESSION.GTID_NEXT= &apos;AUTOMATIC&apos; /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
root@b5bc2ea3fb2c:/var/lib/mysql# 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到我们刚刚进行了，insert操作和update操作，这些都是伪sql&lt;/p&gt;
&lt;p&gt;前面的命令同时显示binlog格式的语句，使用如下命令不显示它
&lt;code&gt;mysqlbinlog -v --base64-output=DECODE-ROWS&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@b5bc2ea3fb2c:/var/lib/mysql# mysqlbinlog -v --base64-output=DECODE-ROWS  &quot;/var/lib/mysql/atguigu-bin.000001&quot;
# The proper term is pseudo_replica_mode, but we use this compatibility alias
# to make the statement usable on server versions 8.0.24 and older.
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#251008 23:58:55 server id 1  end_log_pos 125 CRC32 0x61e1c329  Start: binlog v 4, server v 8.0.27 created 251008 23:58:55 at startup
# Warning: this binlog is either in use or was not closed properly.
ROLLBACK/*!*/;
# at 125
#251008 23:58:55 server id 1  end_log_pos 156 CRC32 0xaf26c362  Previous-GTIDs
# [empty]
# at 156
#251009  0:28:19 server id 1  end_log_pos 235 CRC32 0x603ebf33  Anonymous_GTID  last_committed=0        sequence_number=1     rbr_only=yes    original_committed_timestamp=1759940899272405   immediate_commit_timestamp=1759940899272405  transaction_length=339
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1759940899272405 (2025-10-09 00:28:19.272405 CST)
# immediate_commit_timestamp=1759940899272405 (2025-10-09 00:28:19.272405 CST)
/*!80001 SET @@session.original_commit_timestamp=1759940899272405*//*!*/;
/*!80014 SET @@session.original_server_version=80027*//*!*/;
/*!80014 SET @@session.immediate_server_version=80027*//*!*/;
SET @@SESSION.GTID_NEXT= &apos;ANONYMOUS&apos;/*!*/;
# at 235
#251009  0:28:19 server id 1  end_log_pos 322 CRC32 0x2e028b2c  Query   thread_id=12    exec_time=0     error_code=0
SET TIMESTAMP=1759940899/*!*/;
SET @@session.pseudo_thread_id=12/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1168113696/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8mb4 *//*!*/;
SET @@session.character_set_client=255,@@session.collation_connection=255,@@session.collation_server=224/*!*/;
SET @@session.time_zone=&apos;SYSTEM&apos;/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
/*!80011 SET @@session.default_collation_for_utf8mb4=255*//*!*/;
BEGIN
/*!*/;
# at 322
#251009  0:28:19 server id 1  end_log_pos 396 CRC32 0x1a3d78f4  Table_map: `hm-trade`.`order` mapped to number 98
# at 396
#251009  0:28:19 server id 1  end_log_pos 464 CRC32 0x64ee3309  Write_rows: table id 98 flags: STMT_END_F
### INSERT INTO `hm-trade`.`order`
### SET
###   @1=2000000000000000001
###   @2=88800
###   @3=2
###   @4=3
###   @5=1
###   @6=1759940899
###   @7=NULL
###   @8=NULL
###   @9=NULL
###   @10=NULL
###   @11=NULL
###   @12=1759940899
# at 464
#251009  0:28:19 server id 1  end_log_pos 495 CRC32 0x67f243ac  Xid = 1357
COMMIT/*!*/;
# at 495
#251009  0:28:42 server id 1  end_log_pos 574 CRC32 0xe1964ead  Anonymous_GTID  last_committed=1        sequence_number=2     rbr_only=yes    original_committed_timestamp=1759940922068152   immediate_commit_timestamp=1759940922068152  transaction_length=386
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1759940922068152 (2025-10-09 00:28:42.068152 CST)
# immediate_commit_timestamp=1759940922068152 (2025-10-09 00:28:42.068152 CST)
/*!80001 SET @@session.original_commit_timestamp=1759940922068152*//*!*/;
/*!80014 SET @@session.original_server_version=80027*//*!*/;
/*!80014 SET @@session.immediate_server_version=80027*//*!*/;
SET @@SESSION.GTID_NEXT= &apos;ANONYMOUS&apos;/*!*/;
# at 574
#251009  0:28:42 server id 1  end_log_pos 670 CRC32 0xdfe894fe  Query   thread_id=12    exec_time=0     error_code=0
SET TIMESTAMP=1759940922/*!*/;
BEGIN
/*!*/;
# at 670
#251009  0:28:42 server id 1  end_log_pos 744 CRC32 0x0860a7dd  Table_map: `hm-trade`.`order` mapped to number 98
# at 744
#251009  0:28:42 server id 1  end_log_pos 850 CRC32 0x81c6dd08  Update_rows: table id 98 flags: STMT_END_F
### UPDATE `hm-trade`.`order`
### WHERE
###   @1=2000000000000000001
###   @2=88800
###   @3=2
###   @4=3
###   @5=1
###   @6=1759940899
###   @7=NULL
###   @8=NULL
###   @9=NULL
###   @10=NULL
###   @11=NULL
###   @12=1759940899
### SET
###   @1=2000000000000000001
###   @2=88800
###   @3=2
###   @4=3
###   @5=2
###   @6=1759940899
###   @7=1759940922
###   @8=NULL
###   @9=NULL
###   @10=NULL
###   @11=NULL
###   @12=1759940922
# at 850
#251009  0:28:42 server id 1  end_log_pos 881 CRC32 0xf11d6f94  Xid = 1381
COMMIT/*!*/;
SET @@SESSION.GTID_NEXT= &apos;AUTOMATIC&apos; /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
root@b5bc2ea3fb2c:/var/lib/mysql# 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于mysqlbinlog工具的使用技巧还有很多，例如只解析对某个库的操作或者某个时间段内的操作等。简单分享几个常用的语句，更多操作可以参考官方文档。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 可查看参数帮助
mysqlbinlog --no-defaults --help
# 查看最后100行
mysqlbinlog --no-defaults --base64-output=decode-rows -vv atguigu-bin.000002 |tail
-100
# 根据position查找
mysqlbinlog --no-defaults --base64-output=decode-rows -vv atguigu-bin.000002 |grep -A
20 &apos;4939002&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这种办法读取出binlog日志的全文内容比较多，不容易分辨查看到pos点信息，下面介绍一种更为方便的查询命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show binlog events [IN &apos;log_name&apos;] [FROM pos] [LIMIT [offset,] row_count];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IN &apos;log_name&apos;&lt;/code&gt; ：指定要查询的binlog文件名（不指定就是第一个binlog文件）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FROM pos&lt;/code&gt; ：指定从哪个pos起始点开始查起（不指定就是从整个文件首个pos点开始算）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LIMIT [offset]&lt;/code&gt; ：偏移量(不指定就是0)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;row_count&lt;/code&gt; :查询总条数（不指定就是所有行）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
show binlog events该指令是在mysql下跑的&lt;/p&gt;
&lt;p&gt;而mysqlbinlog是在宿主机跑的&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SHOW BINARY LOGS;
+--------------------+-----------+-----------+
| Log_name           | File_size | Encrypted |
+--------------------+-----------+-----------+
| atguigu-bin.000001 |       881 | No        |
+--------------------+-----------+-----------+
1 row in set (0.00 sec)

mysql&amp;gt; SHOW BINLOG EVENTS IN &apos;atguigu-bin.000001&apos;;
+--------------------+-----+----------------+-----------+-------------+--------------------------------------+
| Log_name           | Pos | Event_type     | Server_id | End_log_pos | Info                                 |
+--------------------+-----+----------------+-----------+-------------+--------------------------------------+
| atguigu-bin.000001 |   4 | Format_desc    |         1 |         125 | Server ver: 8.0.27, Binlog ver: 4    |
| atguigu-bin.000001 | 125 | Previous_gtids |         1 |         156 |                                      |
| atguigu-bin.000001 | 156 | Anonymous_Gtid |         1 |         235 | SET @@SESSION.GTID_NEXT= &apos;ANONYMOUS&apos; |
| atguigu-bin.000001 | 235 | Query          |         1 |         322 | BEGIN                                |
| atguigu-bin.000001 | 322 | Table_map      |         1 |         396 | table_id: 98 (hm-trade.order)        |
| atguigu-bin.000001 | 396 | Write_rows     |         1 |         464 | table_id: 98 flags: STMT_END_F       |
| atguigu-bin.000001 | 464 | Xid            |         1 |         495 | COMMIT /* xid=1357 */                |
| atguigu-bin.000001 | 495 | Anonymous_Gtid |         1 |         574 | SET @@SESSION.GTID_NEXT= &apos;ANONYMOUS&apos; |
| atguigu-bin.000001 | 574 | Query          |         1 |         670 | BEGIN                                |
| atguigu-bin.000001 | 670 | Table_map      |         1 |         744 | table_id: 98 (hm-trade.order)        |
| atguigu-bin.000001 | 744 | Update_rows    |         1 |         850 | table_id: 98 flags: STMT_END_F       |
| atguigu-bin.000001 | 850 | Xid            |         1 |         881 | COMMIT /* xid=1381 */                |
+--------------------+-----+----------------+-----------+-------------+--------------------------------------+
12 rows in set (0.01 sec)

mysql&amp;gt; SHOW BINLOG EVENTS IN &apos;atguigu-bin.000001&apos; from 235 limit 0,1;
+--------------------+-----+------------+-----------+-------------+-------+
| Log_name           | Pos | Event_type | Server_id | End_log_pos | Info  |
+--------------------+-----+------------+-----------+-------------+-------+
| atguigu-bin.000001 | 235 | Query      |         1 |         322 | BEGIN |
+--------------------+-----+------------+-----------+-------------+-------+
1 row in set (0.00 sec)

mysql&amp;gt; SHOW BINLOG EVENTS IN &apos;atguigu-bin.000001&apos; from 235 limit 0,2;
+--------------------+-----+------------+-----------+-------------+-------------------------------+
| Log_name           | Pos | Event_type | Server_id | End_log_pos | Info                          |
+--------------------+-----+------------+-----------+-------------+-------------------------------+
| atguigu-bin.000001 | 235 | Query      |         1 |         322 | BEGIN                         |
| atguigu-bin.000001 | 322 | Table_map  |         1 |         396 | table_id: 98 (hm-trade.order) |
+--------------------+-----+------------+-----------+-------------+-------------------------------+
2 rows in set (0.00 sec)

mysql&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这条语句可以将指定的binlog日志文件，分成有效事件行的方式返回，并可使用limit指定pos点的起始偏移，查询条数。其它举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#a、查询第一个最早的binlog日志：
show binlog events\G;
#b、指定查询mysql-bin.000002这个文件
show binlog events in &apos;atguigu-bin.000002&apos;\G;
#c、指定查询mysql-bin.000002这个文件，从pos点:391开始查起：
show binlog events in &apos;atguigu-bin.000002&apos; from 391\G;
#d、指定查询mysql-bin.000002这个文件，从pos点:391开始查起，查询5条（即5条语句）
show binlog events in &apos;atguigu-bin.000002&apos; from 391 limit 5\G;
#e、指定查询 mysql-bin.000002这个文件，从pos点:391开始查起，偏移2行（即中间跳过2个）查询5条（即5条语句）。
show binlog events in &apos;atguigu-bin.000002&apos; from 391 limit 2,5\G;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面我们讲了这么多都是基于binlog的默认格式，binlog格式查看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show variables like &apos;binlog_format&apos;;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
1 行于数据集 (0.02 秒)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除此之外，binlog还有2种格式，分别是 Statement 和 Mixed&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Statement
每一条会修改数据的sql都会记录在binlog中。
优点：不需要记录每一行的变化，减少了binlog日志量，节约了IO，提高性能。&lt;/li&gt;
&lt;li&gt;Row
5.1.5版本的MySQL才开始支持row level 的复制，它不记录sql语句上下文相关信息，仅保存哪条记录被修改。
优点：row level 的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下 的存储过程，或function，以及trigger的调用和触发无法被正确复制的问题。&lt;/li&gt;
&lt;li&gt;Mixed
从5.1.8版本开始，MySQL提供了Mixed格式，实际上就是Statement与Row的结合。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::info
Statement是在binlog中把每一个有变更的sql都记录下来，只记录变更的sql&lt;/p&gt;
&lt;p&gt;Row记录的是，比如一个update的sql，Statement记录的是update语句，而Row会记录这个10条语句都改了什么(假如修改了10条记录)；
:::&lt;/p&gt;
&lt;h3&gt;使用日志恢复数据&lt;/h3&gt;
&lt;p&gt;如果MySQL服务器启用了二进制日志，在数据库出现意外丢失数据时，可以使用MySQLbinlog工具从指定的时间点开始（例如，最后一次备份）直到现在或另一个指定的时间点的日志中回复数据。&lt;/p&gt;
&lt;p&gt;mysqlbinlog恢复数据的语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlbinlog [option] filename|mysql –uuser -ppass;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令可以这样理解：使用mysqlbinlog命令来读取filename中的内容，然后使用mysql命令将这些内容恢复到数据库中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;filename&lt;/code&gt;：是日志文件名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;option&lt;/code&gt;：可选项，比较重要的两对 option 参数是 --start-date、--stop-date 和 --start-position、--stop-position。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--start-date&lt;/code&gt; 和 &lt;code&gt;--stop-date&lt;/code&gt;：可以指定恢复数据库的起始时间点和结束时间点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--start-position&lt;/code&gt; 和 &lt;code&gt;--stop-position&lt;/code&gt;：可以指定恢复数据的开始位置和结束位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：使用mysqlbinlog命令进行恢复操作时，必须是编号小的先恢复，例如atguigu - bin.000001必须在atguigu - bin.000002之前恢复。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it mysql bash

mysql -uroot -p

# 查看二进制日志
show variables like &apos;%log_bin%&apos;;

# 服务器当前存在的所有二进制日志文件（binlog）
show binary logs;

# 在一张表中做增删改操作，然后将所有数据都delete掉
mysql&amp;gt; delete from pay_order where id &amp;gt; 10;
Query OK, 9 rows affected (0.02 sec)

mysql&amp;gt; select * from pay_order;
Empty set (0.00 sec)

# 生成新的binlog文件，与之前旧的binlog文件分隔开
flush logs;

show binary logs;


# 退出mysql
\q

# linux客户端使用该命令（恢复一条插入的数据）
/usr/bin/mysqlbinlog --start-position=235 --stop-position=594 --database=hm-pay /var/lib/mysql/atguigu-bin.000002 | /usr/bin/mysql -uroot -p123 -v hm-pay

# -v 以及 --database 后面跟的是数据库名称
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恢复后我们可以查看最终结果和删除数据之前的结果一样，利用 binlog 实现了数据恢复。&lt;/p&gt;
&lt;p&gt;当然也可以使用日期恢复，命令格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/bin/mysqlbinlog --start-datetime=&quot;2022-01-05 15:39:22&quot; --stop-datetime=&quot;2022-01-05 15:40:19&quot; --database=atguigu14 /var/lib/mysql/binlog/atguigu-bin.000005 | /usr/bin/mysql -uroot -pabc123 -v atguigu14

# 查看时间的话我们可以在mysql中使用：
mysqlbinlog &apos;/var/lib/mysql/atgJigu-bin.000002&apos;;

# 可以看到里面有TIMESTAMP,可以转为yyyy-mm-dd hh:mm:ss格式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日期可以根据binlog日志详情查看。可能出现一个事务执行时间过短，那么就是同样的时间，一秒内执行完成，此时我们找到下一个事务的开始时间即可，多计算一些时间就可以了。本次事务的开始时间是220105 15:39:22，结束时间设置为220105 15:40:19。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;偏移量的计算方法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show binary logs;

show binlog events in &apos;atguigu-bin.000002&apos;;

# 根据xid去找最近的一个BEGIN，就是一条语句执行的其实与结束位置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-20_23-54-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;log_bin 是否开启了二进制日志功能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;删除二进制日志&lt;/h3&gt;
&lt;p&gt;MySQL的二进制文件可以配置自动删除，同时MySQL也提供了安全的手动删除二进制文件的方法。 &lt;code&gt;PURGE MASTER LOGS&lt;/code&gt; 只删除指定部分的二进制日志文件， &lt;code&gt;RESET MASTER&lt;/code&gt; 删除所有的二进制日志文件。具体如下：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. PURGE MASTER LOGS：删除指定日志文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PURGE MASTER LOGS语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PURGE {MASTER | BINARY} LOGS TO ‘指定日志文件名’
PURGE {MASTER | BINARY} LOGS BEFORE ‘指定日期’
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用示例如下：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看当前已有的binlog文件
show binary logs;

# 为了测试我们可以多执行几次flush logs命令，将所有日志都刷新到磁盘，这样文件能多一些，我们好进行删除操作
flush logs;
# 查看当前已有的binlog文件

mysql&amp;gt; show binary logs;
+--------------------+-----------+-----------+
| Log_name           | File_size | Encrypted |
+--------------------+-----------+-----------+
| atguigu-bin.000005 |       205 | No        |
| atguigu-bin.000006 |       205 | No        |
| atguigu-bin.000007 |       205 | No        |
| atguigu-bin.000008 |       205 | No        |
| atguigu-bin.000009 |       205 | No        |
| atguigu-bin.000010 |       156 | No        |
+--------------------+-----------+-----------+
6 rows in set (0.00 sec)


mysql&amp;gt; purge master logs to &apos;atguigu-bin.000007&apos;;
Query OK, 0 rows affected (0.00 sec)

mysql&amp;gt; show binary logs;
+--------------------+-----------+-----------+
| Log_name           | File_size | Encrypted |
+--------------------+-----------+-----------+
| atguigu-bin.000007 |       205 | No        |
| atguigu-bin.000008 |       205 | No        |
| atguigu-bin.000009 |       205 | No        |
| atguigu-bin.000010 |       156 | No        |
+--------------------+-----------+-----------+
4 rows in set (0.00 sec)


# 比binlog.000007早的所有日志文件都已经被删除了。注意是不包括binlog.000007这个文件。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;举例：&lt;/strong&gt; 使用&lt;code&gt;PURGE MASTER LOGS&lt;/code&gt;语句删除2020年10月25号前创建的所有日志文件。具体步骤如下：&lt;/p&gt;
&lt;p&gt;（1）显示二进制日志文件列表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW BINARY LOGS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（2）执行mysqlbinlog命令查看二进制日志文件binlog.000005的内容（假设我们想删的是该文件中某个日期之前的binlog日志）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlbinlog --no-defaults &quot;/var/lib/mysql/binlog/atguigu-bin.000005&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果可以看出20220105为日志创建的时间，即2022年1月05日。&lt;/p&gt;
&lt;p&gt;（3）使用PURGE MASTER LOGS语句删除2022年1月05日前创建的所有日志文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PURGE MASTER LOGS before &quot;20220105&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（4）显示二进制日志文件列表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW BINARY LOGS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2022年01月05号之前的二进制日志文件都已经被删除，最后一个没有删除，是因为当前在用，还未记录最后的时间，所以未被删除。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. RESET MASTER: 删除所有二进制日志文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;RESET MASTER&lt;/code&gt; 语句，清空所有的binlog日志。MySQL会重新创建二进制文件，新的日志文件扩展名将重新从000001开始编号。&lt;strong&gt;慎用&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;举例：使用RESET MASTER语句删除所有日志文件。&lt;/p&gt;
&lt;p&gt;（1）重启MySQL服务若干次，执行SHOW语句显示二进制日志文件列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW BINARY LOGS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（2）执行RESET MASTER语句，删除所有日志文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RESET MASTER;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完该语句后，原来的所有二进制日志已经全部被删除。&lt;/p&gt;
&lt;h3&gt;其他场景&lt;/h3&gt;
&lt;p&gt;二进制日志可以通过数据库的 &lt;code&gt;全量备份&lt;/code&gt; 和二进制日志中保存的 &lt;code&gt;增量信息&lt;/code&gt; ，完成数据库的 无损失恢复。 但是，如果遇到数据量大、数据库和数据表很多（比如分库分表的应用）的场景，用二进制日志进行数据恢复，是很有挑战性的，因为起止位置不容易管理。&lt;/p&gt;
&lt;p&gt;在这种情况下，一个有效的解决办法是 &lt;code&gt;配置主从数据库服务器&lt;/code&gt; ，甚至是 &lt;code&gt;一主多从&lt;/code&gt; 的架构，把二进制日志文件的内容通过中继日志，同步到从数据库服务器中，这样就可以有效避免数据库故障导致的数据异常等问题。&lt;/p&gt;
&lt;h2&gt;再谈二进制日志&lt;/h2&gt;
&lt;h3&gt;写入机制&lt;/h3&gt;
&lt;p&gt;binlog的写入时机也非常简单，事务执行过程中，先把日志写到 &lt;code&gt;binlog cache&lt;/code&gt; ，事务提交的时候，再把binlog cache写到binlog文件中。因为一个事务的binlog不能被拆开，无论这个事务多大，也要确保一次性写入，所以系统会给每个线程分配一个块内存作为&lt;code&gt;binlog cache&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们可以通过&lt;code&gt;binlog_cache_size&lt;/code&gt;参数控制单个线程 &lt;code&gt;binlog cache&lt;/code&gt; 大小，如果存储内容超过了这个参数，就要暂存到磁盘（Swap）。binlog日志刷盘流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_21-51-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;上图的write，是指把日志写入到文件系统的page cache，并没有把数据持久化到磁盘，所以速度比较快&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上图的fsync，才是将数据持久化到磁盘的操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;write和fsync的时机，可以由参数 &lt;code&gt;sync_binlog&lt;/code&gt; 控制，默认是 &lt;code&gt;0&lt;/code&gt; 。为0的时候，表示每次提交事务都只 write，由系统自行判断什么时候执行 fsync。虽然性能得到提升，但是机器宕机，page cache里面的binglog 会丢失。如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_21-56-29.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了安全起见，可以设置为1，表示每次提交事务都会执行fsync，就如同&lt;strong&gt;redo log刷盘流程&lt;/strong&gt;一样。&lt;/p&gt;
&lt;p&gt;最后还有一种折中方式，可以设置为N(N&amp;gt;1)，表示每次提交事务都write，但累积N个事务后才fsync。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_21-59-31.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在出现I0瓶颈的场景里，将sync_binlog设置成一个比较大的值，可以提升性能。同样的，如果机器宕机，会丢失
最近N个事务的binlog日志。&lt;/p&gt;
&lt;h3&gt;binlog与redolog对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;redo log 它是&lt;code&gt;物理日志&lt;/code&gt;，记录内容是“在某个数据页上做了什么修改”，属于 InnoDB 存储引擎层产生的。&lt;/li&gt;
&lt;li&gt;而 binlog 是&lt;code&gt;逻辑日志&lt;/code&gt;，记录内容是语句的原始逻辑，类似于“给 ID = 2 这一行的 c 字段加 1”，属于 MySQL Server 层。&lt;/li&gt;
&lt;li&gt;虽然它们都属于持久化的保证，但是侧重点不同。
&lt;ul&gt;
&lt;li&gt;redo log 让 InnoDB 存储引擎拥有了崩溃恢复能力。&lt;/li&gt;
&lt;li&gt;binlog 保证了 MySQL 集群架构的数据一致性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;两阶段提交&lt;/h3&gt;
&lt;p&gt;在执行更新语句过程，会记录redo log与binlog两块日志，以基本的事务为单位，redo log在事务执行过程中可以不断写入，而binlog只有在提交事务时才写入，所以redo log与binlog的&lt;strong&gt;写入时机&lt;/strong&gt;不一样。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_22-03-27.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;redo log与binlog两份日志之间的逻辑不一致，会出现什么问题?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;以update语句为例，假设id=2的记录，字段c值是0，把字段c值更新成1，SQL语句为update T set c=1 where id=2。&lt;/p&gt;
&lt;p&gt;假设执行过程中写完redo log日志后，binlog日志写期间发生了异常，会出现什么情况呢?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_22-04-48.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于binlog没写完就异常，这时候binlog里面没有对应的修改记录。因此，之后用binlog日志恢复数据时，就会少这一次更新，恢复出来的这一行c值是0，而原库因为redo log日志恢复，这一行c值是1，最终数据不一致。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_22-06-52.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了解决两份日志之间的逻辑一致问题， InnoDB存储引擎使用&lt;strong&gt;两阶段提交&lt;/strong&gt;方案。原理很简单，将redo log的写入拆成了两个步骤prepare和commit，这就是&lt;strong&gt;两阶段提交&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_22-08-42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;strong&gt;两阶段提交&lt;/strong&gt;后，写入binlog时发生异常也不会有影响，因为MySQL根据redo log日志恢复数据时，发现redo log还处于prepare阶段，并且没有对应binlog日志，就会回滚该事务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_22-09-54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;另一个场景，redo log设置commit阶段发生异常，那会不会回滚事务呢?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-10-27_22-11-45.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;并不会回滚事务，它会执行上图框住的逻辑，虽然redolog是处于prepare阶段，但是能通过事务id找到对应的
binlog日志，所以MySQL认为是完整的，就会提交事务恢复数据。&lt;/p&gt;
&lt;h2&gt;中继日志(relay log)&lt;/h2&gt;
&lt;h3&gt;介绍&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;中继日志只在主从服务器架构的从服务器上存在&lt;/strong&gt;。从服务器为了与主服务器保持一致，要从主服务器读取二进制日志的内容，并且把读取到的信息写入&lt;code&gt;本地的日志文件&lt;/code&gt;中，这个从服务器本地的日志文件就叫&lt;strong&gt;中继日志&lt;/strong&gt;。然后，从服务器读取中继日志，并根据中继日志的内容对从服务器的数据进行更新，完成主从服务器的&lt;code&gt;数据同步&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;搭建好主从服务器之后，中继日志默认会保存在从服务器的数据目录下。&lt;/p&gt;
&lt;p&gt;文件名的格式是：&lt;code&gt;从服务器名 -relay-bin.序号&lt;/code&gt;。中继日志还有一个索引文件：&lt;code&gt;从服务器名 -relay-bin.index&lt;/code&gt;，用来定位当前正在使用的中继日志。&lt;/p&gt;
&lt;h3&gt;查看中继日志&lt;/h3&gt;
&lt;p&gt;中继日志与二进制日志的格式相同，可以用 &lt;code&gt;mysqlbinlog&lt;/code&gt; 工具进行查看。下面是中继日志的一个片段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET TIMESTAMP=1618558728/*!*/;
BEGIN
/*!*/;
# at 950
#210416 15:38:48 server id 1 end_log_pos 832 CRC32 0xcc16d651 Table_map:
`atguigu`.`test` mapped to number 91
# at 1000
#210416 15:38:48 server id 1 end_log_pos 872 CRC32 0x07e4047c Delete_rows: table id
91 flags: STMT_END_F -- server id 1 是主服务器，意思是主服务器删了一行数据
BINLOG &apos;
CD95YBMBAAAAMgAAAEADAAAAAFsAAAAAAAEABGRlbW8ABHRlc3QAAQMAAQEBAFHWFsw=
CD95YCABAAAAKAAAAGgDAAAAAFsAAAAAAAEAAgAB/wABAAAAfATkBw==
&apos;/*!*/;
# at 1040
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一段的意思是，主服务器（“server id 1”）对表 atguigu.test 进行了 2 步操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;定位到表 atguigu.test 编号是 91 的记录，日志位置是 832；
删除编号是 91 的记录，日志位置是 872
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;恢复的典型错误&lt;/h3&gt;
&lt;p&gt;如果从服务器宕机，有的时候为了系统恢复，要重装操作系统，这样就可能会导致你的&lt;code&gt;服务器名称&lt;/code&gt;与之前&lt;code&gt;不同&lt;/code&gt;。
而中继日志里是&lt;code&gt;包含从服务器名&lt;/code&gt;的。在这种情况下，就可能导致你恢复从服务器的时候，无法从宕机前的中继日志里读取数据，以为是日志文件损坏了，其实是名称不对了。&lt;/p&gt;
&lt;p&gt;解决的方法也很简单，把从服务器的名称改回之前的名称。&lt;/p&gt;
</content:encoded></item><item><title>Electron</title><link>https://zzyang.top/posts/electron-note/</link><guid isPermaLink="true">https://zzyang.top/posts/electron-note/</guid><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 Electron&lt;/h2&gt;
&lt;p&gt;Electron 是⼀个 &lt;strong&gt;跨平台&lt;/strong&gt; 的 &lt;strong&gt;桌⾯应⽤&lt;/strong&gt; 开发框架，开发者可以使⽤：HTML、CSS、JavaScript 等 Web 技术来构建桌⾯应⽤程序，
它的本质是结合了 &lt;code&gt;Chromium&lt;/code&gt; 和 &lt;code&gt;Node.js&lt;/code&gt;，现在⼴泛⽤于桌⾯应 ⽤程序开发，例如这写桌⾯应⽤都⽤到了 Electron 技术：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一款应用广泛的跨平台的桌面应用开发框架。&lt;/li&gt;
&lt;li&gt;Electron 的本质是结合了 &lt;code&gt;Chromium&lt;/code&gt; 与 &lt;code&gt;Node.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用 HTML、CSS、JS 等 Web 技术构建桌面应用程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Electron = Chromium + Node.js + Native API(Electron 原生的 API)&lt;/p&gt;
&lt;p&gt;:::tip
CS架构的软件有个优点：它是存在我们电脑包里面的，你的电脑不被木马黑掉，那么这个软件就是没有问题的，网页的话各种接口、数据、图片我们都是可以看到的;&lt;/p&gt;
&lt;p&gt;但是CS架构软件我们可以抓包，我们也可以进行防止抓包，CS软件要比BS更加安全&lt;/p&gt;
&lt;p&gt;总的来说就是，速度快，安全（vscode就是electron开发的）
:::&lt;/p&gt;
&lt;h3&gt;常见的桌面GUI&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;语音&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QT&lt;/td&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;td&gt;跨平台、性能好、生态好&lt;/td&gt;
&lt;td&gt;依赖多，程序包大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyQT&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;底层集成度高、易上手&lt;/td&gt;
&lt;td&gt;授权问题(收费)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WPF&lt;/td&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;类库丰富、扩展灵活&lt;/td&gt;
&lt;td&gt;只支持Windows，程序包大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WinForm&lt;/td&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;性能好，组件丰富，易上手&lt;/td&gt;
&lt;td&gt;只支持Windows，UI差&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swing&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;基于AWT，组件丰富&lt;/td&gt;
&lt;td&gt;性能差，UI一般&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NW.js&lt;/td&gt;
&lt;td&gt;JS&lt;/td&gt;
&lt;td&gt;跨平台性好，界面美观&lt;/td&gt;
&lt;td&gt;底层交互差、性能差，包大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Electron&lt;/td&gt;
&lt;td&gt;JS&lt;/td&gt;
&lt;td&gt;相比NW拓展更好&lt;/td&gt;
&lt;td&gt;底层交互差、性能差，包大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CEF&lt;/td&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;td&gt;性能好，灵活集成，UI美观&lt;/td&gt;
&lt;td&gt;占用资源多，包大&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Electron 流程模型&lt;/h2&gt;
&lt;p&gt;主进程就是个&lt;code&gt;.js&lt;/code&gt;文件，这个 js 文件是个纯粹的 node 环境；主进程主要目的就是管理渲染进程；主进程可以管理多个渲染进程，主进程只有一个，渲染进程可以有n个&lt;/p&gt;
&lt;p&gt;主进程是可以调用原生 API 的&lt;/p&gt;
&lt;p&gt;一个窗口背后对应一个渲染进程，渲染进程就是浏览器环境，需要用 HTML、CSS、JS 来支撑&lt;/p&gt;
&lt;p&gt;进程间的通信简称 IPC&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E4%B8%80%E5%B0%8F%E6%97%B6%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8BElectron_page2_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip
Node 环境（Node.js runtime environment）的内置模块：核心 API，比如：
fs（文件系统）
http / https（网络）
path（路径处理）
os（系统信息）等等，还有 JS 执行环境（V8）；&lt;/p&gt;
&lt;p&gt;Node 环境不包含 DOM / BOM，没有 window、document、alert、localStorage。因为 Node 不运行在浏览器里，它不关心页面。
:::&lt;/p&gt;
&lt;h2&gt;搭建工程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm init

# 然后一路回车到底
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;package.json&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;electron_test&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;main&quot;: &quot;main.js&quot;,
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
  },
  &quot;author&quot;: &quot;zzy&quot;,
  &quot;license&quot;: &quot;ISC&quot;,
  &quot;description&quot;: &quot;this is a electron demo&quot;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装 Electron&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i electron -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改这里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;electron .&quot;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;根目录创建 main.js 文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { app, BrowserWindow } = require(&quot;electron&quot;);

app.on(&quot;ready&quot;, () =&amp;gt; {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true, // 是否隐藏顶部菜单栏
    alwaysOnTop: false, // 是否一直置顶
  });
  win.loadFile(&quot;./pages/index.html&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 BrowserWindow 的更多配置项，请参考：&lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/base-window#%E5%AE%9E%E4%BE%8B%E5%B1%9E%E6%80%A7&quot;&gt;BrowserWindow 实例属性&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;根目录创建&lt;code&gt;pages/index.html&lt;/code&gt;文件以及&lt;code&gt;pages/index.css&lt;/code&gt;文件&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;运行&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时开发者⼯具会报出⼀个安全警告，需要修改&lt;code&gt;index.html&lt;/code&gt;，配置 CSP(Content- Security-Policy)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;default-src &apos;self&apos;; style-src &apos;self&apos; &apos;unsafe-inline&apos;; img-src &apos;self&apos; data:;&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述配置的说明&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;default-src &apos;self&apos;
default-src：配置加载策略，适用于所有未在其它指令中明确指定的资源类型。
self：仅允许从同源的资源加载，禁止从不受信任的外部来源加载，提高安全性。&lt;/li&gt;
&lt;li&gt;style-src &apos;self&apos; &apos;unsafe-inline&apos;
style-src：指定样式表（CSS）的加载策略。
self：仅允许从同源的资源加载，禁止从不受信任的外部来源加载，提高安全性。
unsafe-inline：允许在 HTML 文档内使用内联样式。&lt;/li&gt;
&lt;li&gt;img-src &apos;self&apos; data:
img-src：指定图像资源的加载策略。
self：表示仅允许从同源加载图像。
data:：允许使用 data: URI 来嵌入图像。这种 URI 模式允许将图像数据直接嵌入到 HTML 或 CSS 中，而不是通过外部链接引用。
关于 CSP 的详细说明请参考：&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Security-Policy&quot;&gt;MDN-Content-Security-Policy&lt;/a&gt;、&lt;a href=&quot;https://www.electronjs.org/docs/latest/tutorial/security&quot;&gt;Electron Security&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;完善窗口行为&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Windows 和 Linux 平台窗⼝特点是：关闭所有窗⼝时退出应⽤。&lt;/li&gt;
&lt;li&gt;mac 应⽤即使在没有打开任何窗⼝的情况下也继续运⾏，并且在没有窗⼝可⽤的情况下激活 应⽤时会打开新的窗⼝。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 main.js 中添加如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { app, BrowserWindow } = require(&quot;electron&quot;);

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true, // 是否隐藏顶部菜单栏
    alwaysOnTop: false, // 是否一直置顶
  });
  win.loadFile(&quot;./pages/index.html&quot;);
}

app.on(&quot;ready&quot;, () =&amp;gt; {
  createWindow();
  app.on(&quot;activate&quot;, () =&amp;gt; {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on(&quot;window-all-closed&quot;, () =&amp;gt; {
  if (process.platform !== &quot;darwin&quot;) app.quit();
});

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置⾃动重启&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;安装 Nodemon&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm i nodemon -D
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;修改 package.json 文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
  &quot;start&quot;: &quot;nodemon --exec electron .&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置 nodemon.json 规则
根目录创建 &lt;code&gt;nodemon.json&lt;/code&gt; 文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;ignore&quot;: [&quot;node_modules&quot;, &quot;dist&quot;],
  &quot;restartable&quot;: &quot;r&quot;,
  &quot;watch&quot;: [&quot;*.*&quot;],
  &quot;ext&quot;: &quot;html,js,css&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;主进程与渲染进程&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;主进程&lt;/strong&gt;
每个 Electron 应⽤都有⼀个单⼀的主进程，作为应⽤程序的⼊⼝点。 主进程在 Node.js 环境中运 ⾏，它具有 require 模块和使⽤所有 Node.js API 的能⼒，主进程的核⼼就是：使用BrowserWindow来创建和管理窗口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;渲染进程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每个 BrowserWindow 实例都对应一个单独的渲染器进程，运行在渲染器进程中的代码，必须遵守网页标准，这也就意味着：&lt;strong&gt;渲染器进程无权直接访问 require 或使用任何 Node.js 的 API&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;问题产生：处于渲染器进程的用户界面，该怎样才与 Node.js 和 Electron 的原生桌面功能进行交互呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Preload 脚本&lt;/h2&gt;
&lt;p&gt;预加载（Preload）脚本是运行在渲染进程中的，但它是在网页内容加载之前执行的，这意味着它具有比普通渲染器代码更高的权限，可以访问 &lt;code&gt;Node.js&lt;/code&gt; 的 API，同时又可以与网页内容进行安全的交互。&lt;/p&gt;
&lt;p&gt;简单说：它是 Node.js 和 Web API 的桥梁，Preload 脚本可以安全地将部分 Node.js 功能暴露给网页，从而减少安全风险。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;预加载脚本应用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;主进程：根目录的main.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { app, BrowserWindow } = require(&quot;electron&quot;);
const path = require(&quot;path&quot;);

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true, // 是否隐藏顶部菜单栏
    alwaysOnTop: false, // 是否一直置顶
    webPreferences: {   // 可以在安全的情况下，把 Node.js / Electron 的 API 暴露给网页。
      preload: path.resolve(__dirname, &quot;./preload.js&quot;),
    },
  });
  win.loadFile(&quot;./pages/index.html&quot;);
}

app.on(&quot;ready&quot;, () =&amp;gt; {
  createWindow();
  app.on(&quot;activate&quot;, () =&amp;gt; {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on(&quot;window-all-closed&quot;, () =&amp;gt; {
  if (process.platform !== &quot;darwin&quot;) app.quit();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根目录下的&lt;code&gt;perload.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { contextBridge } = require(&quot;electron&quot;);  // 从 Electron 引入 contextBridge，它是 上下文桥接 API。
console.log(&quot;proload...&quot;);

// exposeInMainWorld 的意思是：把对象注入到渲染进程的 全局 window 上。
contextBridge.exposeInMainWorld(&quot;myAPI&quot;, {
  version: process.version, // // Node.js 的版本号
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pages/render.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const btn1 = document.getElementById(&quot;btn1&quot;);

btn1.onclick = () =&amp;gt; {
  alert(myAPI.version)
  console.log(window);
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点击按钮后即可看到版本号&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;脚本的执行顺序：主进程-&amp;gt;预加载脚本-&amp;gt;渲染进程 （preload.js 是一个 预加载脚本，它会在 渲染进程加载页面之前运行。）&lt;/li&gt;
&lt;li&gt;预加载脚本可以使用一部分Node的API
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;进程通信（IPC）&lt;/h2&gt;
&lt;p&gt;上文中的 &lt;code&gt;preload.js&lt;/code&gt; ，无法使用全部 Node 的 API ，比如：不能使用 Node 中的 &lt;code&gt;fs 模块&lt;/code&gt;，但主进程（main.js）是可以的，这时就需要&lt;strong&gt;进程通信&lt;/strong&gt;了。简单说：要让 &lt;code&gt;preload.js&lt;/code&gt; 通知 &lt;code&gt;main.js&lt;/code&gt; 去调用 &lt;code&gt;fs 模块&lt;/code&gt;去干活。&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;Electron&lt;/code&gt; 进程通信，我们要知道：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IPC 全称为：&lt;code&gt;InterProcess Communication&lt;/code&gt; ，即：进程通信。&lt;/li&gt;
&lt;li&gt;IPC 是 &lt;code&gt;Electron&lt;/code&gt; 中最为核心的内容，它是从 UI 调用原生 API 的唯一方法！&lt;/li&gt;
&lt;li&gt;Electron 中，主要使用 &lt;code&gt;ipcMain&lt;/code&gt; 和 &lt;code&gt;ipcRenderer&lt;/code&gt; 来定义“通道”，进行进程通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;渲染进程-&amp;gt;主进程(单项)&lt;/h3&gt;
&lt;p&gt;概述: 在渲染器进程中 &lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/ipc-renderer&quot;&gt;ipcRenderer.send&lt;/a&gt; 发送消息,在主进程中使用 &lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/ipc-main&quot;&gt;ipcMain.on&lt;/a&gt; 接收消息。&lt;/p&gt;
&lt;p&gt;常用于: &lt;strong&gt;在 Web 中调用主进程的 API&lt;/strong&gt;,例如下面的这个需求:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需求: 点击按钮后,在用户的 D 盘创建一个 &lt;code&gt;hello.txt&lt;/code&gt; 文件,文件内容来自于用户输入。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思路肯定是：从渲染进程拿到用户输入的信息，想办法通知给主进程，让主进程拿到用户的输入，写入D盘，hello.txt&lt;/p&gt;
&lt;p&gt;页面中添加相关元素，&lt;code&gt;render.js&lt;/code&gt;中添加对应脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Electron demo&amp;lt;/title&amp;gt;
    &amp;lt;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;default-src &apos;self&apos;; style-src &apos;self&apos; &apos;unsafe-inline&apos;; img-src &apos;self&apos; data:;&quot;/&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;./index.css&quot; /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;button id=&quot;btn1&quot;&amp;gt;按钮&amp;lt;/button&amp;gt;
    &amp;lt;hr&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;input id=&quot;input&quot; /&amp;gt;
    &amp;lt;button id=&quot;btn2&quot;&amp;gt;向D盘写入hello.txt&amp;lt;/button&amp;gt;


    &amp;lt;script type=&quot;text/javascript&quot; src=&quot;./render.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const btn1 = document.getElementById(&quot;btn1&quot;);
const btn2 = document.getElementById(&quot;btn2&quot;);
const input = document.getElementById(&quot;input&quot;);

btn1.onclick = () =&amp;gt; {
  alert(myAPI.version);
  console.log(window);
};

btn2.onclick = () =&amp;gt; {
  myAPI.saveFile(input.value);
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;preload.js&lt;/code&gt; 中使用 &lt;code&gt;ipcRenderer.send(&apos;信道&apos;, 参数)&lt;/code&gt; 发送消息，与主进程通信。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { contextBridge, ipcRenderer } = require(&quot;electron&quot;);
console.log(&quot;proload...&quot;);

contextBridge.exposeInMainWorld(&quot;myAPI&quot;, {
  version: process.version,
  saveFile: (data) =&amp;gt; {
    ipcRenderer.send(&quot;file-save&quot;, data); // 渲染进程给主进程发送⼀个消息
  },
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主进程中，在加载页面之前，使用 &lt;code&gt;ipcMain.on(&apos;信道&apos;, 回调)&lt;/code&gt; 配置对应回调函数，接收消息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { app, BrowserWindow, ipcMain } = require(&quot;electron&quot;);
const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);

function writeFile(_, data) {
  fs.writeFileSync(&quot;D:/hello.txt&quot;, data);
}

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true, // 是否隐藏顶部菜单栏
    alwaysOnTop: false, // 是否一直置顶
    webPreferences: {
      preload: path.resolve(__dirname, &quot;./preload.js&quot;),
    },
  });
  ipcMain.on(&quot;file-save&quot;, writeFile);
  win.loadFile(&quot;./pages/index.html&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
主进程你就引入&lt;code&gt;ipcMain&lt;/code&gt;模块，渲染进程你就引入&lt;code&gt;ipcRenderer&lt;/code&gt;模块。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ipcMain.on(channel, listener)&lt;/code&gt; – 监听指定通道的消息，当收到时调用 listener 函数。&lt;code&gt;&quot;file-save&quot;&lt;/code&gt;：通道名，自定义字符串；
writeFile：监听器函数（listener），这是一个回调函数。当消息到达时，Electron 会自动调用 writeFile&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ipcRenderer.send(channel, ...args)&lt;/code&gt; – 向主进程发送异步消息；&lt;code&gt;&quot;file-save&quot;&lt;/code&gt;：通道名，必须与主进程监听的相同。data：发送的数据；
:::&lt;/p&gt;
&lt;h3&gt;渲染进程&amp;lt;=&amp;gt;主进程（双向）&lt;/h3&gt;
&lt;p&gt;概述：渲染进程通过&lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/ipc-renderer#ipcrendererinvokechannel-args&quot;&gt;ipcRenderer.invoke&lt;/a&gt;发送消息，主进程使用&lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/ipc-main#ipcmainhandlechannel-listener&quot;&gt;ipcMain.handle&lt;/a&gt;接收并处理消息&lt;/p&gt;
&lt;p&gt;备注: &lt;code&gt;ipcRender.invoke&lt;/code&gt; 的返回值是 &lt;code&gt;Promise&lt;/code&gt; 实例。&lt;/p&gt;
&lt;p&gt;常用于：&lt;strong&gt;从渲染器进程调用主进程方法并等待结果&lt;/strong&gt;，例如下面的这个需求：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需求：点击按钮从 D 盘读取 hello.txt 中的内容，并将结果呈现在页面上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思路：渲染进程要想办法告诉主进程要读取hello.txt文件，主进程读取文件后，把文件内容返回给渲染进程，渲染进程再把文件内容呈现在页面上。&lt;/p&gt;
&lt;p&gt;页面中添加相关元素，render.js中添加对应脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Electron demo&amp;lt;/title&amp;gt;
    &amp;lt;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;default-src &apos;self&apos;; style-src &apos;self&apos; &apos;unsafe-inline&apos;; img-src &apos;self&apos; data:;&quot;/&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;./index.css&quot; /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;button id=&quot;btn1&quot;&amp;gt;按钮&amp;lt;/button&amp;gt;
    &amp;lt;hr&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;input id=&quot;input&quot; /&amp;gt;
    &amp;lt;button id=&quot;btn2&quot;&amp;gt;向D盘写入hello.txt&amp;lt;/button&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;hr&amp;gt;
    &amp;lt;button id=&quot;btn3&quot;&amp;gt;读取D盘中的hello.txt&amp;lt;/button&amp;gt;

    &amp;lt;script type=&quot;text/javascript&quot; src=&quot;./render.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;preload.js&lt;/code&gt; 中使用 &lt;code&gt;ipcRenderer.invoke(&apos;信道&apos;, 参数)&lt;/code&gt; 发送消息，与主进程通信。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { contextBridge, ipcRenderer } = require(&quot;electron&quot;);
console.log(&quot;proload...&quot;);

contextBridge.exposeInMainWorld(&quot;myAPI&quot;, {
  version: process.version,
  saveFile: (data) =&amp;gt; {
    ipcRenderer.send(&quot;file-save&quot;, data);
  },
   readFile: () =&amp;gt; {
    return ipcRenderer.invoke(&quot;file-read&quot;);
  },
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主进程中，在加载页面之前，使用 &lt;code&gt;ipcMain.handle(&apos;信道&apos;, 回调)&lt;/code&gt; 接收消息，并配置回调函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { app, BrowserWindow, ipcMain } = require(&quot;electron&quot;);
const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);

function readFile() {
  return fs.readFileSync(&quot;D:/hello.txt&quot;).toString();
}


function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true, // 是否隐藏顶部菜单栏
    alwaysOnTop: false, // 是否一直置顶
    webPreferences: {
      preload: path.resolve(__dirname, &quot;./preload.js&quot;),
    },
  });
  ipcMain.on(&quot;file-save&quot;, writeFile);
  ipcMain.handle(&quot;file-read&quot;, readFile);
  win.loadFile(&quot;./pages/index.html&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;code&gt;const result = await ipcRenderer.invoke(channel, ...args);&lt;/code&gt;用于在预加载脚本中发起一个异步请求到主进程，并等待返回 Promise 结果的通信方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;channel (必需): 一个字符串，是消息的通道名称或事件名称。。这个名称必须在主进程中由 &lt;code&gt;ipcMain.handle()&lt;/code&gt;进行监听。&lt;/p&gt;
&lt;p&gt;...args (可选): 一个或多个任意类型的参数，会作为参数传递给主进程中的处理函数。&lt;/p&gt;
&lt;p&gt;返回值: 返回一个 Promise。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;ipcMain.handle(&apos;channel-name&apos;, handler)&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参数1：channel（通道名），类型：string；标识这个处理器监听哪个“频道”。必须与渲染进程 ipcRenderer.invoke(channel, ...) 中的 channel 完全一致。&lt;/p&gt;
&lt;p&gt;参数2：listener（监听器函数），类型：Function；当对应 channel 的请求到达时，执行这个函数。
:::&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::warning
渲染进程与渲染进程之间是不能传东西的，可以用主进程作为中间人
:::&lt;/p&gt;
&lt;h2&gt;打包应用&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;electron-builder&lt;/code&gt; 打包应用&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装 electron-builder：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm install electron-builder -D
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在 package.json 中进行相关配置，具体配置如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;备注: json 文件不支持注释, 使用时请去掉所有注释。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;video-tools&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;main&quot;: &quot;main.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;electron .&quot;,
    &quot;build&quot;: &quot;electron-builder&quot;
  },
  &quot;build&quot;: {
    &quot;appId&quot;: &quot;com.atguigu.video&quot;,
    &quot;win&quot;: {
      &quot;icon&quot;: &quot;./logo.ico&quot;,
      &quot;target&quot;: [
        {
          &quot;target&quot;: &quot;nsis&quot;,
          &quot;arch&quot;: [
            &quot;x64&quot;
          ]
        }
      ]
    },
    &quot;nsis&quot;: {
      &quot;oneClick&quot;: false,
      &quot;perMachine&quot;: true,
      &quot;allowToChangeInstallationDirectory&quot;: true
    }
  },
  &quot;devDependencies&quot;: {
    &quot;electron&quot;: &quot;^30.0.0&quot;,
    &quot;electron-builder&quot;: &quot;^24.13.3&quot;,
    &quot;author&quot;: &quot;tianyu&quot;,
    &quot;license&quot;: &quot;ISC&quot;,
    &quot;description&quot;: &quot;A video processing program based on Electron&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;注释版本&amp;lt;/summary&amp;gt;
```
{
&quot;name&quot;: &quot;video-tools&quot;, // 应⽤程序的名称
&quot;version&quot;: &quot;1.0.0&quot;, // 应⽤程序的版本
&quot;main&quot;: &quot;main.js&quot;, // 应⽤程序的⼊⼝⽂件
&quot;scripts&quot;: {
&quot;start&quot;: &quot;electron .&quot;, // 使⽤ &lt;code&gt;electron .&lt;/code&gt; 命令启动应⽤程序
&quot;build&quot;: &quot;electron-builder&quot; // 使⽤ &lt;code&gt;electron-builder&lt;/code&gt; 打包应⽤程序，⽣成 安装包
},
&quot;build&quot;: {
&quot;appId&quot;: &quot;com.atguigu.video&quot;, // 应⽤程序的唯⼀标识符
// 打包windows平台安装包的具体配置
&quot;win&quot;: {
&quot;icon&quot;:&quot;./logo.ico&quot;, //应⽤图标
&quot;target&quot;: [
{
&quot;target&quot;: &quot;nsis&quot;, // 指定使⽤ NSIS 作为安装程序格式
&quot;arch&quot;: [&quot;x64&quot;] // ⽣成 64 位安装包
}
]
},
&quot;nsis&quot;: {
&quot;oneClick&quot;: false, // 设置为 &lt;code&gt;false&lt;/code&gt; 使安装程序显示安装向导界⾯，⽽不是⼀ 键安装
&quot;perMachine&quot;: true, // 允许每台机器安装⼀次，⽽不是每个⽤户都安装
&quot;allowToChangeInstallationDirectory&quot;: true // 允许⽤户在安装过程中选择 安装⽬录
}
},
&quot;devDependencies&quot;: {
&quot;electron&quot;: &quot;^30.0.0&quot;, // 开发依赖中的 Electron 版本
&quot;electron-builder&quot;: &quot;^24.13.3&quot; // 开发依赖中的 &lt;code&gt;electron-builder&lt;/code&gt; 版本 },
&quot;author&quot;: &quot;tianyu&quot;, // 作者信息
&quot;license&quot;: &quot;ISC&quot;, // 许可证信息
&quot;description&quot;: &quot;A video processing program based on Electron&quot; // 应⽤程 序的描述
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;/details&amp;gt;

拿一个script中的`&quot;build&quot;: &quot;electron-builder&quot;`以及`build`中内容
最终`package.json`内容如下
```json
{
  &quot;name&quot;: &quot;electron_test&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;main&quot;: &quot;main.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;nodemon --exec electron .&quot;,
    &quot;build&quot;: &quot;electron-builder&quot;
  },
  &quot;build&quot;: {
    &quot;appId&quot;: &quot;com.atguigu.video&quot;,
    &quot;win&quot;: {
      &quot;icon&quot;: &quot;./logo.png&quot;,
      &quot;target&quot;: [
        {
          &quot;target&quot;: &quot;nsis&quot;,
          &quot;arch&quot;: [
            &quot;x64&quot;
          ]
        }
      ]
    },
    &quot;nsis&quot;: {
      &quot;oneClick&quot;: false,
      &quot;perMachine&quot;: true,
      &quot;allowToChangeInstallationDirectory&quot;: true
    }
  },
  &quot;author&quot;: &quot;zzy&quot;,
  &quot;license&quot;: &quot;ISC&quot;,
  &quot;description&quot;: &quot;this is a electron demo&quot;,
  &quot;devDependencies&quot;: {
    &quot;electron&quot;: &quot;^38.1.0&quot;,
    &quot;electron-builder&quot;: &quot;^26.0.12&quot;,
    &quot;nodemon&quot;: &quot;^3.1.10&quot;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;执行打包命令&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果打包报错，我们可以开启windows的&lt;strong&gt;开发人员模式&lt;/strong&gt;，系统-&amp;gt;开发人员模式；然后再执行打包命令就可以；&lt;/p&gt;
&lt;p&gt;打包后的exe文件就在dist包下；&lt;/p&gt;
&lt;h2&gt;electron-vite&lt;/h2&gt;
&lt;p&gt;electron-vite 是⼀个新型构建⼯具，旨在为 Electron 提供更快、更精简的体验&lt;/p&gt;
&lt;p&gt;electron-vite 快速、简单且功能强⼤，旨在开箱即⽤。 官⽹地址：https://cn-evite.netlify.app/&lt;/p&gt;
&lt;h2&gt;项目实战&lt;/h2&gt;
&lt;h3&gt;初始化项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm init vite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;npm i electron -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除 package.json 中的 &quot;type&quot;: &quot;module&quot;。改用 CommonJS 规范&lt;/p&gt;
&lt;p&gt;根目录创建&lt;code&gt;main.js&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { app, BrowserWindow } = require(&quot;electron&quot;);
const createWindow = () =&amp;gt; {
  const win = new BrowserWindow({
    width: 1000,
    height: 800,
  });
};

app.whenReady().then(() =&amp;gt; {
  createWindow();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改package.json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;main&quot;: &quot;main.js&quot;,
&quot;scripts&quot;: {
  &quot;start&quot;: &quot;electron .&quot;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;安装nodemon&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i nodemon -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改package.json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
  &quot;start&quot;: &quot;nodemon --exec electron . --watch ./ --ext .js,.html,.css,.vue&quot;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;去掉CSP(Content- Security-Policy)警告，在index.html添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;default-src &apos;self&apos;; style-src &apos;self&apos; &apos;unsafe-inline&apos;; img-src &apos;self&apos; data:;&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/hwytree/article/details/121287531&quot;&gt;解决警告&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;安装&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i electron-win-state -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根目录创建preload目录，再创建&lt;code&gt;index.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终根目录下的main.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { app, BrowserWindow } = require(&quot;electron&quot;);
const WinState = require(&quot;electron-win-state&quot;).default;
const path = require(&quot;path&quot;);

const createWindow = () =&amp;gt; {
  const winState = new WinState({
    defaultWidth: 1000,
    defaultHeight: 800,
  });

  const win = new BrowserWindow({
    ...winState.winOptions,
    webPreferences: {
      preload: path.resolve(__dirname, &quot;./preload/index.js&quot;),
    },

  });

  win.loadURL(&quot;http://localhost:3000&quot;);
  win.webContents.openDevTools(); // 打开控制台
  winState.manage(win);
};

app.whenReady().then(() =&amp;gt; {
  createWindow();

  app.on(&quot;activate&quot;, () =&amp;gt; {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on(&apos;window-all-closed&apos;, () =&amp;gt; {
  if (process.platform !== &apos;darwin&apos;) app.quit()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;package.json&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;pin-box&quot;,
  &quot;private&quot;: true,
  &quot;version&quot;: &quot;0.0.0&quot;,
  &quot;main&quot;: &quot;main.js&quot;,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite&quot;,
    &quot;build&quot;: &quot;vite build&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;,
    &quot;start&quot;: &quot;nodemon --exec electron . --watch ./ --ext .js,.html,.css,.vue&quot;
  },
  &quot;dependencies&quot;: {
    &quot;vue&quot;: &quot;^3.5.13&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@types/electron&quot;: &quot;^1.4.38&quot;,
    &quot;@types/nodemon&quot;: &quot;^1.19.6&quot;,
    &quot;@vitejs/plugin-vue&quot;: &quot;^5.2.1&quot;,
    &quot;electron&quot;: &quot;^38.1.0&quot;,
    &quot;electron-win-state&quot;: &quot;^1.1.22&quot;,
    &quot;nodemon&quot;: &quot;^3.1.10&quot;,
    &quot;vite&quot;: &quot;^6.0.1&quot;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;启动项目&lt;/strong&gt;
启动vite项目&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动electron&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;main.js中的load地址为vite项目的启动地址&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优雅打开窗口&lt;/strong&gt;
&lt;code&gt;main.js&lt;/code&gt;中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { app, BrowserWindow } = require(&quot;electron&quot;);
const WinState = require(&quot;electron-win-state&quot;).default;
const path = require(&quot;path&quot;);

const createWindow = () =&amp;gt; {
  const winState = new WinState({
    defaultWidth: 1000,
    defaultHeight: 800,
  });

  const win = new BrowserWindow({
    ...winState.winOptions,
    webPreferences: {
      preload: path.resolve(__dirname, &quot;./preload/index.js&quot;),
    },
    show: false,
  });

  win.loadURL(&quot;http://localhost:5173&quot;);
  win.webContents.openDevTools(); // 打开控制台
  winState.manage(win);

  win.on(&quot;ready-to-show&quot;, () =&amp;gt; {
    win.show();
  });
};

app.whenReady().then(() =&amp;gt; {
  createWindow();

  app.on(&quot;activate&quot;, () =&amp;gt; {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on(&quot;window-all-closed&quot;, () =&amp;gt; {
  if (process.platform !== &quot;darwin&quot;) app.quit();
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;整理vue项目&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;删除components目录下的所有文件&lt;/p&gt;
&lt;p&gt;修改App.vue&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    hello
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, reactive, toRefs, onMounted} from &apos;vue&apos;

&amp;lt;/script&amp;gt;

&amp;lt;style scoped lang=&quot;scss&quot;&amp;gt;
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;src下创建views文件夹，再创建&lt;code&gt;Home.vue&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;安装stylus或者安装sass&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;应用更新&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;electron 应用更新-基于github&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;生成github token：  https://github.com/settings/tokens/new&lt;/p&gt;
&lt;p&gt;修改配置文件&lt;code&gt;package.json&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;主要是&lt;code&gt;build&lt;/code&gt;和&lt;code&gt;release&lt;/code&gt;命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;electron_test&quot;,
  &quot;version&quot;: &quot;1.0.7&quot;,
  &quot;main&quot;: &quot;main.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;nodemon --exec electron .&quot;,
    &quot;build&quot;: &quot;electron-builder&quot;,
    &quot;release&quot;: &quot;electron-builder  --win -p always&quot;
  },
  &quot;build&quot;: {
    &quot;appId&quot;: &quot;com.zzyang.video&quot;,
    &quot;publish&quot;: {
      &quot;provider&quot;: &quot;github&quot;,
      &quot;token&quot;: &quot;你的token&quot;,
      &quot;owner&quot;: &quot;zxyang3636&quot;,
      &quot;private&quot;: true,
      &quot;releaseType&quot;: &quot;release&quot;,
      &quot;repo&quot;: &quot;electron_test&quot;
    },
    &quot;win&quot;: {
      &quot;icon&quot;: &quot;./logo.png&quot;,
      &quot;target&quot;: [
        {
          &quot;target&quot;: &quot;nsis&quot;,
          &quot;arch&quot;: [
            &quot;x64&quot;
          ]
        }
      ]
    },
    &quot;nsis&quot;: {
      &quot;oneClick&quot;: false,
      &quot;perMachine&quot;: true,
      &quot;allowToChangeInstallationDirectory&quot;: true
    }
  },
  &quot;author&quot;: &quot;zzy&quot;,
  &quot;license&quot;: &quot;ISC&quot;,
  &quot;description&quot;: &quot;this is a electron demo&quot;,
  &quot;devDependencies&quot;: {
    &quot;electron&quot;: &quot;^38.1.0&quot;,
    &quot;electron-builder&quot;: &quot;^26.0.12&quot;,
    &quot;nodemon&quot;: &quot;^3.1.10&quot;
  },
  &quot;dependencies&quot;: {
    &quot;electron-log&quot;: &quot;^5.4.3&quot;,
    &quot;electron-updater&quot;: &quot;^6.6.2&quot;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;releaseType: // “draft” | “prerelease” | “release” | “undefined” 默认草稿状态&lt;/p&gt;
&lt;p&gt;repo 你的仓库名称&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://github.com/frontierFlight/fe-content-sharing/blob/main/electron/%E5%BA%94%E7%94%A8%E6%9B%B4%E6%96%B0.md&quot;&gt;github&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>效率开发</title><link>https://zzyang.top/posts/efficiency-dev/</link><guid isPermaLink="true">https://zzyang.top/posts/efficiency-dev/</guid><pubDate>Wed, 08 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;个人整理和日常使用的工具、网站和软件清单。希望能帮助你发现新的生产力工具，提升效率！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;终端与服务器、数据库管理工具&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具名称&lt;/th&gt;
&lt;th&gt;核心功能&lt;/th&gt;
&lt;th&gt;访问/下载&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WindTerm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全平台 SSH 客户端，支持高速上传、命令记忆。&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://kingtoolbox.github.io/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Termora&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全新高颜值SSH客户端&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.termora.app/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tiny RDM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一个更现代化的Redis桌面管理客户端&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://redis.tinycraft.cc/zh/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RedisDesktopManager&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis可视化管理工具&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/redis/RedisDesktopManager/releases/tag/0.9.3&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DataGrip&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;最全数据库开发必备神器&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.jetbrains.com/zh-cn/datagrip/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HexHub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HexHub 为程序员和运维人员量身打造(数据库，服务器，docker)&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.hexhub.cn/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;开发相关&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具名称&lt;/th&gt;
&lt;th&gt;核心功能&lt;/th&gt;
&lt;th&gt;访问/下载&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;json editor online&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在线json格式化 支持超复杂json&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://jsoneditoronline.org/#left=local.qoxuto&amp;amp;right=local.vagafa&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IT-TOOLS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;助力开发人员和 IT 工作者&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://tools.ytdevops.com/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;加速服务&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;docker/github加速&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://demo.52013120.xyz/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DockerHub 国内镜像源&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;加速列表&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://blog.xuanyuan.me/archives/1154&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lorem Picsum&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;随机图片，可设置大小&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://picsum.photos/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;免费图床&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;图床工具&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://iui.su/pic.html&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;nginx配置工具&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在线nginx可视化配置&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.digitalocean.com/community/tools/nginx&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;windows 相关&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具名称&lt;/th&gt;
&lt;th&gt;核心功能&lt;/th&gt;
&lt;th&gt;访问/下载&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Windhawk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;针对windows定制的插件&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://windhawk.net/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kite 待办&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;极简主义的待办应用&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://kite.kitlib.cn/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Optimizer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;最好的Windows优化器(禁用更新等)&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/hellzerg/optimizer&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;小旺AI截图&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;截图录屏翻译提取文本&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.xiaowang.com/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;摇摇鼠&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;保持电脑的工作状态&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://yys.tanpok.com/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;装个机&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不求人，自己装 💻🧰&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zhuangit.ababtools.com/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;实用工具&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具名称&lt;/th&gt;
&lt;th&gt;核心功能&lt;/th&gt;
&lt;th&gt;访问/下载&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF24 Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;免费且易于使用的在线PDF工具&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://tools.pdf24.org/zh/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CloudConvert&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在线文件转换器&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://cloudconvert.com/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;imgdiet&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在线图片处理工具&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.imgdiet.com/zh-CN&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;图片压缩&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;谷歌图片压缩&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://squoosh.app/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;其他&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具名称&lt;/th&gt;
&lt;th&gt;核心功能&lt;/th&gt;
&lt;th&gt;访问/下载&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Luxirty Search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基于 Google，屏蔽内容农场，无广告，无跟踪&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://search.luxirty.com/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;excalidraw&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在线白板绘图&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://excalidraw.com/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;drawnix&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;开源白板&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://drawnix.com/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cpolar&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;内网穿透&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://dashboard.cpolar.com/login&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UP简历&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI书写简历&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://upcv.tech/create&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;简历模板&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;免费的简历模板&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://cv-template.online/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;好文推荐&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.wolai.com/hjTNCjSczr8VbMsoqA76ED&quot;&gt;产品需求文档写作指南&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/q-rWq79HmzPe08gyfOjaIA&quot;&gt;最通俗易懂的 HashMap 源码分析解读&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://learn.lianglianglee.com/&quot;&gt;技术摘抄&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;&amp;lt;strong&amp;gt;🏞️&amp;lt;/strong&amp;gt;&amp;lt;/summary&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具名称&lt;/th&gt;
&lt;th&gt;核心功能&lt;/th&gt;
&lt;th&gt;访问/下载&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;柳橙昔网址导航&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;实用的导航站&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://nav.yiov.top/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;阿虚同学的储物间&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一个储物间&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://axutongxue.com/&quot;&gt;🔗 官网&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL - 索引的数据结构</title><link>https://zzyang.top/posts/mysql-idxdatastruct/</link><guid isPermaLink="true">https://zzyang.top/posts/mysql-idxdatastruct/</guid><pubDate>Sat, 20 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;索引的数据结构&lt;/h2&gt;
&lt;h3&gt;为什么使用索引&lt;/h3&gt;
&lt;p&gt;索引是存储引擎用于快速找到数据记录的一种数据结构，就好比一本教科书的目录部分，通过目录中找到对应文章的页码，便可快速定位到需要的文章。MySQL中也是一样的道理，进行数据查找时，首先查看查询条件是否命中某条索引，符合则&lt;code&gt;通过索引查找&lt;/code&gt;相关数据，如果不符合则需要&lt;code&gt;全表扫描&lt;/code&gt;，即需要一条一条地查找记录，直到找到与条件符合的记录。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/wechat_2025-07-17_215138_204.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，数据库没有索引的情况下，数据&lt;code&gt;分布在硬盘不同的位置上面&lt;/code&gt;，读取数据时，摆臂需要前后摆动查询数据，这样操作非常消耗时间。如果&lt;code&gt;数据顺序摆放&lt;/code&gt;，那么也需要从1到6行按顺序读取，这样就相当于进行了6次IO操作，&lt;code&gt;依旧非常耗时&lt;/code&gt;。如果我们不借助任何索引结构帮助我们快速定位数据的话，我们查找 Col 2 = 89 这条记录，就要逐行去查找、去比较。从Col 2 = 34 开始，进行比较，发现不是，继续下一行。我们当前的表只有不到10行数据，但如果表很大的话，有上千万条数据，就意味着要做&lt;code&gt;很多很多次硬盘I/0&lt;/code&gt;才能找到。现在要查找 Col 2 = 89 这条记录。CPU必须先去磁盘查找这条记录，找到之后加载到内存，再对数据进行处理。这个过程最耗时间就是磁盘I/O（涉及到磁盘的旋转时间（速度较快），磁头的寻道时间(速度慢、费时)）&lt;/p&gt;
&lt;p&gt;假如给数据使用 &lt;code&gt;二叉树&lt;/code&gt; 这样的数据结构进行存储，如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_21-52-44.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对字段 Col 2 添加了索引，就相当于在硬盘上为 Col 2 维护了一个索引的数据结构，即这个 &lt;code&gt;二叉搜索树&lt;/code&gt;。二叉搜索树的每个结点存储的是 &lt;code&gt;(K, V) 结构&lt;/code&gt;，key 是 Col 2，value 是该 key 所在行的文件指针（地址）。比如：该二叉搜索树的根节点就是：&lt;code&gt;(34, 0x07)&lt;/code&gt;。现在对 Col 2 添加了索引，这时再去查找 Col 2 = 89 这条记录的时候会先去查找该二叉搜索树（二叉树的遍历查找）。读 34 到内存，89 &amp;gt; 34; 继续右侧数据，读 89 到内存，89==89；找到数据返回。找到之后就根据当前结点的 value 快速定位到要查找的记录对应的地址。我们可以发现，只需要 &lt;code&gt;查找两次&lt;/code&gt; 就可以定位到记录的地址，查询速度就提高了。&lt;/p&gt;
&lt;p&gt;这就是我们为什么要建索引，目的就是为了 &lt;code&gt;减少磁盘I/O的次数&lt;/code&gt;，加快查询速率。&lt;/p&gt;
&lt;h3&gt;索引及其优缺点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;索引概述&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MySQL官方对索引的定义为：&lt;strong&gt;索引（Index）是帮助MySQL高效获取数据的数据结构&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;索引的本质&lt;/strong&gt;：索引是数据结构。你可以简单理解为“排好序的快速查找数据结构”，满足特定查找算法。 这些数据结构以某种方式指向数据， 这样就可以在这些数据结构的基础上实现 &lt;code&gt;高级查找算法&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;索引是在存储引擎中实现的&lt;/code&gt;，因此每种存储引擎的索引不一定完全相同，并且每种存储引擎不一定支持所有索引类型。同时，存储引擎可以定义每个表的 &lt;code&gt;最大索引数&lt;/code&gt; 和 &lt;code&gt;最大索引长度&lt;/code&gt;。所有存储引擎支持每个表至少16个索引，总索引长度至少为256字节。有些存储引擎支持更多的索引数和更大的索引长度。&lt;/p&gt;
&lt;h4&gt;优点&lt;/h4&gt;
&lt;p&gt;（1）类似大学图书馆建书目索引，提高数据检索的效率，&lt;strong&gt;降低数据库的IO成本&lt;/strong&gt; ，这也是创建索引最主 要的原因。&lt;/p&gt;
&lt;p&gt;（2）通过创建唯一索引，可以保证数据库表中每一行&lt;strong&gt;数据的唯一性&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;（3）在实现数据的 参考完整性方面，可以&lt;strong&gt;加速表和表之间的连接&lt;/strong&gt; 。换句话说，对于有依赖关系的子表和父表联合查询时， 可以提高查询速度。&lt;/p&gt;
&lt;p&gt;（4）在使用分组和排序子句进行数据查询时，可以显著&lt;strong&gt;减少查询中分组和排序的时间&lt;/strong&gt; ，降低了CPU的消耗。&lt;/p&gt;
&lt;h4&gt;缺点&lt;/h4&gt;
&lt;p&gt;增加索引也有许多不利的方面，主要表现在如下几个方面：&lt;/p&gt;
&lt;p&gt;（1）创建索引和维护索引要 &lt;strong&gt;耗费时间&lt;/strong&gt; ，并 且随着数据量的增加，所耗费的时间也会增加。&lt;/p&gt;
&lt;p&gt;（2）索引需要占 &lt;strong&gt;磁盘空间&lt;/strong&gt; ，除了数据表占数据空间之 外，每一个索引还要占一定的物理空间， 存储在磁盘上 ，如果有大量的索引，索引文件就可能比数据文 件更快达到最大文件尺寸。&lt;/p&gt;
&lt;p&gt;（3）虽然索引大大提高了查询速度，同时却会 &lt;strong&gt;降低更新表的速度&lt;/strong&gt; 。当对表 中的数据进行增加、删除和修改的时候，索引也要动态地维护，这样就降低了数据的维护速度。 因此，选择使用索引时，需要综合考虑索引的优点和缺点。&lt;/p&gt;
&lt;p&gt;因此，选择使用索引时，需要综合考虑索引的优点和缺点。&lt;/p&gt;
&lt;p&gt;:::tip
提示：&lt;/p&gt;
&lt;p&gt;索引可以提高查询的速度，但是会影响插入记录的速度。这种情况下，最好的办法是先删除表中的索引，然后插入数据，插入完成后再创建索引。
:::&lt;/p&gt;
&lt;h2&gt;InnoDB中索引的推演&lt;/h2&gt;
&lt;h3&gt;索引之前的查找&lt;/h3&gt;
&lt;p&gt;先来看一个精确匹配的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT [列名列表] FROM 表名 WHERE 列名 = xxx;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1. 在一个页中的查找&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设目前表中的记录比较少，所有的记录都可以被存放到一个页中，在查找记录的时候可以根据搜索条件的不同分为两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;以主键为搜索条件&lt;/p&gt;
&lt;p&gt;可以在页目录中使用 &lt;strong&gt;二分法&lt;/strong&gt; 快速定位到对应的槽，然后再遍历该槽对用分组中的记录即可快速找到指定记录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以其他列作为搜索条件&lt;/p&gt;
&lt;p&gt;因为在数据页中并没有对非主键列简历所谓的页目录，所以我们无法通过二分法快速定位相应的槽。这种情况下只能从 &lt;strong&gt;最小记录&lt;/strong&gt; 开始 &lt;strong&gt;依次遍历单链表中的每条记录&lt;/strong&gt;， 然后对比每条记录是不是符合搜索条件。很显然，这种查找的效率是非常低的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在很多页中查找
在很多页中查找记录的活动可以分为两个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定位到记录所在的页。&lt;/li&gt;
&lt;li&gt;从所在的页内中查找相应的记录。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在没有索引的情况下，不论是根据主键列或者其他列的值进行查找，由于我们并不能快速的定位到记录所在的页，所以只能 &lt;code&gt;从第一个页&lt;/code&gt;沿着&lt;code&gt;双向链表&lt;/code&gt; 一直往下找，在每一个页中根据我们上面的查找方式去查 找指定的记录。因为要遍历所有的数据页，所以这种方式显然是 &lt;code&gt;超级耗时&lt;/code&gt; 的。如果一个表有一亿条记录呢？此时 &lt;code&gt;索引&lt;/code&gt; 应运而生。&lt;/p&gt;
&lt;h3&gt;设计索引&lt;/h3&gt;
&lt;p&gt;建一个表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; CREATE TABLE index_demo(
-&amp;gt; c1 INT,
-&amp;gt; c2 INT,
-&amp;gt; c3 CHAR(1),
-&amp;gt; PRIMARY KEY(c1)
-&amp;gt; ) ROW_FORMAT = Compact;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个新建的 &lt;code&gt;index_demo&lt;/code&gt; 表中有2个INT类型的列，1个CHAR(1)类型的列，而且我们规定了c1列为主键， 这个表使用 &lt;code&gt;Compact&lt;/code&gt; 行格式来实际存储记录的。这里我们简化了index_demo表的行格式示意图：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_22-45-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们只在示意图里展示记录的这几个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;record_type&lt;/code&gt; ：记录头信息的一项属性，表示记录的类型， &lt;code&gt;0&lt;/code&gt; 表示普通记录、 &lt;code&gt;2&lt;/code&gt; 表示最小记录、 &lt;code&gt;3&lt;/code&gt; 表示最大记录、 &lt;code&gt;1&lt;/code&gt; 暂时还没用过，下面讲。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next_record&lt;/code&gt; ：记录头信息的一项属性，表示下一条地址相对于本条记录的地址偏移量，我们用 箭头来表明下一条记录是谁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;各个列的值&lt;/code&gt; ：这里只记录在 &lt;code&gt;index_demo&lt;/code&gt; 表中的三个列，分别是 &lt;code&gt;c1&lt;/code&gt; 、 &lt;code&gt;c2&lt;/code&gt; 和 &lt;code&gt;c3&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;其他信息&lt;/code&gt; ：除了上述3种信息以外的所有信息，包括其他隐藏列的值以及记录的额外信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将记录格式示意图的其他信息项暂时去掉并把它竖起来的效果就是这样：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_22-47-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;把一些记录放到页里的示意图就是：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_22-48-06.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;一个简单的索引设计方案&lt;/h4&gt;
&lt;p&gt;我们在根据某个搜索条件查找一些记录时为什么要遍历所有的数据页呢？因为各个页中的记录并没有规律，我们并不知道我们的搜索条件匹配哪些页中的记录，所以不得不依次遍历所有的数据页。所以如果我们 &lt;code&gt;想快速的定位到需要查找的记录在哪些数据页&lt;/code&gt; 中该咋办？我们可以为快速定位记录所在的数据页而&lt;code&gt;建立一个目录&lt;/code&gt; ，建这个目录必须完成下边这些事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给所有的页建立一个目录项。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设：每个数据结构最多能存放3条记录（实际上一个数据页非常大，可以存放下好多记录）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO index_demo VALUES(1, 4, &apos;u&apos;), (3, 9, &apos;d&apos;), (5, 3, &apos;y&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么这些记录以及按照主键值的大小串联成一个单向链表了，如图所示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_22-53-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从图中可以看出来， &lt;code&gt;index_demo&lt;/code&gt; 表中的3条记录都被插入到了编号为10的数据页中了。此时我们再来插入一条记录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO index_demo VALUES(4, 4, &apos;a&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 &lt;strong&gt;页10&lt;/strong&gt; 最多只能放3条记录，所以我们不得不再分配一个新页：这个过程称为 &lt;code&gt;页分裂&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_22-55-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意：新分配的 &lt;strong&gt;数据页编号可能并不是连续的&lt;/strong&gt;。它们只是通过维护者上一个页和下一个页的编号而建立了 &lt;code&gt;链表&lt;/code&gt; 关系。另外，&lt;strong&gt;页10&lt;/strong&gt;中用户记录最大的主键值是5，而页28中有一条记录的主键值是4，因为5&amp;gt;4，所以这就不符合下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值的要求，所以在插入主键值为4的记录的时候需要伴随着一次 &lt;code&gt;记录移动&lt;/code&gt;，也就是把主键值为5的记录移动到页28中，然后再把主键值为4的记录插入到页10中，这个过程的示意图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_22-57-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个过程表明了在对页中的记录进行增删改查操作的过程中，我们必须通过一些诸如 &lt;code&gt;记录移动&lt;/code&gt; 的操作来始终保证这个状态一直成立：下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;给所有的页建立一个目录项。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于数据页的 &lt;strong&gt;编号可能是不连续&lt;/strong&gt; 的，所以在向 index_demo 表中插入许多条记录后，可能是这样的效果：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_22-59-17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们需要给它们做个 目录，每个页对应一个目录项，每个目录项包括下边两个部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1）页的用户记录中最小的主键值，我们用 key 来表示。

2）页号，我们用 page_on 表示。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们为上边几个页做好的目录就像这样子:
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_22-51-12.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以 页28 为例，它对应 目录项2 ，这个目录项中包含着该页的页号 28 以及该页中用户记录的最小主 键值 5 。我们只需要把几个目录项在物理存储器上连续存储（比如：数组），就可以实现根据主键 值快速查找某条记录的功能了。比如：查找主键值为 20 的记录，具体查找过程分两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先从目录项中根据 二分法 快速确定出主键值为 20 的记录在 目录项3 中（因为 12 &amp;lt; 20 &amp;lt; 209 ），它对应的页是 页9 。&lt;/li&gt;
&lt;li&gt;再根据前边说的在页中查找记录的方式去 页9 中定位具体的记录。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，针对数据页做的简易目录就搞定了。这个目录有一个别名，称为 &lt;code&gt;索引&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;InnoDB中的索引方案&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;① 迭代1次：目录项纪录的页&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;InnoDB怎么区分一条记录是普通的 &lt;strong&gt;用户记录&lt;/strong&gt; 还是 &lt;strong&gt;目录项记录&lt;/strong&gt; 呢？使用记录头信息里的 &lt;code&gt;record_type&lt;/code&gt; 属性，它的各自取值代表的意思如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;0：普通的用户记录&lt;/li&gt;
&lt;li&gt;1：目录项记录&lt;/li&gt;
&lt;li&gt;2：最小记录&lt;/li&gt;
&lt;li&gt;3：最大记录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们把前边使用到的目录项放到数据页中的样子就是这样：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_23-25-01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从图中可以看出来，我们新分配了一个编号为30的页来专门存储目录项记录。这里再次强调 &lt;strong&gt;目录项记录&lt;/strong&gt; 和普通的 &lt;strong&gt;用户记录&lt;/strong&gt; 的不同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目录项记录&lt;/strong&gt; 的 record_type 值是1，而 &lt;strong&gt;普通用户记录&lt;/strong&gt; 的 record_type 值是0。&lt;/li&gt;
&lt;li&gt;目录项记录只有 &lt;strong&gt;主键值和页的编号&lt;/strong&gt; 两个列，而普通的用户记录的列是用户自己定义的，可能包含 &lt;strong&gt;很多列&lt;/strong&gt; ，另外还有InnoDB自己添加的隐藏列。&lt;/li&gt;
&lt;li&gt;了解：记录头信息里还有一个叫 min_rec_mask 的属性，只有在存储 &lt;strong&gt;目录项记录&lt;/strong&gt; 的页中的主键值最小的 &lt;strong&gt;目录项记录&lt;/strong&gt; 的 min_rec_mask 值为 1 ，其他别的记录的 min_rec_mask 值都是 0 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;相同点&lt;/strong&gt;：两者用的是一样的数据页，都会为主键值生成 &lt;strong&gt;Page Directory （页目录）&lt;/strong&gt;，从而在按照主键值进行查找时可以使用 &lt;code&gt;二分法&lt;/code&gt; 来加快查询速度。&lt;/p&gt;
&lt;p&gt;现在以查找主键为 20 的记录为例，根据某个主键值去查找记录的步骤就可以大致拆分成下边两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先到存储 目录项记录 的页，也就是页30中通过 二分法 快速定位到对应目录项，因为 12 &amp;lt; 20 &amp;lt; 209 ，所以定位到对应的记录所在的页就是页9。&lt;/li&gt;
&lt;li&gt;再到存储用户记录的页9中根据 二分法 快速定位到主键值为 20 的用户记录。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;② 迭代2次：多个目录项纪录的页&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_23-29-26.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从图中可以看出，我们插入了一条主键值为320的用户记录之后需要两个新的数据页：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为存储该用户记录而新生成了 &lt;code&gt;页31&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;因为原先存储目录项记录的 &lt;code&gt;页30的容量已满&lt;/code&gt; （我们前边假设只能存储4条目录项记录），所以不得 不需要一个新的 &lt;code&gt;页32&lt;/code&gt; 来存放 &lt;code&gt;页31&lt;/code&gt; 对应的目录项。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在因为存储目录项记录的页不止一个，所以如果我们想根据主键值查找一条用户记录大致需要3个步骤，以查找主键值为 &lt;code&gt;20&lt;/code&gt; 的记录为例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确定 目录项记录页 我们现在的存储目录项记录的页有两个，即 &lt;code&gt;页30&lt;/code&gt; 和 &lt;code&gt;页32&lt;/code&gt; ，又因为页30表示的目录项的主键值的 范围是 &lt;code&gt;[1, 320)&lt;/code&gt; ，页32表示的目录项的主键值不小于 &lt;code&gt;320&lt;/code&gt; ，所以主键值为 &lt;code&gt;20&lt;/code&gt; 的记录对应的目 录项记录在 &lt;code&gt;页30&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;通过目录项记录页 &lt;code&gt;确定用户记录真实所在的页&lt;/code&gt; 。 在一个存储 &lt;code&gt;目录项记录&lt;/code&gt; 的页中通过主键值定位一条目录项记录的方式说过了。&lt;/li&gt;
&lt;li&gt;在真实存储用户记录的页中定位到具体的记录。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;③ 迭代3次：目录项记录页的目录页&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果我们表中的数据非常多则会&lt;code&gt;产生很多存储目录项记录的页&lt;/code&gt;，那我们怎么根据主键值快速定位一个存储目录项记录的页呢？那就为这些存储目录项记录的页再生成一个&lt;code&gt;更高级的目录&lt;/code&gt;，就像是一个多级目录一样，&lt;code&gt;大目录里嵌套小目录&lt;/code&gt;，小目录里才是实际的数据，所以现在各个页的示意图就是这样子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_23-33-17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如图，我们生成了一个存储更高级目录项的 &lt;code&gt;页33&lt;/code&gt; ，这个页中的两条记录分别代表页30和页32，如果用 户记录的主键值在 &lt;code&gt;[1, 320)&lt;/code&gt; 之间，则到页30中查找更详细的目录项记录，如果主键值 &lt;code&gt;不小于320&lt;/code&gt; 的 话，就到页32中查找更详细的目录项记录。&lt;/p&gt;
&lt;p&gt;我们可以用下边这个图来描述它：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_23-34-47.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个数据结构，它的名称是 &lt;code&gt;B+树&lt;/code&gt; 。&lt;/p&gt;
&lt;h4&gt;B+Tree&lt;/h4&gt;
&lt;p&gt;为什么b+树最多只有4层？&lt;/p&gt;
&lt;p&gt;一个B+树的节点其实可以分成好多层，规定最下边的那层，也就是存放我们用户记录的那层为第 0 层， 之后依次往上加。之前我们做了一个非常极端的假设：存放用户记录的页 最多存放3条记录 ，存放目录项 记录的页 最多存放4条记录 。其实真实环境中一个页存放的记录数量是非常大的，假设所有存放&lt;strong&gt;用户记录&lt;/strong&gt; 的叶子节点代表的&lt;strong&gt;数据页&lt;/strong&gt;可以存放 100条用户记录 ，所有存放目录项记录的内节点代表的&lt;strong&gt;数据页&lt;/strong&gt;可以存 放 1000条目录项记录 ，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果B+树只有1层，也就是只有1个用于存放&lt;strong&gt;用户记录&lt;/strong&gt;的节点，最多能存放 100 条记录。&lt;/li&gt;
&lt;li&gt;如果B+树有2层，最多能存放 1000×100=10,0000  十万条记录。&lt;/li&gt;
&lt;li&gt;如果B+树有3层，最多能存放 1000×1000×100=1,0000,0000 一亿条记录。&lt;/li&gt;
&lt;li&gt;如果B+树有4层，最多能存放 1000×1000×1000×100=1000,0000,0000 条记录。相当多的记录！&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你的表里能存放 100000000000 条记录吗？所以一般情况下，我们用到的 &lt;code&gt;B+树都不会超过4层&lt;/code&gt; ，那我们通过主键值去查找某条记录最多只需要做4个页面内的查找（查找3个&lt;code&gt;目录项页&lt;/code&gt;和一个&lt;code&gt;用户记录&lt;/code&gt;页），又因为在每个页面内有所谓的 Page Directory （页目录），所以在页面内也可以通过 &lt;strong&gt;二分法&lt;/strong&gt; 实现快速 定位记录。&lt;/p&gt;
&lt;h3&gt;常见索引概念&lt;/h3&gt;
&lt;p&gt;索引按照物理实现方式，索引可以分为 2 种：聚簇（聚集）和非聚簇（非聚集）索引。我们也把非聚集 索引称为二级索引或者辅助索引。&lt;/p&gt;
&lt;h4&gt;聚簇索引&lt;/h4&gt;
&lt;p&gt;聚簇索引并不是一种单独的索引类型，而是&lt;code&gt;一种数据存储方式&lt;/code&gt;（所有的&lt;strong&gt;用户记录&lt;/strong&gt;都存储在了叶子结点），也就是所谓的 &lt;code&gt;索引即数据，数据即索引&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;术语&quot;聚簇&quot;表示当前数据行和相邻的键值聚簇的存储在一起&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用记录主键值的大小进行记录和页的排序，这包括三个方面的含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;页内&lt;/code&gt; 的记录是按照主键的大小顺序排成一个 &lt;code&gt;单向链表&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;各个存放 &lt;code&gt;用户记录的页&lt;/code&gt; 也是根据页中用户记录的主键大小顺序排成一个 &lt;code&gt;双向链表&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;存放 &lt;code&gt;目录项记录的页&lt;/code&gt; 分为不同的层次，在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个 &lt;code&gt;双向链表&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B+树的 &lt;code&gt;叶子节点&lt;/code&gt; 存储的是完整的用户记录。&lt;/p&gt;
&lt;p&gt;所谓完整的用户记录，就是指这个记录中存储了所有列的值（包括隐藏列）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们把具有这两种特性的B+树称为聚簇索引，所有完整的用户记录都存放在这个&lt;code&gt;聚簇索引&lt;/code&gt;的叶子节点处。这种聚簇索引并不需要我们在MySQL语句中显式的使用INDEX 语句去创建， &lt;code&gt;InnDB&lt;/code&gt; 存储引擎会 &lt;code&gt;自动&lt;/code&gt; 的为我们创建聚簇索引。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;数据访问更快&lt;/code&gt; ，因为聚簇索引将索引和数据保存在同一个B+树中，因此从聚簇索引中获取数据比非聚簇索引更快&lt;/li&gt;
&lt;li&gt;聚簇索引对于主键的 &lt;code&gt;排序查找&lt;/code&gt; 和 &lt;code&gt;范围查找&lt;/code&gt; 速度非常快&lt;/li&gt;
&lt;li&gt;按照聚簇索引排列顺序，查询显示一定范围数据的时候，由于数据都是紧密相连，数据库不用从多 个数据块中提取数据，所以 &lt;code&gt;节省了大量的io操作&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;插入速度严重依赖于插入顺序&lt;/code&gt; ，按照主键的顺序插入是最快的方式，否则将会出现页分裂，严重影响性能。因此，对于InnoDB表，我们一般都会定义一个自增的ID列为主键&lt;/li&gt;
&lt;li&gt;&lt;code&gt;更新主键的代价很高&lt;/code&gt; ，因为将会导致被更新的行移动。因此，对于InnoDB表，我们一般定义主键为不可更新&lt;/li&gt;
&lt;li&gt;&lt;code&gt;二级索引访问需要两次索引查找&lt;/code&gt; ，第一次找到主键值，第二次根据主键值找到行数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;二级索引（辅助索引、非聚簇索引）&lt;/h4&gt;
&lt;p&gt;如果我们想以别的列作为搜索条件该怎么办？肯定不能是从头到尾沿着链表依次遍历记录一遍。&lt;/p&gt;
&lt;p&gt;答案：我们可以&lt;code&gt;多建几颗B+树&lt;/code&gt;，不同的B+树中的数据采用不同的排列规则。比方说我们用c2列的大小作为数据页、页中记录的排序规则，再建一课B+树，效果如下图所示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_00-26-18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个B+树与上边介绍的聚簇索引有几处不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用记录c2列的大小进行记录和页的排序，这包括三个方面的含义:
&lt;ul&gt;
&lt;li&gt;页内的记录是按照c2列的大小顺序排成一个 &lt;code&gt;单向链表&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;各个存放 &lt;code&gt;用户记录的页&lt;/code&gt; 也是根据页中记录的c2列大小顺序排成一个 &lt;code&gt;双向链表&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;存放 &lt;code&gt;目录项记录的页&lt;/code&gt; 分为不同的层次，在同一层次中的页也是根据页中目录项记录的c2列大小顺序排成一个 &lt;code&gt;双向链表&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B+树的叶子节点存储的并不是完整的用户记录，而只是 &lt;code&gt;c2列+主键&lt;/code&gt; 这两个列的值&lt;/li&gt;
&lt;li&gt;目录项记录中不再是 &lt;code&gt;主键+页号&lt;/code&gt; 的搭配，而变成了 &lt;code&gt;c2列+页号&lt;/code&gt; 的搭配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以如果我们现在想通过c2列的值查找某些记录的话就可以使用我们刚刚建好的这个B+树了。以查找c2列的值为&lt;code&gt;4&lt;/code&gt;的记录为例，查找过程如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确定 &lt;code&gt;目录项记录页&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;根据 &lt;code&gt;根页面&lt;/code&gt;，也就是&lt;code&gt;页44&lt;/code&gt;，可以快速定位到 &lt;code&gt;目录项记录&lt;/code&gt;所在的页为&lt;code&gt;页42&lt;/code&gt;(因为&lt;code&gt;2&amp;lt;4&amp;lt;9&lt;/code&gt;)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 &lt;code&gt;目录项记录&lt;/code&gt; 页确定用户记录真实所在的页&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在&lt;code&gt;页42&lt;/code&gt; 中可以快速定位到实际存储用户记录的页，但是由于&lt;code&gt;c2&lt;/code&gt; 列并没有唯一性约束，所以 &lt;code&gt;c2&lt;/code&gt; 列值为&lt;code&gt;4&lt;/code&gt;的记录可能分布在多个数据页中，又因为&lt;code&gt;2&amp;lt;4≤4&lt;/code&gt;，所以确定实际存储用户记录的页在 &lt;code&gt;页34&lt;/code&gt; 和 &lt;code&gt;页35&lt;/code&gt; 中。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在真实存储用户记录的页中定位到具体的记录。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到 &lt;code&gt;页34&lt;/code&gt; 和 &lt;code&gt;页35&lt;/code&gt; 中定位到具体的记录。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;但是这个B+树的叶子节点中的记录只存储了 &lt;code&gt;c2&lt;/code&gt;和 &lt;code&gt;c1&lt;/code&gt;(也就是 &lt;code&gt;主键&lt;/code&gt;)两个列，所以我们必须再根据主键值4去聚簇索引中再查找一遍完整的用户记录。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;概念：&lt;strong&gt;回表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值，所以如果我们想根据c2列的值查找到完整的用户记录的话，仍然需要到 &lt;code&gt;聚簇索引&lt;/code&gt; 中再查一遍，这个过程称为 &lt;code&gt;回表&lt;/code&gt; 。也就是根据c2列的值查询一条完整的用户记录需要使用到 &lt;code&gt;2&lt;/code&gt; 棵B+树！&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;问题：为什么我们还需要一次 &lt;code&gt;回表&lt;/code&gt; 操作呢？直接把完整的用户记录放到叶子节点不OK吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;回答：&lt;/p&gt;
&lt;p&gt;如果把完整的用户记录放到叶子结点是可以不用回表。但是太占地方了，相当于每建立一课B+树都需要把所有的用户记录再都拷贝一遍，这就有点太浪费存储空间了。
比如表中有10个字段，意味着把每个字段都要拷贝一遍，放到叶子节点，叶子节点又完整的存储了表中所有记录。而且一张表是可以有多个聚簇索引，假如有三个聚簇索引，
每个二级索引都存储了一份完整的表记录，那表记录一共存储了四份，这就乱了，冗余特别大；&lt;/p&gt;
&lt;p&gt;因为这种按照非主键列建立的B+树需要一次回表操作才可以定位到完整的用户记录，所以这种B+树也被称为二级索引，或者辅助索引。由于使用的是c2列的大小作为B+树的排序规则，所以我们也称这个B+树为c2列建立的索引。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;非聚簇索引的存在不影响数据在聚簇索引中的组织，所以一张表可以有多个非聚簇索引。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_00-38-17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;小结：聚簇索引与非聚簇索引的原理不同，在使用上也有一些区别：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;聚簇索引的&lt;code&gt;叶子节点&lt;/code&gt;存储的就是我们的&lt;code&gt;数据记录&lt;/code&gt;, 非聚簇索引的叶子节点存储的是&lt;code&gt;数据位置&lt;/code&gt;。非聚簇索引不会影响数据表的物理存储顺序。&lt;/li&gt;
&lt;li&gt;一个表&lt;code&gt;只能有一个聚簇索引&lt;/code&gt;，因为只能有一种排序存储的方式，但可以有&lt;code&gt;多个非聚簇索引&lt;/code&gt;，也就是多个索引目录提供数据检索。&lt;/li&gt;
&lt;li&gt;使用聚簇索引的时候，数据的&lt;code&gt;查询效率高&lt;/code&gt;，但如果对数据进行插入，删除，更新等操作，效率会比非聚簇索引低。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;联合索引&lt;/h4&gt;
&lt;p&gt;联合索引就是非聚簇索引中的一种&lt;/p&gt;
&lt;p&gt;联合索引是指对表中的多个列组合起来创建的索引。&lt;/p&gt;
&lt;p&gt;我们也可以同时以多个列的大小作为排序规则，也就是同时为多个列建立索引，比方说我们想让B+树按 照 &lt;code&gt;c2和c3列&lt;/code&gt; 的大小进行排序，这个包含两层含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先把各个&lt;code&gt;记录&lt;/code&gt;和&lt;code&gt;页&lt;/code&gt;按照c2列进行排序。&lt;/li&gt;
&lt;li&gt;在记录的c2列相同的情况下，采用c3列进行排序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为c2和c3建立的索引的示意图如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_20-13-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如图所示，我们需要注意以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每条&lt;code&gt;目录项&lt;/code&gt;都由c2、c3、&lt;code&gt;页号&lt;/code&gt;这三个部分组成，各条记录先按照c2列的值进行排序，如果记录的c2列相同，则按照c3列的值进行排序&lt;/li&gt;
&lt;li&gt;B+树叶子节点处的&lt;code&gt;用户记录&lt;/code&gt;由c2、c3和&lt;code&gt;主键c1&lt;/code&gt;列组成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意一点，以c2和c3列的大小为排序规则建立的B+树称为 &lt;code&gt;联合索引&lt;/code&gt; ，本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的，不同点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;建立 &lt;code&gt;联合索引&lt;/code&gt; 只会建立如上图一样的1棵B+树。&lt;/li&gt;
&lt;li&gt;为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵B+树。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;InnoDB的B+树索引的注意事项&lt;/h3&gt;
&lt;h4&gt;1. 根页面位置万年不动&lt;/h4&gt;
&lt;p&gt;实际上B+树的形成过程是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每当为某个表创建一个B+树索引（聚簇索引不是人为创建的，默认就有）的时候，都会为这个索引创建一个 &lt;code&gt;根结点&lt;/code&gt; 页面。最开始表中没有数据的时候，每个B+树索引对应的 &lt;code&gt;根结点&lt;/code&gt; 中即没有&lt;code&gt;用户记录&lt;/code&gt;，也没有&lt;code&gt;目录项&lt;/code&gt;记录。&lt;/li&gt;
&lt;li&gt;随后向表中插入用户记录时，先把&lt;code&gt;用户记录&lt;/code&gt;存储到这个&lt;code&gt;根节点&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;当根节点中的可用 &lt;code&gt;空间用完时&lt;/code&gt; 继续插入记录，此时会将根节点中的所有记录复制到一个新分配的页，比如 &lt;code&gt;页a&lt;/code&gt; 中，然后对这个新页进行 &lt;code&gt;页分裂&lt;/code&gt; 的操作，得到另一个新页，比如&lt;code&gt;页b&lt;/code&gt; 。这时新插入的记录根据键值（也就是聚簇索引中的主键值，二级索引中对应的索引列的值）的大小就会被分配到 &lt;code&gt;页a&lt;/code&gt; 或者 &lt;code&gt;页b&lt;/code&gt; 中，而 &lt;code&gt;根节点&lt;/code&gt; 便升级为存储&lt;code&gt;目录项&lt;/code&gt;记录的页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个过程特别注意的是：一个B+树索引的根节点自诞生之日起，便不会再移动。这样只要我们对某个表建一个索引，那么它的根节点的页号便会被记录到某个地方。然后凡是 &lt;code&gt;InnoDB&lt;/code&gt; 存储引擎需要用到这个索引的时候，都会从哪个固定的地方取出根节点的页号，从而来访问这个索引。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;节点之间是双向链表 节点内部 是单向链表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 内节点中目录项记录的唯一性&lt;/h4&gt;
&lt;p&gt;内节点也就是非叶子节点&lt;/p&gt;
&lt;p&gt;我们知道B+树索引的内节点中目录项记录的内容是 &lt;code&gt;索引列 + 页号&lt;/code&gt; 的搭配，但是这个搭配对于二级索引来说有点不严谨。还拿 index_demo 表为例，假设这个表中的数据是这样的：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;c1&lt;/th&gt;
&lt;th&gt;c2&lt;/th&gt;
&lt;th&gt;c3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&apos;u&apos;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&apos;d&apos;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&apos;y&apos;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&apos;a&apos;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果二级索引中目录项记录的内容只是 &lt;code&gt;索引列 + 页号&lt;/code&gt; 的搭配的话，那么为 &lt;code&gt;c2&lt;/code&gt; 列建立索引后的B+树应该长这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_20-55-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果我们想新插入一行记录，其中 &lt;code&gt;c1&lt;/code&gt; 、&lt;code&gt;c2&lt;/code&gt; 、&lt;code&gt;c3&lt;/code&gt; 的值分别是: &lt;code&gt;9、1、c&lt;/code&gt;, 那么在修改这个为 c2 列建立的二级索引对应的 B+树时便碰到了个大问题：由于 &lt;code&gt;页3&lt;/code&gt; 中存储的目录项记录是由 &lt;code&gt;c2列 + 页号&lt;/code&gt; 的值构成的，&lt;code&gt;页3&lt;/code&gt; 中的两条目录项记录对应的 &lt;code&gt;c2列&lt;/code&gt;的值都是1，而我们 新插入的这条记录的 &lt;code&gt;c2 列&lt;/code&gt; 的值也是 1，那我们这条新插入的记录到底应该放在 &lt;code&gt;页4&lt;/code&gt; 中，还是应该放在 &lt;code&gt;页5&lt;/code&gt; 中？答案：对不起，懵了&lt;/p&gt;
&lt;p&gt;为了让新插入记录找到自己在那个页面，我们需要&lt;strong&gt;保证在B+树的同一层页节点的目录项记录除页号这个字段以外是唯一的&lt;/strong&gt;。所以对于二级索引的内节点的&lt;code&gt;目录项&lt;/code&gt;记录的内容实际上是由三个部分构成的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;索引列的值&lt;/li&gt;
&lt;li&gt;主键值&lt;/li&gt;
&lt;li&gt;页号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是我们把&lt;code&gt;主键值&lt;/code&gt;也添加到二级索引&lt;code&gt;内节点&lt;/code&gt;中的&lt;code&gt;目录项&lt;/code&gt;记录，这样就能保住 B+ 树每一层节点中各条&lt;code&gt;目录项&lt;/code&gt;记录除页号这个字段外是唯一的，所以我们为c2建立二级索引后的示意图实际上应该是这样子的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_21-00-41.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样我们再插入记录&lt;code&gt;(9, 1, &apos;c&apos;)&lt;/code&gt; 时，由于 &lt;code&gt;页3&lt;/code&gt; 中存储的目录项记录是由 &lt;code&gt;c2列 + 主键 + 页号&lt;/code&gt; 的值构成的，可以先把新纪录的 &lt;code&gt;c2 列&lt;/code&gt;的值和 &lt;code&gt;页3&lt;/code&gt; 中各目录项记录的 &lt;code&gt;c2 列&lt;/code&gt;的值作比较，如果 c2 列的值相同的话，可以接着比较主键值，因为B+树同一层中不同&lt;code&gt;目录项&lt;/code&gt;记录的 &lt;code&gt;c2列 + 主键的值&lt;/code&gt;肯定是不一样的，所以最后肯定能定位唯一的一条&lt;code&gt;目录项&lt;/code&gt;记录，在本例中最后确定新纪录应该被插入到 &lt;code&gt;页5&lt;/code&gt; 中。&lt;/p&gt;
&lt;h4&gt;3. 一个页面最少存储 2 条记录&lt;/h4&gt;
&lt;p&gt;一个B+树只需要很少的层级就可以轻松存储数亿条记录，查询速度相当不错！这是因为B+树本质上就是一个大的多层级目录，每经过一个目录时都会过滤掉许多无效的子目录，直到最后访问到存储真实数据的目录。那如果一个大的目录中只存放一个子目录是个啥效果呢？那就是目录层级非常非常多，而且最后的那个存放真实数据的目录中只存放一条数据。所以 &lt;strong&gt;InnoDB 的一个数据页至少可以存放两条记录&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;MyISAM中的索引方案&lt;/h3&gt;
&lt;p&gt;B树索引适用存储引擎如表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;索引 / 存储引擎&lt;/th&gt;
&lt;th&gt;MyISAM&lt;/th&gt;
&lt;th&gt;InnoDB&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B+Tree索引&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;即使多个存储引擎支持同一种类型的索引，但是他们的实现原理也是不同的。Innodb和MyISAM默认的索引是Btree索引；而Memory默认的索引是Hash索引。&lt;/p&gt;
&lt;p&gt;MyISAM引擎使用 &lt;code&gt;B+Tree&lt;/code&gt; 作为索引结构，叶子节点的data域存放的是 &lt;code&gt;数据记录的地址&lt;/code&gt; 。而InnoDB叶子节点存的就要看你是聚簇索引还是非局促索引了。&lt;/p&gt;
&lt;h4&gt;MyISAM索引的原理&lt;/h4&gt;
&lt;p&gt;MyISAM中是没有聚簇索引的，全部都可以理解为二级索引。并且数据与索引是分开存储的，&lt;code&gt;.MYD&lt;/code&gt;存储数据&lt;code&gt;.MYI&lt;/code&gt;存储索引。而InnoDB中我们说的是&lt;code&gt;数据即索引，索引即数据&lt;/code&gt;，就是索引和数据都是存储在一个&lt;code&gt;.ibd&lt;/code&gt;文件中。&lt;/p&gt;
&lt;p&gt;我们知道 &lt;code&gt;InnoDB中索引即数据&lt;/code&gt;，也就是聚簇索引的那棵B+树的叶子节点中已经把所有完整的用户记录都包含了而 &lt;code&gt;MyISAM&lt;/code&gt; 的索引方案虽然也使用树形结构，但是却 &lt;code&gt;将索引和数据分开存储&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将表中的记录 &lt;code&gt;按照记录的插入顺序&lt;/code&gt; 单独存储在一个文件中，称之为 &lt;code&gt;数据文件&lt;/code&gt;。这个文件并不划分为若干个数据页，有多少记录就往这个文件中塞多少记录就成了。由于在插入数据的时候并 &lt;code&gt;没有刻意按照主键大小排序&lt;/code&gt;，所以我们并不能在这些数据上使用二分法进行查找。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;MyISAM&lt;/code&gt; 存储引擎的表会把索引信息另外存储到一个称为 &lt;code&gt;索引文件&lt;/code&gt; 的另一个文件中。 &lt;code&gt;MyISAM&lt;/code&gt; 会单独为表的主键创建一个索引，只不过在索引的叶子节点中存储的不是完整的用户记录，而是 &lt;code&gt;主键值 + 数据记录地址&lt;/code&gt;的组合。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_21-57-32.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里设表一共有三列，假设我们以Col1为主键，上图是一个MyISAM表的主索引(Primary key)示意。可以看出&lt;code&gt;MyISAM的索引文件仅仅保存数据记录的地址&lt;/code&gt;。在MyISAM中，主键索引和二级索引(Secondary key)在结构上没有任何区别，只是主键索引要求key是唯一的，而二级索引的key可以重复。如果我们在Co12上建立一个二级索
引，则此索引的结构如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_21-59-35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同样也是一棵&lt;code&gt;B+Tree&lt;/code&gt;，data域保存数据记录的地址。因此，MyISAM中索引检索的算法为:首先按照B+Tree搜索算法搜索索引，如果指定的Key存在，则取出其data域的值，然后以data域的值为地址，直接去&lt;code&gt;.MYD文件&lt;/code&gt;中读取相应数据记录。&lt;/p&gt;
&lt;h3&gt;MyISAM 与 InnoDB对比&lt;/h3&gt;
&lt;p&gt;MyISAM的索引方式都是“非聚簇”的，与InnoDB包含1个聚簇索引是不同的。小结两种引擎中索引的区别：&lt;/p&gt;
&lt;p&gt;① 在InnoDB存储引擎中，我们只需要根据主键值对 &lt;code&gt;聚簇索引&lt;/code&gt; 进行一次查找就能找到对应的记录，而在 &lt;code&gt;MyISAM&lt;/code&gt; 中却需要进行一次 &lt;code&gt;回表&lt;/code&gt; 操作，意味着MyISAM中建立的索引相当于全部都是 &lt;code&gt;二级索引&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;② InnoDB的数据文件本身就是索引文件，而MyISAM索引文件和数据文件是&lt;code&gt;分离的&lt;/code&gt; ，索引文件仅保存数据记录的地址。&lt;/p&gt;
&lt;p&gt;③ InnoDB的非聚簇索引data域存储&lt;code&gt;相应记录&lt;/code&gt;及 &lt;code&gt;主键的值&lt;/code&gt; ，而MyISAM索引记录的是 &lt;code&gt;地址&lt;/code&gt; 。换句话说， InnoDB的所有非聚簇索引都引用主键作为data域。&lt;/p&gt;
&lt;p&gt;④ MyISAM的回表操作是十分 &lt;code&gt;快速&lt;/code&gt; 的，因为是拿着地址偏移量直接到&lt;code&gt;.MYD文件&lt;/code&gt;中取数据的，反观InnoDB是通过获取主键之后再去聚簇索引里找记录，虽然说也不慢，但还是比不上直接用地址去访问。&lt;/p&gt;
&lt;p&gt;⑤ InnoDB要求表 必须有主键 （ &lt;code&gt;MyISAM可以没有&lt;/code&gt; ）。如果没有显式指定，则MySQL系统会自动选择一个 可以非空且唯一标识数据记录的列作为主键。如果不存在这种列，则MySQL自动为InnoDB表生成一个&lt;code&gt;隐含字段&lt;/code&gt;作为主键，这个字段长度为6个字节，类型为长整型。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;小结：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助。比如:&lt;/p&gt;
&lt;p&gt;举例1: 知道了InnoDB的索引实现后，就很容易明白&lt;code&gt;为什么不建议使用过长的字段作为主键&lt;/code&gt;，因为所有二级索引都引用主键索引，过长的主键索引会令二级索引变得过大。&lt;/p&gt;
&lt;p&gt;举例2: 用非单调的字段作为主键在InnoDB中不是个好主意，因为InnoDB数据文件本身是一棵B+Tree，非单调的主键会造成在插入新记录时，数据文件为了维持B+Tree的特性而频繁的&lt;code&gt;页分裂&lt;/code&gt;，&lt;code&gt;记录移动&lt;/code&gt;，并且还会导致随机磁盘IO，十分低效，而使用自增字段作为主键则是一个很好的选择。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_22-10-16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;索引的代价&lt;/h3&gt;
&lt;p&gt;索引是个好东西，可不能乱建，它在空间和时间上都会有消耗：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;空间上的代价&lt;/p&gt;
&lt;p&gt;每建立一个索引都要为它建立一棵B+树，每一棵B+树的每一个节点都是一个&lt;code&gt;数据页&lt;/code&gt;，一个页默认会 占用 &lt;code&gt;16KB&lt;/code&gt; 的存储空间，一棵很大的B+树由许多数据页组成，那就是很大的一片存储空间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;时间上的代价&lt;/p&gt;
&lt;p&gt;每次对表中的数据进行 &lt;code&gt;增、删、改&lt;/code&gt; 操作时，都需要去修改各个&lt;code&gt;B+树&lt;/code&gt;索引。而且我们讲过，B+树每层节点都是按照索引列的值 &lt;code&gt;从小到大的顺序排序&lt;/code&gt; 而组成了 &lt;code&gt;双向链表&lt;/code&gt; 。不论是叶子节点中的记录，还是内节点中的记录（也就是不论是&lt;code&gt;用户记录&lt;/code&gt;还是&lt;code&gt;目录项记录&lt;/code&gt;）都是按照索引列的值从小到大的顺序 而形成了一个&lt;code&gt;单向链表&lt;/code&gt;。而增、删、改操作可能会对节点和记录的排序造成破坏，所以存储引擎需要额外的时间进行一些 &lt;code&gt;记录移位&lt;/code&gt; ， &lt;code&gt;页面分裂&lt;/code&gt; 、 &lt;code&gt;页面回收&lt;/code&gt; 等操作来维护好节点和记录的排序。如果我们建了许多索引，每个索引对应的B+树都要进行相关的维护操作，会给性能拖后腿。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一个表上索引建的越多，就会占用越多的存储空间，在增删改记录的时候性能就越差。为了能建立又好又少的索引，我们得学学这些索引在哪些条件下起作用的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;MySQL数据结构选择的合理性&lt;/h3&gt;
&lt;p&gt;从MySQL的角度讲，不得不考虑一个现实问题就是磁盘IO。如果我们能让索引的数据结构尽量减少硬盘的 I/0 操作，所消耗的时间也就越小。可以说，&lt;code&gt;磁盘的I/0 操作次数&lt;/code&gt; 对索引的使用效率至关重要。&lt;/p&gt;
&lt;p&gt;查找都是索引操作，一般来说索引非常大，尤其是关系型数据库，当数据量比较大的时候，索引的大小有可能几个G甚至更多，为了减少索引在内容的占用，&lt;code&gt;数据库索引是存储在外部磁盘上的&lt;/code&gt;。当我们利用索引查询的时候，不可能把整个索引全部加载到内存，只能 &lt;code&gt;逐一加载&lt;/code&gt;，那么MySQL衡量查询效率的标准就是磁盘I0次数。&lt;/p&gt;
&lt;h4&gt;1. 全表查询&lt;/h4&gt;
&lt;p&gt;这里都懒得说了。&lt;/p&gt;
&lt;h4&gt;2. Hash查询&lt;/h4&gt;
&lt;p&gt;Hash 本身是一个函数，又被称为散列函数，它可以帮助我们大幅提升检索数据的效率。&lt;/p&gt;
&lt;p&gt;Hash 算法是通过某种确定性的算法（比如 MD5、SHA1、SHA2、SHA3）将输入转变为输出。&lt;code&gt;相同的输入永远可以得到相同的输出&lt;/code&gt;，假设输入内容有微小偏差，在输出中通常会有不同的结果。&lt;/p&gt;
&lt;p&gt;举例：如果你想要验证两个文件是否相同，那么你不需要把两份文件直接拿来比对，只需要让对方把 Hash 函数计算得到的结果告诉你即可，然后在本地同样对文件进行 Hash 函数的运算，最后通过比较这两个 Hash 函数的结果是否相同，就可以知道这两个文件是否相同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;加快查找速度的数据结构，常见的有两类：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;(1) 树，例如平衡二叉搜索树，查询/插入/修改/删除的平均时间复杂度都是&lt;code&gt;O(log2N)&lt;/code&gt;;&lt;/p&gt;
&lt;p&gt;(2) 哈希，例如HashMap，查询/插入/修改/删除的平均时间复杂度都是 &lt;code&gt;O(1)&lt;/code&gt;; (key, value)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_23-10-42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;采用 Hash 进行检索效率非常高，基本上一次检索就可以找到数据，而 B+ 树需要自顶向下依次查找，多次访问节点才能找到数据，中间需要多次 I/O 操作，&lt;code&gt;从效率来说 Hash 比 B+ 树更快&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在哈希的方式下，一个元素k处于h(k)中，即利用哈希函数h，根据关键字k计算出槽的位置。函数h将关键字域映射到哈希表&lt;code&gt;T[0…m-1]&lt;/code&gt;的槽位上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_23-12-27.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中哈希函数h有可能将两个不同的关键字映射到相同的位置，这叫做 &lt;code&gt;碰撞&lt;/code&gt; ，在数据库中一般采用 &lt;code&gt;链接法&lt;/code&gt; 来解决。在链接法中，将散列到同一槽位的元素放在一个链表中，如下图所示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_23-12-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实验：体会数组和hash表的查找方面的效率区别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 算法复杂度为 O(n)
@Test
public void test1(){
    int[] arr = new int[100000];
    for(int i = 0;i &amp;lt; arr.length;i++){
        arr[i] = i + 1;
    }
    long start = System.currentTimeMillis();
    for(int j = 1; j&amp;lt;=100000;j++){
        int temp = j;
        for(int i = 0;i &amp;lt; arr.length;i++){
            if(temp == arr[i]){
                break;
            }
        }
    }
    long end = System.currentTimeMillis();
    System.out.println(&quot;time： &quot; + (end - start)); //time： 823ms
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 算法复杂度为 O(1)
@Test
public void test2(){
    HashSet&amp;lt;Integer&amp;gt; set = new HashSet&amp;lt;&amp;gt;(100000);
    for(int i = 0;i &amp;lt; 100000;i++){
    	set.add(i + 1);
    }
    long start = System.currentTimeMillis();
    for(int j = 1; j&amp;lt;=100000;j++) {
        int temp = j;
        boolean contains = set.contains(temp);
    }
    long end = System.currentTimeMillis();
    System.out.println(&quot;time： &quot; + (end - start)); //time： 5ms
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hash结构效率高，那为什么索引结构要设计成树型呢？&lt;/p&gt;
&lt;p&gt;原因1: &lt;code&gt;Hash 索引&lt;/code&gt;仅能满足&lt;code&gt;(=)&lt;/code&gt; &lt;code&gt;(&amp;lt;&amp;gt;)&lt;/code&gt;和&lt;code&gt;IN&lt;/code&gt;查询。如果进行范围查询,哈希型的索引,时间复杂度会退化为&lt;code&gt;O(n)&lt;/code&gt;; 而树型的&quot;有序&quot;特性, 依然能够保持&lt;code&gt;O(log2N)&lt;/code&gt;的高效率。&lt;/p&gt;
&lt;p&gt;原因2: Hash 索引还有一个缺陷,数据的存储是&lt;code&gt;没有顺序的&lt;/code&gt;,在&lt;code&gt;ORDER BY&lt;/code&gt;的情况下,使用 &lt;code&gt;Hash 索引&lt;/code&gt;还需要对数据重新排序。&lt;/p&gt;
&lt;p&gt;原因3: 对于联合索引的情况,Hash 值是将联合索引键&lt;code&gt;合并后一起来计算的&lt;/code&gt;,无法对单独的一个键或者几个索引键进行查询。多字段联合索引的B+树仍然可以通过单个字段来查。Hash方式则不行。如果是树形的，联合索引可以单独使用C3，而Hash只能同时用C2和C3。&lt;/p&gt;
&lt;p&gt;原因4: 对于等值查询来说,通常 Hash 索引的效率更高,不过也存在一种情况,就是索引列的重复值如果很多,效率就会降低。这是因为遇到 Hash 冲突时,需要遍历桶中的行指针来进行比较,找到查询的关键字,非常耗时。所以,Hash 索引通常不会用到重复值多的列上,比如列为性别、年龄的情况等。&lt;/p&gt;
&lt;p&gt;Hash索引适用存储引擎如表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;索引 / 存储引擎&lt;/th&gt;
&lt;th&gt;MyISAM&lt;/th&gt;
&lt;th&gt;InnoDB&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HASH索引&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Hash索引的适用性：&lt;/p&gt;
&lt;p&gt;Hash 索引存在着很多限制，相比之下在数据库中 B+ 树索引的使用面会更广，不过也有一些场景采用 Hash 索引效率更高，比如在键值型（Key-Value）数据库中，&lt;code&gt;Redis 存储的核心就是 Hash 表&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MySQL&lt;/code&gt; 中的 &lt;code&gt;Memory&lt;/code&gt; 存储引擎支持&lt;code&gt; Hash 存储&lt;/code&gt;，如果我们需要用到查询的临时表时，就可以选择 Memory 存储引擎，把某个字段设置为 &lt;code&gt;Hash 索引&lt;/code&gt;，比如字符串类型的字段，进行 &lt;code&gt;Hash 计算&lt;/code&gt;之后长度可以缩短到几个字节。当字段的重复度低，而且经常需要进行等值查询的时候，采用 &lt;code&gt;Hash 索引&lt;/code&gt;是个不错的选择。&lt;/p&gt;
&lt;p&gt;另外，InnoDB 本身不支持 &lt;code&gt;Hash 索引&lt;/code&gt;，但是提供&lt;code&gt;自适应 Hash 索引&lt;/code&gt;（Adaptive Hash Index）。什么情况下才会使用自适应 Hash 索引呢？如果某个数据经常被访问，当满足一定条件的时候，就会将这个数据页的地址存放到 &lt;code&gt;Hash 表&lt;/code&gt;中。这样下次查询的时候，就可以直接找到这个页面的所在位置。这样让 &lt;code&gt;B+ 树&lt;/code&gt;也具备了 Hash 索引的优点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-18_23-25-27.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;采用自适应 Hash 索引目的是方便根据 SQL 的查询条件加速定位到叶子节点，特别是当 B+ 树比较深的时 候，通过自适应 Hash 索引可以明显提高数据的检索效率。&lt;/p&gt;
&lt;p&gt;我们可以通过 &lt;code&gt;innodb_adaptive_hash_index&lt;/code&gt; 变量来查看是否开启了&lt;code&gt;自适应 Hash&lt;/code&gt;，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show variables like &apos;%adaptive_hash_index&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 二叉搜索树&lt;/h4&gt;
&lt;p&gt;如果我们利用二叉树作为索引结构，那么磁盘的IO次数和索引树的高度是相关的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;二叉搜索树的特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个节点只能有两个子节点，也就是一个节点度不能超过2&lt;/li&gt;
&lt;li&gt;左子节点 &amp;lt; 本节点; 右子节点 &amp;gt;= 本节点，比我大的向右，比我小的向左&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查找规则&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们先来看下最基础的二叉搜索树（Binary Search Tree），搜索某个节点和插入节点的规则一样，我们假设搜索插入的数值为 key：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 key 大于根节点，则在右子树中进行查找；&lt;/li&gt;
&lt;li&gt;如果 key 小于根节点，则在左子树中进行查找；&lt;/li&gt;
&lt;li&gt;如果 key 等于根节点，也就是找到了这个节点，返回根节点即可。
举个例子，我们对数列（34，22，89，5，23，77，91）创造出来的二分查找树如下图所示：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-21_21-43-54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是特殊情况，就是有时候二叉树的深度非常大，比如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-21_21-46-19.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了提高查询效率，就需要 &lt;code&gt;减少磁盘IO数&lt;/code&gt; 。为了减少磁盘IO的次数，就需要尽量 &lt;code&gt;降低树的高度&lt;/code&gt; ，需要把 原来“瘦高”的树结构变的“矮胖”，树的每层的分叉越多越好。&lt;/p&gt;
&lt;h4&gt;4. AVL树&lt;/h4&gt;
&lt;p&gt;为了解决上面二叉查找树退化成链表的问题，人们提出了&lt;code&gt;平衡二叉搜索树（Balanced Binary Tree）&lt;/code&gt;，又称为AVL树（有别于AVL算法），它在二叉搜索树的基础上增加了约束，具有以下性质：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它是一棵空树或它的左右两个子树的高度差的绝对值不超过1，并且左右两个子树都是一棵平衡二叉树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里说一下，常见的平衡二叉树有很多种，包括了&lt;code&gt;平衡二叉搜索树&lt;/code&gt;、&lt;code&gt;红黑树&lt;/code&gt;、&lt;code&gt;数堆&lt;/code&gt;、&lt;code&gt;伸展树&lt;/code&gt;。平衡二叉搜索树是最早提出来的自平衡二叉搜索树，当我们提到平衡二叉树时一般指的就是平衡二叉搜索树。事实上，第一棵树就属于平衡二叉搜索树，搜索时间复杂度就是 &lt;code&gt;O(log2n)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;数据查询的时间主要依赖于磁盘 I/O 的次数，如果我们采用二叉树的形式，即使通过平衡二叉搜索树进行了改进，树的深度也是 &lt;code&gt;O(log2n)&lt;/code&gt;，当 n 比较大时，深度也是比较高的，比如下图的情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-21_21-50-01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每访问一次节点就需要进行一次磁盘 I/O 操作&lt;/strong&gt;，对于上面的树来说，我们需要进行 5次 I/O 操作。虽然平衡二叉树的效率高，但是树的深度也同样高，这就意味着磁盘 I/O 操作次数多，会影响整体数据查询的效率。&lt;/p&gt;
&lt;p&gt;针对同样的数据，如果我们把二叉树改成 M 叉树 （M&amp;gt;2）呢？当 M=3 时，同样的 31 个节点可以由下面 的三叉树来进行存储：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-21_21-57-00.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你能看到此时树的高度降低了，当数据量 N 大的时候，以及树的分叉树 M 大的时候，M叉树的高度会远小于二叉树的高度 (M &amp;gt; 2)。所以，我们需要把 &lt;code&gt;树从“瘦高” 变 “矮胖”&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;B-Tree&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;B 树的英文是 &lt;code&gt;Balance Tree&lt;/code&gt;，也就是 &lt;strong&gt;多路平衡查找树&lt;/strong&gt;。简写为 &lt;code&gt;B-Tree&lt;/code&gt;。它的高度远小于平衡二叉树的高度。&lt;/p&gt;
&lt;p&gt;B 树的结构如下图所示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-21_21-59-26.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;B 树作为&lt;em&gt;多路平衡查找树&lt;/em&gt;，它的每一个节点最多可以包括 M 个子节点，&lt;code&gt;M 称为 B 树的阶&lt;/code&gt;。每个磁盘块中包括了&lt;code&gt;关键字&lt;/code&gt;和&lt;code&gt;子节点的指针&lt;/code&gt;。如果一个磁盘块中包括了 x 个关键字，那么指针数就是 x+1。对于一个 100 阶的 B 树来说，如果有 3 层的话最多可以存储约 100 万的索引数据。对于大量的索引数据来说，采用 B 树的结构是非常适合的，因为树的高度要远小于二叉树的高度。&lt;/p&gt;
&lt;p&gt;一个 M 阶的 B 树（M&amp;gt;2）有以下的特性：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根节点的儿子数的范围是 &lt;code&gt;[2,M]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;每个中间节点包含 &lt;code&gt;k-1&lt;/code&gt; 个关键字和 k 个孩子，孩子的数量 = 关键字的数量 +1，k 的取值范围为 &lt;code&gt;[ceil(M/2), M]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;叶子节点包括 &lt;code&gt;k-1&lt;/code&gt; 个关键字（叶子节点没有孩子），k 的取值范围为 &lt;code&gt;[ceil(M/2), M]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;假设中间节点节点的关键字为：&lt;code&gt;Key[1]&lt;/code&gt;, &lt;code&gt;Key[2]&lt;/code&gt;, …, &lt;code&gt;Key[k-1]&lt;/code&gt;，且关键字按照升序排序，即 &lt;code&gt;Key[i]&amp;lt;Key[i+1]&lt;/code&gt;。此时 &lt;code&gt;k-1&lt;/code&gt; 个关键字相当于划分了 &lt;code&gt;k&lt;/code&gt; 个范围，也就是对应着 &lt;code&gt;k&lt;/code&gt; 个指针，即为：&lt;code&gt;P[1]&lt;/code&gt;,&lt;code&gt; P[2]&lt;/code&gt;, …, &lt;code&gt;P[k]&lt;/code&gt;，其中 &lt;code&gt;P[1]&lt;/code&gt; 指向关键字小于 &lt;code&gt;Key[1]&lt;/code&gt; 的子树，&lt;code&gt;P[i]&lt;/code&gt; 指向关键字属于 &lt;code&gt;(Key[i-1], Key[i])&lt;/code&gt; 的子树，&lt;code&gt;P[k]&lt;/code&gt; 指向关键字大于 &lt;code&gt;Key[k-1]&lt;/code&gt; 的子树。
所有叶子节点位于同一层。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面那张图所表示的 B 树就是一棵 3 阶的 B 树。我们可以看下磁盘块 2，里面的关键字为（8，12），它 有 3 个孩子 (3，5)，(9，10) 和 (13，15)，你能看到 (3，5) 小于 8，(9，10) 在 8 和 12 之间，而 (13，15) 大于 12，刚好符合刚才我们给出的特征。&lt;/p&gt;
&lt;p&gt;:::tip
M 的含义，表示 B 树的阶数（Order of B-tree），&lt;strong&gt;单个节点最多拥有的子节点数量&lt;/strong&gt;，必须是 &amp;gt;2 的整数（保证平衡性）。&lt;/p&gt;
&lt;p&gt;k 的含义，表示 当前节点的子节点数量&lt;/p&gt;
&lt;p&gt;P 的含义，表示 指向子树的指针（Pointer）&lt;/p&gt;
&lt;p&gt;关键概念图解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          [中间节点: k=4个子节点]
         /         |       |       \
        ↓          ↓       ↓        ↓
关键字:   10       (10,30)   (30,50)  &amp;gt;50
指针:    P[1]      P[2]     P[3]    P[4]
        |          |        |       |
        ▼          ▼        ▼       ▼
     [子树]     [子树]    [子树]  [子树]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;然后我们来看下如何用 B 树进行查找。假设我们想要 &lt;code&gt;查找的关键字是 9&lt;/code&gt; ，那么步骤可以分为以下几步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们与根节点的关键字 &lt;code&gt;(17，35）&lt;/code&gt;进行比较，9 小于 17 那么得到指针 P1；&lt;/li&gt;
&lt;li&gt;按照指针 P1 找到磁盘块 2，关键字为（8，12），因为 9 在 8 和 12 之间，所以我们得到指针 P2；&lt;/li&gt;
&lt;li&gt;按照指针 P2 找到磁盘块 6，关键字为（9，10），然后我们找到了关键字 9。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;你能看出来在 B 树的搜索过程中，我们比较的次数并不少，但如果把数据读取出来然后在内存中进行比 较，这个时间就是可以忽略不计的。而读取磁盘块本身需要进行 I/O 操作，消耗的时间比在内存中进行 比较所需要的时间要多，是数据查找用时的重要因素。 &lt;code&gt;B 树相比于平衡二叉树来说磁盘 I/O 操作要少&lt;/code&gt; ， 在数据查询中比平衡二叉树效率要高。所以 &lt;code&gt;只要树的高度足够低，IO次数足够少，就可以提高查询性能&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;小结:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;B树在插入和删除节点的时候如果导致树不平衡,就通过自动调整节点的位置来保持树的自平衡。&lt;/li&gt;
&lt;li&gt;关键字集合分布在整棵树中,即叶子节点和非叶子节点都存放数据。搜索有可能在非叶子节点结束&lt;/li&gt;
&lt;li&gt;其搜索性能等价于在关键字全集内做一次二分查找。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;再举例1：&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-21_22-12-54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;6. B+Tree&lt;/h4&gt;
&lt;p&gt;B+ 树也是一种多路搜索树，&lt;code&gt;基于 B 树做出了改进&lt;/code&gt;，主流的 DBMS 都支持 B+ 树的索引方式，比如 MySQL。相比于 B-Tree，&lt;code&gt;B+Tree 适合文件索引系统&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;B+ 树和 B 树的差异在于以下几点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有 k 个孩子的节点就有 k 个关键字。也就是孩子数量 = 关键字数，而 B 树中，孩子数量 = 关键字数 +1。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;B+Tree&lt;/code&gt;非叶子节点的关键字也会同时存在在子节点中，并且是在子节点中所有关键字的最大（或最小）。&lt;/li&gt;
&lt;li&gt;非叶子节点仅用于索引，不保存数据记录，跟记录有关的信息都放在叶子节点中。而 B 树中， &lt;code&gt;非叶子节点既保存索引，也保存数据记录&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;所有关键字都在叶子节点出现，叶子节点构成一个有序链表，而且叶子节点本身按照关键字的大小从小到大顺序链接。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下图就是一棵 B+ 树，阶数为 3，根节点中的关键字 1、18、35 分别是子节点（1，8，14），（18，24，31）和（35，41，53）中的最小值。每一层父节点的关键字都会出现在下一层的子节点的关键字中，因此在叶子节点中包括了所有的关键字信息，并且每一个叶子节点都有一个指向下一个节点的指针，这样就形成了一个链表。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-21_22-21-12.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比如，我们想要查找关键字 16，&lt;code&gt;B+ 树&lt;/code&gt;会自顶向下逐层进行查找：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;与根节点的关键字 (1，18，35) 进行比较，16 在 1 和 18 之间，得到指针 P1 (指向磁盘块 2)&lt;/li&gt;
&lt;li&gt;找到磁盘块 2，关键字为 (1，8，14)，因为 16 大于 14，所以得到指针 P3 (指向磁盘块 7)&lt;/li&gt;
&lt;li&gt;找到磁盘块 7，关键字为 (14，16，17)，然后我们找到了关键字 16，所以可以找到关键字 16 所对应的数
据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整个过程一共进行了3次&lt;code&gt;I/O操作&lt;/code&gt;，看起来&lt;code&gt;B+树&lt;/code&gt;和&lt;code&gt;B树&lt;/code&gt;的查询过程差不多，但是B+树和B树有个根本的差异在于，&lt;code&gt;B+树的中间节点并不直接存储数据&lt;/code&gt;。这样的好处都有什么呢？&lt;/p&gt;
&lt;p&gt;首先，&lt;code&gt;B+树查询效率更稳定&lt;/code&gt;。因为B+树每次只有访问到叶子节点才能找到对应的数据，而在B树中，非叶子节点也会存储数据，这样就会造成查询效率不稳定的情况，有时候访问到了非叶子节点就可以找到关键字，而有时需要访问到叶子节点才能找到关键字。&lt;/p&gt;
&lt;p&gt;其次，&lt;code&gt;B+树的查询效率更高&lt;/code&gt;。这是因为通常B+树比B树&lt;code&gt;更矮胖&lt;/code&gt;（阶数更大，深度更低），查询所需要的磁盘I/O也会更少。同样的磁盘页大小，B+树可以存储更多的节点关键字。&lt;/p&gt;
&lt;p&gt;不仅是对单个关键字的查询上，&lt;code&gt;在查询范围上，B+树的效率也比B树高&lt;/code&gt;。这是因为所有关键字都出现在B+树的叶子节点中，叶子节点之间会有指针，数据又是递增的，这使得我们范围查找可以通过指针连接查找。而在B树中则需要通过中序遍历才能完成查询范围的查找，效率要低很多。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;B 树和 B+ 树都可以作为索引的数据结构，在 MySQL 中采用的是 B+ 树。 但B树和B+树各有自己的应用场景，不能说B+树完全比B树好，反之亦然。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;思考题：为了减少IO，索引树会一次性加载吗？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、数据库索引是存储在磁盘上的，如果数据量很大，必然导致索引的大小也会很大，超过几个G。&lt;/p&gt;
&lt;p&gt;2、当我们利用索引查询时候，是不可能将全部几个G的索引都加载进内存的，我们能做的只能是：逐一加载每一个磁盘页，因为磁盘页对应着索引树的节点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思考题：B+树的存储能力如何？为何说一般查找行记录，最多只需1~3次磁盘IO&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;InnoDB 存储引擎中页的大小为 16KB，一般表的主键类型为 INT（占用 4 个字节）或 BIGINT（占用 8 个字节），指针类型也一般为 4 或 8 个字节，也就是说一个页（B+Tree 中的一个节点）中大概存储 16KB/(8B+8B)=1K 个键值（因为是估值，为方便计算，这里的 K 取值为 10^3。也就是说一个深度为 3 的 B+Tree 索引可以维护 10^3 * 10^3 * 10^3 = 10 亿条记录。(这里假定一个数据页也存储 10^3 条行记录数据了)&lt;/p&gt;
&lt;p&gt;实际情况中每个节点可能不能填满，因此在数据库中， B+Tree 的高度一般都在 2~4 层 。MySQL 的 InnoDB 存储引擎在设计时是将根节点常驻内存的，也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思考题：为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中，那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。&lt;/p&gt;
&lt;p&gt;2、B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点，而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同，导致每一个数据的查询效率相当。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思考题：Hash 索引与 B+ 树索引的区别&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、Hash 索引&lt;code&gt;不能进行范围查询&lt;/code&gt;，而 B+ 树可以。这是因为 Hash 索引指向的数据是无序的，而 B+ 树的叶子节点是个有序的链表。&lt;/p&gt;
&lt;p&gt;2、Hash 索引&lt;code&gt;不支持联合索引的最左侧原则&lt;/code&gt;（即联合索引的部分索引无法使用），而 B+ 树可以。对于联合索引来说，Hash 索引在计算 Hash 值的时候是将索引键合并后再一起计算 Hash 值，所以不会针对每个索引单独计算 Hash 值。因此如果用到联合索引的一个或者几个索引时，联合索引无法被利用。&lt;/p&gt;
&lt;p&gt;3、Hash 索引&lt;code&gt;不支持 ORDER BY 排序&lt;/code&gt;，因为 Hash 索引指向的数据是无序的，因此无法起到排序优化的作用，而 B+ 树索引数据是有序的，可以起到对该字段 ORDER BY 排序优化的作用。同理，我们也无法用 Hash 索引进行模糊查询，而 B+ 树使用 LIKE 进行模糊查询的时候，LIKE 后面后&lt;code&gt;模糊查询&lt;/code&gt;（比如 % 结尾）的话就可以起到优化作用。&lt;/p&gt;
&lt;p&gt;4、&lt;code&gt;InnoDB 不支持哈希索引&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思考题：Hash 索引与 B+ 树索引是在建索引的时候手动指定的吗？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你能看到，针对 InnoDB 和 MyISAM 存储引擎，都会默认采用 B+ 树索引，无法使用 Hash 索引。InnoDB 提供的自适应 Hash 是不需要手动指定的。如果是 Memory/Heap 和 NDB 存储引擎，是可以进行选择 Hash 索引的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;7. R树&lt;/h4&gt;
&lt;p&gt;R-Tree在MySQL很少使用，仅支持 geometry数据类型 ，支持该类型的存储引擎只有myisam、bdb、 innodb、ndb、archive几种。举个R树在现实领域中能够解决的例子：查找20英里以内所有的餐厅。如果 没有R树你会怎么解决？一般情况下我们会把餐厅的坐标(x,y)分为两个字段存放在数据库中，一个字段记 录经度，另一个字段记录纬度。这样的话我们就需要遍历所有的餐厅获取其位置信息，然后计算是否满 足要求。如果一个地区有100家餐厅的话，我们就要进行100次位置计算操作了，如果应用到谷歌、百度 地图这种超大数据库中，这种方法便必定不可行了。R树就很好的 解决了这种高维空间搜索问题 。它把B 树的思想很好的扩展到了多维空间，采用了B树分割空间的思想，并在添加、删除操作时采用合并、分解 结点的方法，保证树的平衡性。因此，R树就是一棵用来 存储高维数据的平衡树 。相对于B-Tree，R-Tree 的优势在于范围查找。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;索引 / 存储引擎&lt;/th&gt;
&lt;th&gt;MyISAM&lt;/th&gt;
&lt;th&gt;InnoDB&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;R-Tree索引&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;小结&lt;/h4&gt;
&lt;p&gt;使用索引可以帮助我们从海量的数据中快速定位想要查找的数据，不过索引也存在一些不足，比如占用存储空间、降低数据库写操作的性能等，如果有多个索引还会增加索引选择的时间。当我们使用索引时，需要平衡索引的利（提升查询效率）和弊（维护索引所需的代价）。&lt;/p&gt;
&lt;p&gt;在实际工作中，我们还需要基于需求和数据本身的分布情况来确定是否使用索引，尽管&lt;code&gt;索引不是万能的&lt;/code&gt;，但&lt;code&gt;数据量大的时候不使用索引是不可想象的&lt;/code&gt;，毕竟索引的本质，是帮助我们提升数据检索的效率。&lt;/p&gt;
&lt;p&gt;附录：算法的时间复杂度
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-21_23-06-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Java 共享模型之 JMM</title><link>https://zzyang.top/posts/java-jmm/</link><guid isPermaLink="true">https://zzyang.top/posts/java-jmm/</guid><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上一章讲解的 Monitor 主要关注的是访问共享变量时，保证临界区代码的【原子性】&lt;/p&gt;
&lt;p&gt;这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题&lt;/p&gt;
&lt;p&gt;:::info
volatile可以解决 可见性 和 有序性 问题,但不能处理原子性问题&lt;/p&gt;
&lt;p&gt;synchronized 则对于原子性,有序性,可见性, 都可以解决
:::&lt;/p&gt;
&lt;h2&gt;Java 内存模型&lt;/h2&gt;
&lt;p&gt;JMM 即 &lt;code&gt;Java Memory Model&lt;/code&gt;，它定义了主存、工作内存抽象概念，底层对应着 CPU 寄存器、CPU缓存、硬件内存、CPU 指令优化等。&lt;/p&gt;
&lt;p&gt;JMM 体现在以下几个方面&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原子性 - 保证指令不会受到线程上下文切换的影响&lt;/li&gt;
&lt;li&gt;可见性 - 保证指令不会受 cpu 缓存的影响&lt;/li&gt;
&lt;li&gt;有序性 - 保证指令不会受 cpu 指令并行优化的影响&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;可见性&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;退不出的循环&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static boolean run = true;

public static void main(String[] args) throws InterruptedException {

    Thread t = new Thread(()-&amp;gt;{
        while(run){
            // ....
        }
    });
    t.start();

    sleep(1);
    run = false; // 线程t不会如预想的停下来
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info
即便主线程把 run 设为 false，线程 t 一直在循环，程序不会按预期停止。&lt;/p&gt;
&lt;p&gt;写入对其他线程不可见
:::&lt;/p&gt;
&lt;p&gt;为什么呢？分析一下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始状态， t 线程刚开始从主内存读取了 run 的值到工作内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-08_23-04-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;因为 t 线程要频繁从主内存中读取 run 的值，JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中，减少对主存中 run 的访问，提高效率&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-08_23-06-00.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;1 秒之后，main 线程修改了 run 的值，并同步至主存，而 t 是从自己工作内存中的高速缓存中读取这个变量的值，结果永远是旧值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-08_23-06-56.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个线程对主存的数据进行了修改，对于另外一个线程不可见，这就是可见性问题。&lt;/p&gt;
&lt;p&gt;:::tip
JIT（Just-In-Time Compiler，即时编译器）它是 JVM（Java Virtual Machine）的一个重要组件。Java 的特点是 跨平台 —— 写一次，随处运行。这是因为 Java 代码不是直接编译成机器码，而是编译成 字节码（.class 文件），由 JVM 来执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 代码执行的流程&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编译阶段（javac）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Java 源码（.java） → 编译成字节码（.class）。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;运行阶段（JVM）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;解释执行（Interpreter）：一行字节码→翻译成机器码→CPU执行。
（慢，因为要一行一行翻译）&lt;/li&gt;
&lt;li&gt;即时编译（JIT）：把热点代码（经常执行的代码块，比如循环、方法）一次性翻译成本地机器码，存起来，之后直接执行机器码。
（快，接近C++原生性能）
所以 Java 用 解释器 + JIT 混合模式;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JIT 就是 JVM 的“即时编译器”，它能把热点字节码编译成本地机器码，并进行优化&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;热点代码是怎么判定的？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JVM 里有个“计数器”，比如一个方法调用超过 10000 次，JIT 就会认为它是“热点方法(hotspot code)”，进行优化编译。
这也是为什么 Java 程序刚启动时比较慢，但运行一段时间后性能会提升 —— 因为 JIT 起作用了。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/somefuture/p/14272221.html&quot;&gt;可参考该博客&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;volatile（易变）&lt;/p&gt;
&lt;p&gt;它可以用来&lt;strong&gt;修饰成员变量和静态成员变量&lt;/strong&gt;，他可以避免线程从自己的工作缓存中查找变量的值，必须到主存中获取它的值，&lt;strong&gt;线程操作 volatile 变量都是直接操作主存&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test17 {
    volatile static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -&amp;gt; {
            while (run) {
                // ....
            }
        });
        t.start();

        Thread.sleep(1000);
        log.info(&quot;停止t&quot;);
        run = false; // 线程t不会如预想的停下来
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;23:44:09.309 [main] INFO com.thread.concurrent1.Test17 -- 停止t
Process finished with exit code 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用synchronized&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test17 {
    static boolean run = true;

    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -&amp;gt; {
            while (true) {
                // ....
                synchronized (lock) {
                    if (!run) {
                        break;
                    }
                }
            }
        });
        t.start();

        Thread.sleep(1000);
        log.info(&quot;停止t&quot;);
        synchronized (lock) {
            run = false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Java内存模型中，synchronized规定，线程在加锁时， 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。&lt;/p&gt;
&lt;h3&gt;可见性 vs 原子性&lt;/h3&gt;
&lt;p&gt;前面例子体现的实际就是可见性，它保证的是在多个线程之间，一个线程对 volatile 变量的修改对另一个线程可见，不能保证原子性，&lt;strong&gt;仅用在一个写线程，多个读线程的情况&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;上例从字节码理解是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getstatic     run     // 线程 t 获取 run true
getstatic     run     // 线程 t 获取 run true
getstatic     run    // 线程 t 获取 run true
getstatic     run     // 线程 t 获取 run true
putstatic     run     // 线程 main 修改 run 为 false，仅此一次
getstatic     run     // 线程 t 获取 run false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比较一下之前我们将线程安全时举的例子：两个线程一个&lt;code&gt;i++&lt;/code&gt;一个&lt;code&gt;i--&lt;/code&gt;，只能保证看到最新值，不能解决指令交错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 假设i的初始值为0
getstatic      i  // 线程2-获取静态变量i的值 线程内i=0

getstatic      i  // 线程1-获取静态变量i的值 线程内i=0
iconst_1       // 线程1-准备常量1
iadd           // 线程1-自增 线程内i=1
putstatic      i  // 线程1-将修改后的值存入静态变量i 静态变量i=1

iconst_1       // 线程2-准备常量1
isub           // 线程2-自减 线程内i=-1
putstatic      i  // 线程2-将修改后的值存入静态变量i 静态变量i=-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
synchronized 语句块既可以保证代码块的原子性，也同时保证代码块内变量的可见性。但缺点是
synchronized 是属于重量级操作，性能相对更低&lt;/p&gt;
&lt;p&gt;volatile保证可见性，禁止指令重排，不保证原子性。比如 count++ 不是原子操作，用 volatile 不能保证线程安全。
:::&lt;/p&gt;
&lt;p&gt;如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符，线程 t 也能正确看到对 run 变量的修改了，想一想为什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是因为它内部用了 &lt;code&gt;synchronized&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;设计模式-两阶段终止-volatile&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV16J411h7Rd?t=2.7&amp;amp;p=138&quot;&gt;设计模式&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;有序性&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;指令重排&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CPU的层面需要对指令进行并行处理，就会对指令的执行顺序进行调整，这就引起了重排序的问题&lt;/p&gt;
&lt;p&gt;JVM 会在不影响正确性的前提下，可以调整语句的执行顺序&lt;/p&gt;
&lt;p&gt;思考下面一段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int i;
static int j;

// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，至于是先执行 i 还是 先执行 j ，对最终的结果不会产生影响。所以，上面代码真正执行时，既可以是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;i = ...;
j = ...;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;j = ...;
i = ...;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种特性称之为『指令重排』，多线程下『指令重排』会影响正确性。&lt;/p&gt;
&lt;h3&gt;指令重排原理-指令并行优化&lt;/h3&gt;
&lt;p&gt;加工一条鱼需要 50 分钟，只能一条鱼、一条鱼顺序加工...
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-09_00-21-31.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以将每个鱼罐头的加工流程细分为5个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去鳞清洗10分钟&lt;/li&gt;
&lt;li&gt;蒸煮沥水10分钟&lt;/li&gt;
&lt;li&gt;加注汤料10分钟&lt;/li&gt;
&lt;li&gt;杀菌出锅10分钟&lt;/li&gt;
&lt;li&gt;真空封罐10分钟&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-09_00-22-38.png&quot; alt=&quot;&quot; /&gt;
即使只有一个工人，最理想的情况是：他能够在10分钟内同时做好这5件事，因为对第一条鱼的真空装罐，不会影响对第二条鱼的杀菌出锅...&lt;/p&gt;
&lt;p&gt;这个故事讲的是鱼罐头加工的过程。比如说，现在要加工一条鱼，总共需要50分钟。你可以把加工鱼的工人想象成 CPU，而每一条鱼的加工过程，就像 CPU 要执行的一条指令。你会发现，如果每次只处理一条指令，这样的效率其实是比较低的。&lt;/p&gt;
&lt;p&gt;那怎么改进呢？在现实生活中，加工鱼罐头肯定不是靠人工一条条慢慢做的，对吧？通常会把整个流程分成多个工序，每个工序之间采用流水线作业，这样可以大大提高生产效率。&lt;/p&gt;
&lt;p&gt;类似地，我们也可以把一条鱼的加工过程细分成五个步骤。这五个步骤，我查过资料，分别是：第一步，去鳞和清洗，把鱼洗干净；第二步，蒸煮和沥水，把鱼煮熟；第三步，加注汤料，也就是加入调料和配料；第四步，杀菌；最后一步是真空罐装，这样鱼就变成了罐头。&lt;/p&gt;
&lt;p&gt;这样一来，一条鱼的加工过程就被细分成了五个步骤。有同学可能会说，把一条鱼分成五个步骤，每一步花10分钟，加起来还是50分钟，时间并没有减少，效率也没有提高。别急，我们接着往下看。现在我们来看，有一个工人要按照这五个步骤来加工鱼。其实，真正的鱼罐头加工并不是全靠人工完成的，而是在不同阶段会用到不同的机器，比如去鳞机、蒸煮锅、汤料锅、杀菌锅，还有封罐器。&lt;/p&gt;
&lt;p&gt;比如说，当工人在操作封罐机的时候，封罐其实是机器在做的。与此同时，他可以让另一条鱼进入杀菌处理；第三条鱼则可以进行加汤料的步骤，因为前面的步骤已经完成了。第四条鱼可以进行蒸煮处理，第五条鱼则在做清洗。借助这些工具，每个步骤之间互不干扰，就能实现五个步骤同时进行。&lt;/p&gt;
&lt;p&gt;当然，这是一种最理想的情况。在这种情况下，虽然处理一条鱼的总时间没有变化，还是50分钟，但你可以在同一时刻，对多条鱼的不同步骤同时进行操作。这样，总的处理时间没有减少，但你提升了并行度，也就是所谓的吞吐量。单位时间内，你可以完成更多的指令。&lt;/p&gt;
&lt;p&gt;比如说，真空封罐、杀菌、加汤料、沥水、去鳞清洗，这些步骤虽然分别对应不同的鱼，但合起来就相当于同时处理了多条鱼的不同步骤。通过划分步骤，我们实现了效率的提升。CPU也采用了类似的处理机制，它将指令执行过程分为五个阶段，就像我们处理鱼的步骤一样。具体来说，CPU将每条指令分为取指令、指令译码、执行指令、内存访问和数据写回这五个阶段。每个阶段都有其特定的缩写，这种划分方式使得CPU能够更高效地处理指令。&lt;/p&gt;
&lt;p&gt;:::tip
&lt;strong&gt;Clock Cycle Time 时钟周期时间&lt;/strong&gt;
主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time (时钟周期时间),等于主频的倒数,意思是 CPU 能够识别的最小时间单位,比如说
4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的Cycle Time 是 1s&lt;/p&gt;
&lt;p&gt;例如,运行一条加法指令一般需要一个时钟周期时间&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CPI 平均时钟周期数&lt;/strong&gt;
有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction) 指令平均时钟周期数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IPC 即 CPI 的倒数&lt;/strong&gt;
IPC (Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CPU执行时间&lt;/strong&gt;
程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示
程序 CPU 执行时间=指令数&lt;em&gt;CPI&lt;/em&gt;Clock Cycle Time
:::&lt;/p&gt;
&lt;p&gt;事实上，现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢？可以想到指令还可以再划分成一个个更小的阶段，例如，每条指令都可以分为：&lt;code&gt;取指令&lt;/code&gt; - &lt;code&gt;指令译码&lt;/code&gt; - &lt;code&gt;执行指令&lt;/code&gt; - &lt;code&gt;内存访问&lt;/code&gt; - &lt;code&gt;数据写回&lt;/code&gt; 这 5 个阶段
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-09_00-32-18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在不改变程序结果的前提下，这些指令的各个阶段可以通过&lt;strong&gt;重排序&lt;/strong&gt;和&lt;strong&gt;组合&lt;/strong&gt;来实现&lt;strong&gt;指令级并行&lt;/strong&gt;，这一技术在 80&apos;s 中叶到 90&apos;s 中叶占据了计算架构的重要地位。&lt;/p&gt;
&lt;p&gt;:::tip
分阶段，分工是提升效率的关键！
:::&lt;/p&gt;
&lt;p&gt;指令重排的前提是，重排指令不能影响结果，例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );

// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;参考：
Scoreboarding and the Tomasulo algorithm (which is similar to scoreboarding but makes use of
register renaming) are two of the most common techniques for implementing out-of-order execution
and instruction-level parallelism.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;支持流水线的处理器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;现代 CPU 支持多级指令流水线，例如支持同时执行 &lt;code&gt;取指令&lt;/code&gt; - &lt;code&gt;指令译码&lt;/code&gt; - &lt;code&gt;执行指令&lt;/code&gt; - &lt;code&gt;内存访问&lt;/code&gt; - &lt;code&gt;数据写回&lt;/code&gt; 的处理器，就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内，同时运行五条指令的不同阶段（相当于一条执行时间最长的复杂指令），IPC = 1，本质上，流水线技术并不能缩短单条指令的执行时间，但它变相地提高了指令地吞吐率。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：
奔腾四（Pentium 4）支持高达 35 级流水线，但由于功耗太高被废弃&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-09_00-35-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;指令重排问题&lt;/h3&gt;
&lt;p&gt;(指令重排序导致的)诡异的结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int num = 0;
boolean ready = false;

// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}

// 线程2 执行此方法
public void actor2(I_Result r) {
    //这里可能发生指令重排序
    num = 2;
    ready = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;I_Result&lt;/code&gt; 是一个对象，有一个属性 &lt;code&gt;r1&lt;/code&gt; 用来保存结果，问，可能的结果有几种？&lt;/p&gt;
&lt;p&gt;有同学这么分析&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;情况1：线程1 先执行，这时 ready = false，所以进入 else 分支结果为 1&lt;/li&gt;
&lt;li&gt;情况2：线程2 先执行 num = 2，但没来得及执行 ready = true，线程1 执行，还是进入 else 分支,结果为1&lt;/li&gt;
&lt;li&gt;情况3：线程2 执行到 ready = true，线程1 执行，这回进入 if 分支，结果为 4（因为 num 已经执行过了）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但我告诉你，结果还有可能是 0，
这种情况下是：线程2 执行 &lt;code&gt;ready = true&lt;/code&gt;，切换到线程1，进入 if 分支，相加为 0，再切回线程2 执行 num = 2
:::tip
因为 actor2 的指令可能被重排序：ready=true 提前执行了，但 num=2 还没写入主内存。
:::&lt;/p&gt;
&lt;p&gt;这种现象叫做指令重排，是 JIT 编译器在运行时的一些优化，这个现象需要通过大量测试才能复现：&lt;/p&gt;
&lt;h3&gt;指令重排验证&lt;/h3&gt;
&lt;p&gt;借助 openjdk 并发压测工具 &lt;a href=&quot;https://wiki.openjdk.java.net/display/CodeTools/jcstress&quot;&gt;jcstress&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在idea 命令行中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -DartifactId=ordering -Dversion=1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 maven 项目，提供如下测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@JCStressTest
@Outcome(id = {&quot;1&quot;, &quot;4&quot;}, expect = Expect.ACCEPTABLE, desc = &quot;ok&quot;)
@Outcome(id = &quot;0&quot;, expect = Expect.ACCEPTABLE_INTERESTING, desc = &quot;!!!!&quot;)
@State
public class ConcurrencyTest {

    int num = 0;
    boolean ready = false;

    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打包之后，进入 &lt;code&gt;target&lt;/code&gt; 目录，找到 &lt;code&gt;jcstress.jar&lt;/code&gt; 并执行命令：&lt;code&gt;java -jar target/jcstress.jar&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;会看到如下部分的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2 matching test results.
    [OK] cn.itcast.ConcurrencyTest
    (JVM args: [-XX:-TieredCompilation])
Observed state    Occurrences             Expectation       Interpretation
        0            5,404    ACCEPTABLE_INTERESTING      !!!!!
        1        27,874,016          ACCEPTABLE            ok
        4        35,147,721          ACCEPTABLE            ok

    [OK] cn.itcast.ConcurrencyTest
    (JVM args: [])
Observed state    Occurrences             Expectation       Interpretation
        0            1,568    ACCEPTABLE_INTERESTING      !!!!!
        1        17,913,929          ACCEPTABLE            ok
        4        34,664,864          ACCEPTABLE            ok
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行了34,664,864 次测试，结果是4，执行了17,913,929 次测试，结果是1，也有1568次出现结果为0，确实发生了指令重排序现象。&lt;/p&gt;
&lt;h3&gt;指令重排-禁用&lt;/h3&gt;
&lt;p&gt;volatile禁用指令重排&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;volatile 修饰的变量，可以禁用指令重排&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@JCStressTest
@Outcome(id = {&quot;1&quot;, &quot;4&quot;}, expect = Expect.ACCEPTABLE, desc = &quot;ok&quot;)
@Outcome(id = &quot;0&quot;, expect = Expect.ACCEPTABLE_INTERESTING, desc = &quot;!!!!&quot;)
@State
public class ConcurrencyTest {

    int num = 0;
    volatile boolean ready = false;

    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RUN RESULTS:
-------------------------------------------------------------------------------
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.
0 matching test results.
*** FAILED tests
Strong asserts were violated. Correct implementations should have no assert failures here.
0 matching test results.
*** ERROR tests
Tests break for some reason, other than failing the assert. Correct implementations should have none.
0 matching test results.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;为什么只给 ready 加 volatile 就够了，而不用给 num 加?&lt;/p&gt;
&lt;p&gt;只加在volatile变量上，可以防止之前的代码被重排序，实际上是加了一个写屏障，写屏障就能够保证之前的所有代码不会被排到ready的后面去，所以加一个就够了。
:::&lt;/p&gt;
&lt;h2&gt;volatile 原理&lt;/h2&gt;
&lt;p&gt;volatile 的底层实现原理是内存屏障，&lt;code&gt;Memory Barrier（Memory Fence）&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 volatile 变量的 写指令后会加入写屏障 : 保证在该屏障之前的，对共享变量的改动，都同步到主存当中&lt;/li&gt;
&lt;li&gt;对 volatile 变量的 读指令前会加入读屏障 : 在该屏障之后，对共享变量的读取，加载的是主存中最新数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;保证可见性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;如何保证可见性&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写屏障（sfence）保证在该屏障之前的，对共享变量的改动，都同步到主存当中&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public void actor2(I_Result r) {
    num = 2;
    ready = true; // ready 是 volatile 赋值带写屏障
    // 写屏障
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;而读屏障（lfence）保证在该屏障之后，对共享变量的读取，加载的是主存中最新数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public void actor1(I_Result r) {
    // 读屏障
    // ready 是 volatile 读取值带读屏障
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-10_00-13-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;保证有序性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;如何保证有序性&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写屏障会确保指令重排序时，不会将写屏障之前的代码排在写屏障之后&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public void actor2(I_Result r) {
    num = 2;
    ready = true; // ready 是 volatile 赋值带写屏障
    // 写屏障
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;读屏障会确保指令重排序时，不会将读屏障之后的代码排在读屏障之前&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public void actor1(I_Result r) {
    // 读屏障
    // ready 是 volatile 读取值带读屏障
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-10_00-29-12.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还是那句话，不能解决指令交错：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写屏障仅仅是保证之后的读能够读到最新的结果，但不能保证读跑到它前面去&lt;/li&gt;
&lt;li&gt;而有序性的保证也只是保证了本线程内相关代码不被重排序
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-10_00-30-26.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;volatile不能解决原子性问题  即指令的交错执行，只能保证本线程内的相关代码不被重排序&lt;/p&gt;
&lt;p&gt;volatile只能适用于一个线程写，多个线程读的场景。&lt;/p&gt;
&lt;h3&gt;double-checked locking 问题&lt;/h3&gt;
&lt;p&gt;以著名的 double-checked locking 单例模式为例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;

    public static Singleton getInstance() {
        if(INSTANCE == null) { // t2
            // 首次访问会同步，而之后的使用没有 synchronized
            synchronized(Singleton.class) {
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只在 synchronized 中创建实例（每次都锁），虽然线程安全，但性能差。双重检查的目的是：只有第一次创建实例时才加锁，之后直接返回实例。&lt;/p&gt;
&lt;p&gt;以上的实现特点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;懒惰实例化&lt;/li&gt;
&lt;li&gt;首次使用 getInstance() 才使用 synchronized 加锁，后续使用时无需加锁&lt;/li&gt;
&lt;li&gt;有隐含的，但很关键的一点：第一个 if 使用了 INSTANCE 变量，是在同步块之外&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这段代码其实是有问题的，完全在synchronized作用域内的 共享变量 才能保证其 原子性,可见性,有序性。这里 &lt;code&gt;INSTANCE&lt;/code&gt; 并没有完全在 &lt;code&gt;synchronized&lt;/code&gt; 作用域内,所以对其可能发生重排序;&lt;/p&gt;
&lt;p&gt;但在多线程环境下，上面的代码是有问题的，getInstance 方法对应的字节码为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: getstatic     #2               // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull     37               // 如果 INSTANCE != null，跳到 37 直接返回
6: ldc           #3               // class cn/itcast/n5/Singleton
8: dup                           // 复制栈顶元素（类对象），保证后面 monitorenter/monitorexit 用
9: astore_0
10: monitorenter
11: getstatic    #2               // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull    27
17: new          #3               // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4               // Method &quot;&amp;lt;init&amp;gt;&quot;:()V
24: putstatic    #2               // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto         37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic    #2               // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;17 表示创建对象，将对象引用入栈 // new Singleton&lt;/li&gt;
&lt;li&gt;20 表示复制一份对象引用 // 引用地址&lt;/li&gt;
&lt;li&gt;21 表示利用一个对象引用，调用构造方法&lt;/li&gt;
&lt;li&gt;24 表示利用一个对象引用，赋值给 static INSTANCE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也许 jvm 会优化为：先执行 24，再执行 21。如果两个线程 t1，t2 按如下时间序列执行：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-11_00-53-31.png&quot; alt=&quot;&quot; /&gt;
这段字节码的隐患在于 &lt;code&gt;putstatic&lt;/code&gt; 可能在 &lt;code&gt;&amp;lt;init&amp;gt;&lt;/code&gt; 之前执行，如果没有 &lt;code&gt;volatile&lt;/code&gt; 修饰 &lt;code&gt;INSTANCE&lt;/code&gt;，就可能导致另一个线程读到“半初始化”的对象。&lt;/p&gt;
&lt;p&gt;关键在于 0: getstatic 这行代码在 &lt;code&gt;monitor&lt;/code&gt; 控制之外，它就像之前举例中不守规则的人，可以越过 &lt;code&gt;monitor&lt;/code&gt; 读取&lt;code&gt;INSTANCE&lt;/code&gt; 变量的值 .&lt;/p&gt;
&lt;p&gt;这时 t1 还未完全将构造方法执行完毕，如果在构造方法中要执行很多初始化操作，那么 t2 拿到的是将是一个未初始化完毕的单例 .&lt;/p&gt;
&lt;p&gt;对 &lt;code&gt;INSTANCE&lt;/code&gt; 使用 &lt;code&gt;volatile&lt;/code&gt; 修饰即可，可以禁用指令重排，但要注意在 &lt;code&gt;JDK 5&lt;/code&gt; 以上的版本的 &lt;code&gt;volatile&lt;/code&gt; 才会真正有效 .&lt;/p&gt;
&lt;p&gt;一个共享变量完全被synchronized 保护，那么这个变量就不会出现原子、有序、可见性问题。但是在以上代码中有问题，是因为这个共享变量并没有完全的被synchronized 保护
，synchronized 的外面还是有对 INSTANCE 共享变量的使用&lt;/p&gt;
&lt;p&gt;:::info
synchronized是可以保证原子性、可见性、有序性的，但是前提是这个共享变量都交给synchronized来管理
:::&lt;/p&gt;
&lt;h4&gt;dcl问题解决&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;volatile&lt;/code&gt; 修饰 &lt;code&gt;INSTANCE&lt;/code&gt; 变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class Singleton {
    private Singleton() { }
    private static volatile Singleton INSTANCE = null;

    public static Singleton getInstance() {
        // 实例没创建，才会进入内部的 synchronized代码块
        if (INSTANCE == null) {
            synchronized (Singleton.class) { // t2
                // 也许有其它线程已经创建实例，所以再判断一次
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字节码上看不出来 volatile 指令的效果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ------------------------------&amp;gt; 加入对 INSTANCE 变量的读屏障
0: getstatic     #2                      // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull     37
6: ldc           #3                      // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter  ------------------&amp;gt; 保证原子性、可见性
11: getstatic     #2                      // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull     27
17: new           #3                      // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4                      // Method &quot;&amp;lt;init&amp;gt;&quot;:()V
24: putstatic     #2                      // Field INSTANCE:Lcn/itcast/n5/Singleton;
// ------------------------------&amp;gt; 加入对 INSTANCE 变量的写屏障
27: aload_0
28: monitorexit  ------------------&amp;gt; 保证原子性、可见性
29: goto          37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic     #2                      // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上面的注释内容所示，读写 &lt;code&gt;volatile&lt;/code&gt; 变量时会加入内存屏障（Memory Barrier（Memory Fence）），保证下面两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可见性
&lt;ul&gt;
&lt;li&gt;写屏障（sfence）保证在该屏障之前的 t1 对共享变量的改动，都同步到主存当中&lt;/li&gt;
&lt;li&gt;而读屏障（lfence）保证在该屏障之后 t2 对共享变量的读取，加载的是主存中最新数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有序性
&lt;ul&gt;
&lt;li&gt;写屏障会确保指令重排序时，不会将写屏障之前的代码排在写屏障之后&lt;/li&gt;
&lt;li&gt;读屏障会确保指令重排序时，不会将读屏障之后的代码排在读屏障之前&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-09-12_00-25-53.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;happens-before&lt;/h2&gt;
&lt;p&gt;happens-before就是对共享变量可见性的总结（七个规则）&lt;/p&gt;
&lt;p&gt;happens-before 规定了对共享变量的写操作对其它线程的读操作可见，它是可见性与有序性的一套规则总结，抛开以下 happens-before 规则，IMM 并不能保证一个线程对共享变量的写，对于其它线程对该共享变量的读可见。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程解锁 m 之前对变量的写，对于接下来对 m 加锁的其它线程对该变量的读可见&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    static int x;
    static Object m = new Object();
    new Thread(() -&amp;gt; {
        synchronized(m) {
            x = 10;
        }
    }, &quot;t1&quot;).start();
    new Thread(() -&amp;gt; {
        synchronized(m) {
            System.out.println(x);
        }
    }, &quot;t2&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;线程对 volatile 变量的写，对接下来其它线程对该变量的读可见&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;volatile static int x;

new Thread(() -&amp;gt; {
    x = 10;
}, &quot;t1&quot;).start();

new Thread(() -&amp;gt; {
    System.out.println(x);
}, &quot;t2&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;线程 start 前对变量的写，对该线程开始后对该变量的读可见&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;static int x;
x = 10;
new Thread(() -&amp;gt; {
    System.out.println(x);
}, &quot;t2&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;线程结束前对变量的写，对其它线程得知它结束后的读可见（比如其它线程调用 t1.isAlive() 或 t1.join() 等待它结束）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    static int x;
    Thread t1 = new Thread(() -&amp;gt; {
        x = 10;
    }, &quot;t1&quot;);
    t1.start();     // 线程结束之前，就会把共享变量的值同步到主存中
    t1.join();
    System.out.println(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;线程 t1 打断 t2（interrupt）前对变量的写，对于其他线程得知 t2 被打断后对变量的读可见（通过 t2.interrupted 或 t2.isInterrupted）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;static int x;

public static void main(String[] args) {
    Thread t2 = new Thread(() -&amp;gt; {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println(x);
                break;
            }
        }
    }, &quot;t2&quot;);
    t2.start();

    new Thread(() -&amp;gt; {
        sleep(1);
        x = 10;
        t2.interrupt(); // 打断之前对变量的修改，对其他线程是可见的
    }, &quot;t1&quot;).start();

    while (!t2.isInterrupted()) {
        Thread.yield();
    }
    System.out.println(x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对变量默认值（0，false，null）的写，对其它线程对该变量的读可见&lt;/li&gt;
&lt;li&gt;具有传递性，如果x hb-&amp;gt; y并且y hb-&amp;gt; z 那么有x hb-&amp;gt; z，配合volatile的防指令重排，有下面的例子&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    volatile static int x;
    static int y;
    new Thread(() -&amp;gt; {
        y = 10;
        x = 20;     // 写屏障会把，写屏障之前的所有操作都同步到主存中并且还不会重排序，即使之前操作不是volatile
    }, &quot;t1&quot;).start();
    new Thread(() -&amp;gt; {
        // x=20 对 t2 可见，同时 y=10 也对 t2 可见
        System.out.println(x);
    }, &quot;t2&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可见性 - 由 JVM 缓存优化引起&lt;/li&gt;
&lt;li&gt;有序性 - 由 JVM 指令重排序优化引起&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Java 共享模型之管程（Monitor）</title><link>https://zzyang.top/posts/java-monitor/</link><guid isPermaLink="true">https://zzyang.top/posts/java-monitor/</guid><pubDate>Fri, 05 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;本章内容&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享资源问题
&lt;ul&gt;
&lt;li&gt;多线程并发访问共享资源时可能存在的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;synchronized
&lt;ul&gt;
&lt;li&gt;解决多线程并发访问的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线程安全分析
&lt;ul&gt;
&lt;li&gt;知道怎么样的代码编写是线程安全的，怎样的代码编写是存在线程安全隐患的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Monitor
&lt;ul&gt;
&lt;li&gt;从源码的角度讲解管程的底层实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;wait/notify&lt;/li&gt;
&lt;li&gt;线程状态转换
&lt;ul&gt;
&lt;li&gt;线程六种状态如何转换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;活跃性
&lt;ul&gt;
&lt;li&gt;死锁、活锁、饥饿&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ReentrantLock&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;共享资源问题&lt;/h2&gt;
&lt;h3&gt;Java体现&lt;/h3&gt;
&lt;p&gt;问：两个线程对初始值为 0 的静态变量一个做自增，一个做自减，各做 5000 次，结果是 0 吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test17&quot;)
public class Test17 {
    static int counter = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                counter++;
            }
        }, &quot;t1&quot;);

        Thread t2 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                counter--;
            }
        }, &quot;t2&quot;);

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.info(&quot;counter = {}&quot;, counter);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;21:26:18.126 [main] INFO com.thread.concurrent1.Test8 -- counter = -697
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;结论：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于分时系统造成的线程切换而导致的安全问题。&lt;/p&gt;
&lt;h3&gt;问题分析&lt;/h3&gt;
&lt;p&gt;以上的结果可能是正数、负数、零。为什么呢？因为 Java 中对静态变量的自增，自减并不是原子操作，要彻底理解，必须从字节码来进行分析&lt;/p&gt;
&lt;p&gt;例如对于i++ 而言（i 为静态变量），实际会产生如下的四条 JVM 字节码指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而对应i--也是类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 Java 的内存模型如下，完成静态变量的自增，自减需要在主存和工作内存中进行数据交换：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page35_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果是单线程以上 8 行代码是顺序执行（不会交错）没有问题：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-15_21-29-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但多线程下这 8 行代码可能交错运行。&lt;/p&gt;
&lt;p&gt;出现负数的情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-15_21-29-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;出现正数的情况：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-15_21-30-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;临界区 Critical Section&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个程序运行多个线程本身是没有问题的&lt;/li&gt;
&lt;li&gt;问题出在多个线程访问共享资源
&lt;ul&gt;
&lt;li&gt;多个线程读共享资源其实也没有问题&lt;/li&gt;
&lt;li&gt;在多个线程对共享资源读写操作时发生指令交错，就会出现问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一段代码块内如果存在对共享资源的多线程读写操作，称这段代码块为&lt;strong&gt;临界区&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么在这个临界区对共享资源的操作，我们就称发生了竞态条件&lt;/p&gt;
&lt;p&gt;例如，下面代码中的临界区&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int counter = 0;

static void increment()
// 临界区
{
    counter++;
}

static void decrement()
// 临界区
{
    counter--;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;竞态条件 Race Condition&lt;/h3&gt;
&lt;p&gt;多个线程在临界区内执行，由于代码的&lt;strong&gt;执行序列不同&lt;/strong&gt;而导致结果无法预测，称之为发生了&lt;strong&gt;竞态条件&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;synchronized 解决方案&lt;/h2&gt;
&lt;p&gt;为了避免临界区的竞态条件发生，有多种手段可以达到目的:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻塞式的解决方案: &lt;code&gt;synchronized&lt;/code&gt;，&lt;code&gt;Lock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;非阻塞式的解决方案: &lt;code&gt;原子变量&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;synchronized&lt;/code&gt;，即俗称的&lt;em&gt;对象锁&lt;/em&gt;。它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】，其它线程再想获取这个【对象锁】时就会阻塞住，进入 &lt;code&gt;BLOCKED&lt;/code&gt; 状态。这样就能保证拥有锁的线程可以安全的执行临界区内的代码，不用担心线程上下文切换&lt;/p&gt;
&lt;p&gt;:::warning
虽然 java 中互斥和同步都可以采用 &lt;code&gt;synchronized&lt;/code&gt; 关键字来完成, 但它们还是有区别的:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码&lt;/li&gt;
&lt;li&gt;同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;synchronized(对象) {  // 得保证多个线程是对同一个对象来使用对象锁
 	临界区代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;同一时刻，只能有一个线程持有这个对象锁，其他线程会进入阻塞状态（Blocked）&lt;/li&gt;
&lt;li&gt;括号内的对象不能为空，必须 new 一个&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;synchronized解决&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test8 {
    static int counter = 0;
    private static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                synchronized (object) {
                    counter++;
                }
            }
        }, &quot;t1&quot;);

        Thread t2 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                synchronized (object) {
                    counter--;
                }
            }
        }, &quot;t2&quot;);

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.info(&quot;counter = {}&quot;, counter);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;synchronized-理解&lt;/h3&gt;
&lt;p&gt;你可以做这样的类比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;synchronized(对象)中的对象，可以想象为一个房间（room），有唯一入口（门）房间只能一次进入一人进行计算，线程 t1，t2 想象成两个人&lt;/li&gt;
&lt;li&gt;当线程 t1 执行到synchronized(room)时就好比 t1 进入了这个房间，并锁住了门拿走了钥匙，在门内执行count++代码&lt;/li&gt;
&lt;li&gt;这时候如果 t2 也运行到了synchronized(room)时，它发现门被锁住了，只能在门外等待，发生了上下文切换，阻塞住了&lt;/li&gt;
&lt;li&gt;这中间即使 t1 的 cpu 时间片不幸用完，被踢出了门外（不要错误理解为锁住了对象就能一直执行下去哦），这时门还是锁住的，t1 仍拿着钥匙，t2 线程还在阻塞状态进不来，只有下次轮到 t1 自己再次获得时间片时才能开门进入&lt;/li&gt;
&lt;li&gt;当 t1 执行完synchronized{}块内的代码，这时候才会从 obj 房间出来并解开门上的锁，唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间，锁住了门拿上钥匙，执行它的count--代码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page40_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用图表示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-15_22-03-35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思考&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;synchronized 实际是用对象锁保证了临界区内代码的原子性，临界区内的代码对外是不可分割的，不会被线程切换所打断。&lt;/p&gt;
&lt;p&gt;为了加深理解，请思考下面的问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果把synchronized(obj)放在 for 循环的外面，如何理解？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;答：放在 for 循环外部会把整个 for 循环的代码当成一个原子操作，会执行 5000 次 ++ 或 -- 操作后才会释放锁&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 t1 线程synchronized(obj1)而 t2 线程synchronized(obj2)会怎样运作？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;答：不会保证临界区内代码的原子性。没有锁住同一个对象，无法保护共享资源，相当于是两把不同的锁&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 t1 线程synchronized(obj)而 t2 线程没有加会怎么样？如何理解？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;答：无法保证临界区内代码的原子性。因为 t2 线程没有用 synchronized(obj)加锁会导致它不会被阻塞住。要对临界区钟的代码进行保护就必须多个线程都对同一个对象加锁&lt;/p&gt;
&lt;h4&gt;锁对象面向对象改进&lt;/h4&gt;
&lt;p&gt;我们可以把 需要保护的共享变量放入一个类 中统一管理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test17&quot;)
public class Test17 {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new Lock();
        Thread t1 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                synchronized (lock) {
                    lock.increment();
                }
            }
        }, &quot;t1&quot;);

        Thread t2 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                synchronized (lock) {
                    lock.decrement();
                }
            }
        }, &quot;t2&quot;);

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug(&quot;counter = {}&quot;, lock.getCounter());
    }
}



class Lock {
    private int counter = 0;

    /**
     * ++ 操作
     */
    public void increment() {
        synchronized (this) {
            counter++;
        }
    }

    /**
     * -- 操作
     */
    public void decrement() {
        synchronized (this) {
            counter--;
        }
    }

    /**
     * 获取结果
     *
     * @return 结果值
     */
    public int getCounter() {
        // 为了保证获取值时得到一个准确的结果而不是一个中间结果。也需要进行加锁！
        synchronized (this) {
            return counter;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方法上的 synchronized&lt;/h3&gt;
&lt;p&gt;加在成员方法上，等价于锁住了 this 对象。(synchronized只能锁对象！)&lt;/p&gt;
&lt;p&gt;加在静态方法上，等价于锁住了类对象。&lt;/p&gt;
&lt;p&gt;synchronized 加在成员方法上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test{
    public synchronized void test() {

    }
}

// 等价于
class Test{
    public void test() {
        synchronized(this) {

        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;synchronized 加在静态方法上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test{
    public synchronized static void test() {

    }
}

// 等价于
class Test{
    public static void test() {
        synchronized(Test.class) {

        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info
synchronized(Test.class)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁住的是 类对象（Class 对象）。&lt;/li&gt;
&lt;li&gt;这个锁是 全局的（只要是同一个 Test.class，不管哪个线程、哪个实例），都会竞争同一把锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;            ┌───────────────────┐
线程A  ---&amp;gt; │   Test.class锁     │ &amp;lt;--- 线程B
            └───────────────────┘
                  ▲
                  │
    test1.method1()    test2.method1()
   （不同对象实例都会竞争同一把锁）

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;synchronized(this)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁住的是当前实例对象，不同实例之间互不影响。&lt;/li&gt;
&lt;li&gt;如果有两个 Test 对象，线程 A 锁住 test1，线程 B 还是能同时锁住 test2。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;线程A ---&amp;gt; [ test1实例锁 ]             [ test2实例锁 ] &amp;lt;--- 线程B
           （互不干扰）                 （互不干扰）

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;synchronized(this)示例：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;✅ 安全的情况（同一个对象）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Counter {
    private int count = 0;

    public void increment() {
        synchronized(this) {
            count++;
        }
    }
}

Counter c = new Counter();
new Thread(c::increment).start();
new Thread(c::increment).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里两个线程操作的是同一个对象 c，所以 &lt;code&gt;count++&lt;/code&gt; 会被同步，不会出现线程安全问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;⚠️ 不安全的情况（多个对象）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Counter {
    private int count = 0;

    public void increment() {
        synchronized(this) {
            count++;
        }
    }
}

Counter c1 = new Counter();
Counter c2 = new Counter();
new Thread(c1::increment).start();
new Thread(c2::increment).start();

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里两个线程用的是不同对象（c1 和 c2），锁对象也不一样。
所以它们同时执行 &lt;code&gt;count++&lt;/code&gt;，不会互相阻塞，可能就有线程安全问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;synchronized(this)&lt;/code&gt; 线程安全的前提：所有访问共享资源的线程，必须锁住同一个对象。&lt;/li&gt;
&lt;li&gt;如果可能有多个对象实例同时访问共享资源，就应该考虑：
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;synchronized(someClass.class)&lt;/code&gt; (类锁，全局唯一 )，&lt;/li&gt;
&lt;li&gt;或者自己定义一个全局锁对象 &lt;code&gt;private static final object LOCK = new object();&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test {
    public void method1() {
        synchronized(Test.class) {
            System.out.println(Thread.currentThread().getName() + &quot; got class lock&quot;);
        }
    }

    public void method2() {
        synchronized(this) {
            System.out.println(Thread.currentThread().getName() + &quot; got instance lock&quot;);
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;两个线程用不同对象调用 method1() → 会互相等待（因为是同一个 Test.class 锁）。&lt;/li&gt;
&lt;li&gt;两个线程用不同对象调用 method2() → 不会互相等待（锁的是不同实例）。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;相关面试题💡&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;构造方法可以用 synchronized 修饰吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;构造方法不能使用 &lt;code&gt;synchronized&lt;/code&gt; 关键字修饰。不过，可以在构造方法内部使用 &lt;code&gt;synchronized&lt;/code&gt; 代码块。&lt;/p&gt;
&lt;p&gt;另外，&lt;strong&gt;构造方法本身是线程安全的&lt;/strong&gt;，但如果在构造方法中涉及到共享资源的操作，就需要采取适当的同步措施来保证整个构造过程的线程安全&lt;/p&gt;
&lt;h3&gt;synchronized加在方法上-线程八锁&lt;/h3&gt;
&lt;p&gt;其实就是考察 synchronized 锁住的是哪个对象&lt;/p&gt;
&lt;p&gt;情况1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test9 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -&amp;gt; {
            n1.a();
        }).start();

        new Thread(() -&amp;gt; {
            n1.b();
        }).start();
    }
}

@Slf4j
class Number {
    public synchronized void a() {
        log.info(&quot;1&quot;);
    }

    public synchronized void b() {
        log.info(&quot;2&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;锁住的是同一个 this 对象，有可能先打印 1 再打印 2；也可能先打印 2 再打印 1。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;情况2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test9 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -&amp;gt; {
            log.info(&quot;begin&quot;);
            try {
                n1.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(() -&amp;gt; {
            log.info(&quot;begin&quot;);
            n1.b();
        }).start();
    }
}

@Slf4j
class Number {
    public synchronized void a() throws InterruptedException {
        Thread.sleep(1000);     // sleep() 不会让出锁资源，只会让线程进入阻塞状态
        log.info(&quot;1&quot;);
    }

    public synchronized void b() {
        log.info(&quot;2&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;p&gt;第一种情况：线程 1 先获得锁，此时会先睡眠 1s，再打印 1。然后线程 2 再打印 2&lt;/p&gt;
&lt;p&gt;第二种情况：线程 2 先获得锁，此时会先打印 2。然后线程 1 获得锁，此时会先睡眠 1s，再打印 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;情况3：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test9 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -&amp;gt; {
            try {
                n1.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(() -&amp;gt; {
            n1.b();
        }).start();

        new Thread(() -&amp;gt; {
            n1.c();
        }).start();
    }
}

@Slf4j
class Number {
    public synchronized void a() throws InterruptedException {
        Thread.sleep(1000);     // sleep() 不会让出锁资源，只会让线程进入阻塞状态
        log.info(&quot;1&quot;);
    }

    public synchronized void b() {
        log.info(&quot;2&quot;);
    }

    public void c() {
        log.info(&quot;3&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 3 1s 12
// 23 1s 1
// 32 1s 1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;第一种情况：先打印3，一秒后打印 1，最后打印 2&lt;/p&gt;
&lt;p&gt;第二种情况：先打印2、3，然后 1s 后打印 1&lt;/p&gt;
&lt;p&gt;第三种情况：先打印 3，1s 后打印 1，最后打印 2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;情况4：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test9 {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -&amp;gt; {
            try {
                n1.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(() -&amp;gt; {
            n2.b();
        }).start();
    }
}

@Slf4j
class Number {
    public synchronized void a() throws InterruptedException {
        Thread.sleep(1000);     // sleep() 不会让出锁资源，只会让线程进入阻塞状态
        log.info(&quot;1&quot;);
    }

    public synchronized void b() {
        log.info(&quot;2&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;p&gt;锁住的不是同一个对象。所以无论先执行线程 1 还是线程 2。由于线程 1 要 Sleep()，所以时间片会分给线程 2。 会先打印 2，再打印 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;情况5：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test9 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -&amp;gt; {
            try {
                n1.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(() -&amp;gt; {
            n1.b();
        }).start();
    }
}

@Slf4j
class Number {
    public static synchronized void a() throws InterruptedException {
        Thread.sleep(1000);     // sleep() 不会让出锁资源，只会让线程进入阻塞状态
        log.info(&quot;1&quot;);
    }

    public synchronized void b() {
        log.info(&quot;2&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果&lt;/p&gt;
&lt;p&gt;线程 1 调用 a 方法时，锁住的是类对象。线程 2 调用 b 方法时，锁住的是 n1 对象。因为锁住的不是同一个对象，所以它们之间不互斥。先运行 2，过 1s 后再运行 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;情况6：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test9 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -&amp;gt; {
            try {
                n1.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(() -&amp;gt; {
            n1.b();
        }).start();
    }
}

@Slf4j
class Number {
    public static synchronized void a() throws InterruptedException {
        Thread.sleep(1000);     // sleep() 不会让出锁资源，只会让线程进入阻塞状态
        log.info(&quot;1&quot;);
    }

    public static synchronized void b() {
        log.info(&quot;2&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果&lt;/p&gt;
&lt;p&gt;类对象整个内存中只有一份，所以锁定的是同一个对象。&lt;/p&gt;
&lt;p&gt;第一种情况：过 1s 后打印 1，再打印 2&lt;/p&gt;
&lt;p&gt;第二种情况：先打印 2，过 1s 后再打印 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;情况7：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test9 {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -&amp;gt; {
            try {
                n1.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(() -&amp;gt; {
            n2.b();
        }).start();
    }
}

@Slf4j
class Number {
    public static synchronized void a() throws InterruptedException {
        Thread.sleep(1000);     // sleep() 不会让出锁资源，只会让线程进入阻塞状态
        log.info(&quot;1&quot;);
    }

    public synchronized void b() {
        log.info(&quot;2&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果&lt;/p&gt;
&lt;p&gt;线程 1 锁定的是类对象；线程 2 锁定的是 n2 对象。锁住的不是同一个对象
总是先 2 再过 1s 后打印 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;情况8：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test9 {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -&amp;gt; {
            try {
                n1.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(() -&amp;gt; {
            n2.b();
        }).start();
    }
}

@Slf4j
class Number {
    public static synchronized void a() throws InterruptedException {
        Thread.sleep(1000);
        log.info(&quot;1&quot;);
    }

    public static synchronized void b() {
        log.info(&quot;2&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果&lt;/p&gt;
&lt;p&gt;因为是静态方法，锁的是类对象。所以线程 1 和线程 2 锁定的是同一个对象&lt;/p&gt;
&lt;p&gt;第一种情况：过 1s 后打印 1，再打印 2&lt;/p&gt;
&lt;p&gt;第二种情况：先打印 2，过 1s 后再打印 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;变量的线程安全分析&lt;/h3&gt;
&lt;h4&gt;成员变量和静态变量是否线程安全?&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果它们&lt;strong&gt;没有共享&lt;/strong&gt;，则线程安全&lt;/li&gt;
&lt;li&gt;如果它们&lt;strong&gt;被共享&lt;/strong&gt;了，根据它们的状态是否能够改变，又分两种情况
&lt;ul&gt;
&lt;li&gt;如果只有读取操作，则线程安全&lt;/li&gt;
&lt;li&gt;如果有读写操作，则这段代码是临界区，需要考虑线程安全&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;局部变量是否线程安全?&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;局部变量是线程安全的&lt;/li&gt;
&lt;li&gt;但局部变量引用的对象则未必
&lt;ul&gt;
&lt;li&gt;如果引用的对象没有逃离方法的作用访问，它是线程安全的&lt;/li&gt;
&lt;li&gt;如果引用的对象逃离方法的作用范围，需要考虑线程安全&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;局部变量线程安全分析&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;如果局部变量没有引用对象&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void test1() {
    int i = 10;
    i++; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个线程调用 test1() 方法时,局部变量 i 都会在每个线程的栈帧内存中被创建多份，因此不存在共享！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
        stack=1, locals=1, args_size=0
        0: bipush            10
        2: istore_0
        3: iinc               0, 1
        6: return
    LineNumberTable:
        line 10: 0
        line 11: 3
        line 12: 6
    LocalVariableTable:
        Start Length Slot Name Signature
            3      4     0    i   I
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page49_image.png&quot; alt=&quot;&quot; /&gt;
:::warning
局部变量的 i++操作在底层字节码文件中涉及一步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iinc  // 通过 iinc 指令自增
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;静态变量的 i++ 操作在底层字节码文件中涉及四步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;不同线程的虚拟机栈的栈帧的局部变量不共享&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;如果局部变量引用了对象&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class TestThreadSafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i &amp;lt; THREAD_NUMBER; i++) {
            new Thread(() -&amp;gt; test.method1(LOOP_NUMBER), &quot;Thread&quot; + (i + 1)).start();
        }
    }
}

class ThreadUnsafe {
    // 成员变量
    ArrayList&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();

    public void method1(int loopNumber) {
        for (int i = 0; i &amp;lt; loopNumber; i++) {
            // 临界区，会产生竞态条件
            method2();
            method3();
        }
    }

    private void method2() {
        list.add(&quot;1&quot;);
    }

    private void method3() {
        list.remove(0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，可能存在线程2 还未 add，线程1 就 remove。报错如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Exception in thread &quot;Thread2&quot; java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
	at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:100)
	at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:106)
	at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:302)
	at java.base/java.util.Objects.checkIndex(Objects.java:385)
	at java.base/java.util.ArrayList.remove(ArrayList.java:551)
	at com.thread.concurrent1.ThreadUnsafe.method3(TestThreadSafe.java:45)
	at com.thread.concurrent1.ThreadUnsafe.method1(TestThreadSafe.java:36)
	at com.thread.concurrent1.TestThreadSafe.lambda$main$0(TestThreadSafe.java:23)
	at java.base/java.lang.Thread.run(Thread.java:1583)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add 操作不是原子性的，add 方法内部会去更新集合的 size 值。可能 t1 线程将数据加入集合，但是还没更新 size 的时候，时间片就被 t2 线程抢走了。t2 线程执行完 add 后并将 size 值更新成 1。此时时间片又被 t1 线程抢走，size 的值再次被设置为 1。这就导致 remove 的时候会有一个线程报索引越界。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分析:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无论哪个线程中的 &lt;code&gt;method2&lt;/code&gt; 引用的都是同一个对象中的 &lt;code&gt;list&lt;/code&gt; 成员变量，此时临界区产生了&lt;/li&gt;
&lt;li&gt;&lt;code&gt;method3&lt;/code&gt; 与 &lt;code&gt;method2&lt;/code&gt; 分析相同&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page51_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果将 list 修改为局部变量，并且此局部变量的引用没有暴露给外部：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 局部变量线程安全
 */
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt; loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    private void method2(List&amp;lt;String&amp;gt; list) {
        list.add(&quot;1&quot;);
    }

    private void method3(List&amp;lt;String&amp;gt; list) {
        list.remove(0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么，无论运行多少遍，都不会出现上面的索引越界异常。&lt;/p&gt;
&lt;p&gt;分析:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt; 是局部变量,每个线程调用时会创建其不同实例,没有共享&lt;/li&gt;
&lt;li&gt;而 &lt;code&gt;method2&lt;/code&gt; 的参数是从 &lt;code&gt;method1&lt;/code&gt; 中传递过来的,与 &lt;code&gt;method1&lt;/code&gt; 中引用同一个对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;method3&lt;/code&gt; 的参数分析与 &lt;code&gt;method2&lt;/code&gt; 相同&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page52_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;如果把 method2 和 method3 的方法修改为 public 会不会出现线程安全问题？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;情况一：有其它线程调用 method2 和 method3&lt;/li&gt;
&lt;li&gt;情况二：在 情况1 的基础上，为 ThreadSafe 类添加子类，子类覆盖 method2 或 method3 方法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class ThreadSafe {
    public final void method1(int loopNumber) {
        List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt; loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    
    private void method2(List&amp;lt;String&amp;gt; list) {
        list.add(&quot;1&quot;);
    }
    
    private void method3(List&amp;lt;String&amp;gt; list) {
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(List&amp;lt;String&amp;gt; list) {
        new Thread(() -&amp;gt; {
            list.remove(0);
        }).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;从这个例子可以看出 &lt;code&gt;private&lt;/code&gt; 或 &lt;code&gt;final&lt;/code&gt; 提供【安全】的意义所在，请体会开闭原则中的【闭】&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;ThreadSafe：线程安全 ✅（因为 list 是局部变量，只有一个线程访问）。&lt;/li&gt;
&lt;li&gt;ThreadSafeSubClass：线程不安全 ❌（因为 list 被多个线程并发访问，而 ArrayList 不是线程安全的）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可能出现的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;list.add(&quot;1&quot;)&lt;/code&gt; 还没执行完，新的线程就来 &lt;code&gt;remove(0)&lt;/code&gt;，可能抛 &lt;code&gt;IndexOutOfBoundsException&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ArrayList&lt;/code&gt; 不是线程安全的，如果多个线程同时&lt;code&gt; add/remove&lt;/code&gt;，可能会导致数据错乱甚至 &lt;code&gt;ConcurrentModificationException&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
如果在子类中定义的方法和基类中的一个 private 方法签名相同&lt;strong&gt;此时子类的方法不是重写基类方法，而是在子类中定义了一个新的方法。&lt;/strong&gt;
:::&lt;/p&gt;
&lt;h4&gt;常见线程安全类&lt;/h4&gt;
&lt;p&gt;:::info&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String&lt;/li&gt;
&lt;li&gt;Integer、Boolean、Double 等包装类&lt;/li&gt;
&lt;li&gt;StringBuffer&lt;/li&gt;
&lt;li&gt;Random&lt;/li&gt;
&lt;li&gt;Vector&lt;/li&gt;
&lt;li&gt;Hashtable&lt;/li&gt;
&lt;li&gt;java.util.concurrent 包下的类
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;多个线程调用它们同一个实例的某个方法时，是线程安全的。&lt;/strong&gt; 也可以理解为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它们的每个方法都用&lt;code&gt;synchronized&lt;/code&gt;所修饰，都是原子操作，不会被线程的上下文切换所干扰&lt;/li&gt;
&lt;li&gt;但注意它们&lt;strong&gt;多个方法组合在一起就不是原子操作&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;HashTable table = new HashTable();

Thread t1 = new Thread(() -&amp;gt; {
    table.put(&quot;key&quot;, &quot;value1&quot;);  // 每个方法可以保证方法内的临界区代码是原子性的
}, &quot;t1&quot;);
t1.start();

Thread t2 = new Thread(() -&amp;gt; {
    table.put(&quot;key&quot;, &quot;value2&quot;);
}, &quot;t2&quot;);
t2.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;线程安全类方法组合使用&lt;/h5&gt;
&lt;p&gt;分析这段代码是否线程安全:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hashtable table = new Hashtable();
// 线程1，线程2 执行下面方法
if( table.get(&quot;key&quot;) == null) {
    table.put(&quot;key&quot;, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-20_21-10-23.png&quot; alt=&quot;&quot; /&gt;
此时会产生数据覆盖问题&lt;/p&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;p&gt;由此可见，哪怕线程安全类中的每个方法都是线程安全的，都能保证原子性。但是它们组合到一起不是线程安全的，不能保证原子性。要想它们的组合也能保证原子性，需要手动在外部加线程安全的保护，加锁。&lt;/p&gt;
&lt;h5&gt;不可变类的线程安全性&lt;/h5&gt;
&lt;p&gt;String、Integer 等都是不可变类，因为其内部的属性都不可以改变，因此它们的方法都是线程安全的。&lt;/p&gt;
&lt;p&gt;:::tip
但 String 有 replace，substring 等方法可以改变值啊，那么这些方法又是如何保证线程安全的呢？&lt;/p&gt;
&lt;p&gt;答： String 类内部的replace()、substring()都不是在原先的 String 对象上操作，而是每次修改就新建了一个 String 对象。
:::&lt;/p&gt;
&lt;p&gt;String 的 substring 源码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public String substring(int beginIndex) {
    if (beginIndex &amp;lt; 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    } else {
        int subLen = this.length() - beginIndex;
        if (subLen &amp;lt; 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        } else if (beginIndex == 0) {
            return this;
        } else {
            // 核心代码 内部调用了 System.arrayCopy() 来复制字符数组
            return this.isLatin1() ? StringLatin1.newString(this.value, beginIndex, subLen) : StringUTF16.newString(this.value, beginIndex, subLen);
        }
    }
}

// 由此可见，这些方法底层都是新建了一个 String 对象，并把旧对象上的数据复制到新对象上
public static String newString(byte[] val, int index, int len) {
    return new String(Arrays.copyOfRange(val, index, index + len), (byte)0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;案例分析&lt;/h5&gt;
&lt;p&gt;例1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyServlet extends HttpServlet {
    // 是否安全？  HashMap 是线程不安全的
    Map&amp;lt;String,Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
    // 是否安全？  安全
    String S1 = &quot;...&quot;;
    // 是否安全？  安全
    final String S2 = &quot;...&quot;;
    // 是否安全？  不安全，常见线程安全类中没有
    Date D1 = new Date();
    // 是否安全？  不安全，final 只能保证 D2 这个成员变量的引用值不能变。
    //             但是这个日期里面的属性可以发生变化
    final Date D2 = new Date();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 使用上述变量
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyServlet extends HttpServlet {
    // 是否安全？  不安全，UserService 是成员变量，被共享使用
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 记录调用次数
    private int count = 0;  // 共享资源

    public void update() {
        // 临界区
        count++;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例3：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Aspect
@Component
public class MyAspect {
    // 是否安全？ 不安全 
    // Spring 中的 bean 没有特殊说明的话，默认情况下都是单例的
    // 由于 MyAspect 是单例的，是被共享的；那 start 这个成员变量也是被共享的
    private long start = 0L;

    @Before(&quot;execution(* *(..))&quot;)
    public void before() {
        start = System.nanoTime();
    }

    @After(&quot;execution(* *(..))&quot;)
    public void after() {
        long end = System.nanoTime();
        System.out.println(&quot;cost time:&quot; + (end - start));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;可以使用环绕通知来解决这个线程安全问题。把这些属性变成环绕通知中的局部变量&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例 4：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyServlet extends HttpServlet {
    // 是否安全  虽然 UserService 中有一个 UserDao 的成员变量，但是没有其他的地方可以修改它。
    //			 所以这个成员变量 UserDao 是不可变的，所以是安全的
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 是否安全  虽然 UserDao 是成员变量，也会被共享。但内部没有可以更改的属性。所以是安全的
    private UserDao userDao = new UserDaoImpl();

    public void update() {
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    public void update() {
        String sql = &quot;update user set password = ? where username = ?&quot;;
        // 是否安全  因为没有成员变量，Connection 是局部变量。所以是线程安全的
        try (Connection conn = DriverManager.getConnection(&quot;&quot;,&quot;&quot;,&quot;&quot;)){
            // ...
        } catch (Exception e) {
            // ...
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例 5：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyServlet extends HttpServlet {
    // 是否安全  安全。思路同上
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 是否安全  安全。思路同上
    private UserDao userDao = new UserDaoImpl();

    public void update() {
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    // 是否安全  由于 UserDaoImpl 是被多个线程所共享的，所以 Connection 是被共享的成员变量
    // 			 所以是线程不安全的
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = &quot;update user set password = ? where username = ?&quot;;
        conn = DriverManager.getConnection(&quot;&quot;,&quot;&quot;,&quot;&quot;);
        // ...
        conn.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里Connection对象被共享，是说线程a执行到close前，cpu时间片完了。切换线程b，b执行完close后，它时间片也完了。这是切换线程a，它去执行close方法时，会报空指针异常。&lt;/p&gt;
&lt;p&gt;例 6：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyServlet extends HttpServlet {
    // 是否安全  安全。思路同上
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    public void update() {
        UserDao userDao = new UserDaoImpl();
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    // 是否安全  由于 前面的 service 中每次都创建了一个新的 UserDao 对象，所以多个线程操作的
    //     		 不是同一个对象，是线程安全的	
    private Connection = null;
    public void update() throws SQLException {
        String sql = &quot;update user set password = ? where username = ?&quot;;
        conn = DriverManager.getConnection(&quot;&quot;,&quot;&quot;,&quot;&quot;);
        // ...
        conn.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例 7：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class Test {

    public void bar() {
        // 是否安全  由于是抽象类，局部变量 sdf 可能会传递给抽象方法 foo。
        // 			 可能子类会进行不恰当的实现。所以是线程不安全的
        SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        foo(sdf);
    }
    
    public abstract foo(SimpleDateFormat sdf);

    public static void main(String[] args) {
        new Test().bar();
    }
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 foo 的行为是不确定的，可能导致不安全的发生，被称之为&lt;strong&gt;外星方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void foo(SimpleDateFormat sdf) {
    String dateStr = &quot;1999-10-11 00:00:00&quot;;
    for (int i = 0; i &amp;lt; 20; i++) {
        new Thread(() -&amp;gt; {
            try {
                sdf.parse(dateStr);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
实现线程安全有三种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无共享变量&lt;/li&gt;
&lt;li&gt;共享变量不可变&lt;/li&gt;
&lt;li&gt;同步
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;卖票练习&lt;/h5&gt;
&lt;p&gt;测试下面代码是否存在线程安全问题，并尝试改正&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.ExerciseSell&quot;)
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        // TODO 模拟多线程场景下买票操作
        TicketWindow ticket = new TicketWindow(1000);  // 创建一个售票窗口，有 1000 张票

        // 所有线程集合
        List&amp;lt;Thread&amp;gt; threadList = new ArrayList&amp;lt;&amp;gt;();
        // 统计卖出的票数
        List&amp;lt;Integer&amp;gt; amountList = new Vector&amp;lt;&amp;gt;();  // Vector 是线程安全的实现
        for (int i = 0; i &amp;lt; 4000; i++) {
            Thread thread = new Thread(() -&amp;gt; {
                // 买票
                int amount = ticket.sell(randomAmount());
                amountList.add(amount);
            });
            
            // threadList只在主线程中被创建和使用,是非共享数据,没有其他线程修改它。
            // 所以是线程安全的。可以使用 ArrayList 来创建
            threadList.add(thread);
            thread.start();
        }

        // 主线程需要等待所有线程运行结束，再往下执行
        for (Thread thread : threadList) {
            thread.join();
        }

        // 统计卖出的票数和剩余的票数
        log.debug(&quot;余票数量为：{}&quot;, ticket.getCount());
        log.debug(&quot;卖出的票数为：{}&quot;, amountList.stream().mapToInt(Integer::intValue).sum());
    }

    // Random 为线程安全
    static Random random = new Random();

    /**
     * 随机产生 1~5
     *
     * @return 产生的值
     */
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

/**
 * 售票窗口
 */
class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    // 获取余票数量
    public int getCount() {
        return count;
    }

    // 售票
    public int sell(int amount) {
        if (this.count &amp;gt;= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;23:28:42.967 c.ExerciseSell [main] - 余票数量为：0
23:28:42.973 c.ExerciseSell [main] - 卖出的票数为：1005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，此时的代码存在线程安全问题。多卖出去了 5 张票。&lt;/p&gt;
&lt;p&gt;:::info
让我们分析下这段代码中的临界区以及共享变量：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ticket 是共享变量，多个线程都会用到。&lt;/li&gt;
&lt;li&gt;sell() 方法内部有对 amount 共享变量的读写操作，属于临界区。&lt;/li&gt;
&lt;li&gt;amountList 也存在线程安全问题，内部有对数组的操作。但我们不用考虑，因为 Vector 已经加了锁，会对 add 方法做线程安全的保护。
:::
所以，要想解决这段代码的线程安全。就需要对临界区加锁    &lt;code&gt;public synchronized int sell&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;线程安全的卖票代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.ExerciseSell&quot;)
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        // TODO 模拟多线程场景下买票操作
        TicketWindow ticket = new TicketWindow(1000);  // 创建一个售票窗口，有 1000 张票

        // 所有线程集合
        List&amp;lt;Thread&amp;gt; threadList = new ArrayList&amp;lt;&amp;gt;();
        // 统计卖出的票数
        List&amp;lt;Integer&amp;gt; amountList = new Vector&amp;lt;&amp;gt;();  // Vector 是线程安全的集合实现
        for (int i = 0; i &amp;lt; 4000; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 买票
                    // TODO 1. ticket 是共享变量，多个线程都会用到。
                    int amount = ticket.sell(randomAmount());
                    amountList.add(amount);  // TODO 3. amountList 也存在线程安全问题，内部有对数组的操作。但我们不用考虑，因为 Vector 已经加了锁，会对 add 方法做线程安全的保护
                }
            });
            threadList.add(thread);  // threadList只在主线程中被创建和使用,是非共享数据,没有其他线程修改它,所以是线程安全的。可以使用ArrayList来创建
            thread.start();
        }

        // 主线程需要等待所有线程运行结束，再往下执行
        for (Thread thread : threadList) {
            thread.join();
        }

        // 统计卖出的票数和剩余的票数
        log.debug(&quot;余票数量为：{}&quot;, ticket.getCount());
        log.debug(&quot;卖出的票数为：{}&quot;, amountList.stream().mapToInt(Integer::intValue).sum());
    }

    // Random 为线程安全
    static Random random = new Random();

    /**
     * 随机产生 1~5
     *
     * @return 产生的值
     */
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

/**
 * 售票窗口
 */
class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    // 获取余票数量
    public int getCount() {
        return count;
    }

    // 售票
    // 2. sell() 方法内部有对 amount 共享变量的读写操作。属于临界区。使用 synchronized 加锁保护
    public synchronized int sell(int amount) {
        if (this.count &amp;gt;= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;转账练习&lt;/h5&gt;
&lt;p&gt;线程不安全的转账代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.ExerciseTransfer&quot;)
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);

        Thread t1 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, &quot;t1&quot;);

        Thread t2 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, &quot;t2&quot;);

        t1.start();
        t2.start();
        // 等待 t1、t2 线程执行完毕
        t1.join();
        t2.join();

        // 查看转账 2000 次后的总金额
        log.debug(&quot;total:  {}&quot;, (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    /**
     * 随机产生 1~100
     *
     * @return 产生的值
     */
    public static int randomAmount() {
        return random.nextInt(100) +1;
    }
}

class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    // 转账
    public void transfer(Account target, int amount) {
        if (this.money &amp;gt;= amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;22:11:30.593 c.ExerciseTransfer [main] - total:  4291
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，此时的代码存在线程安全问题。总金额变多了。&lt;/p&gt;
&lt;p&gt;:::info
让我们分析下这段代码中的临界区以及共享变量：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;transfer()方法涉及到共享资源的读写，这段方法为临界区。&lt;/li&gt;
&lt;li&gt;共享变量为account，并且由于是两个对象操作transfer()方法。所以共享变量有两个。分别是对象 a 的account和对象 b 的account。&lt;/li&gt;
&lt;li&gt;涉及Account类的多个实例对象。所以不能用对象锁（两个线程锁的是不同对象，不起作用），要用类锁。
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，要想解决这段代码的线程安全。就需要对临界区加锁    &lt;code&gt;synchronized (Account.class) {}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;线程安全的转账代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.ExerciseTransfer&quot;)
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);

        Thread t1 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, &quot;t1&quot;);

        Thread t2 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, &quot;t2&quot;);

        t1.start();
        t2.start();
        // 等待 t1、t2 线程执行完毕
        t1.join();
        t2.join();

        // 查看转账 2000 次后的总金额
        log.debug(&quot;total:  {}&quot;, (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    /**
     * 随机产生 1~100
     *
     * @return 产生的值
     */
    public static int randomAmount() {
        return random.nextInt(100) +1;
    }
}

class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    // 转账
    // TODO 涉及到共享资源的读写。a 对象的 money 和 b 对象的 money 是共享变量。此段代码为临界区。
    public void transfer(Account target, int amount) {
        // 需要把锁加在共享类上。不能到 this 对象上
        synchronized (Account.class) {
            if (this.money &amp;gt;= amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Monitor&lt;/h3&gt;
&lt;h4&gt;Java对象头&lt;/h4&gt;
&lt;p&gt;通常， 我们创建的对象都由两部分组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对象头&lt;/li&gt;
&lt;li&gt;对象中的成员变量&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以 32 位虚拟机为例&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;普通对象：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;数组对象：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;|---------------------------------------------------------------------------------|
| 	        Object Header (96 bits) 											  |
|--------------------------------|-----------------------|------------------------|
| 	  	  Mark Word (32bits)     |   Klass Word (32bits) |  array length (32bits) |
|--------------------------------|-----------------------|------------------------|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;其中 Mark Word 的结构为：&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-20_22-34-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;64 位虚拟机 Mark Word&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-20_22-35-27.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考资料：https://stackoverflow.com/questions/26357186/what-is-in-java-object-header&lt;/p&gt;
&lt;h4&gt;原理 - Monitor 锁&lt;/h4&gt;
&lt;p&gt;Monitor被翻译为&lt;strong&gt;监视器&lt;/strong&gt;或&lt;strong&gt;管程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每个 Java 对象都可以关联一个 Monitor 对象，如果使用 synchronized 给对象上锁（重量级）之后，该对象头的 Mark Word 中就被设置指向 Monitor 重量级锁对象的地址&lt;/p&gt;
&lt;p&gt;Monitor 结构如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Owner：所有者，Monitor 中只能有一个所有者&lt;/li&gt;
&lt;li&gt;EntryList：等待队列（阻塞队列），进入此队列的线程会进入 BLOCKED 阻塞状态&lt;/li&gt;
&lt;li&gt;WaitSet：之前获取过锁，但执行条件不满足，进入 WAITING 状态的线程&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-20_22-49-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-20_22-55-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;刚开始 Monitor 中 Owner（所有者）为空&lt;/li&gt;
&lt;li&gt;当 Thread-2 执行 synchronized(obj) 时，就会把 Java 对象 obj 和操作系统对象 Monitor 相关联。（靠 obj 对象头中的 Mark Word 记录 Monitor 对象的指针地址）  因为目前只有 Thread-2 一个线程，所以 Monitor 的 Owner 属性会关联上 Thread-2&lt;/li&gt;
&lt;li&gt;如果 Thread-3，Thread-4，Thread-5 也来执行 synchronized(obj)，由于 obj 已经关联了一个 Monitor 锁，这些线程就会检查 Monitor 锁是否有主人。因为此时锁的 Owner 属性已经关联上了 Thread-2，所以这些线程就会进入 EntryList 等待队列。这些线程也会进入 BLOCKED 阻塞状态&lt;/li&gt;
&lt;li&gt;Thread-2 执行完同步代码块的内容，Owner 就会空出来，然后唤醒 EntryList 中等待的线程来竞争锁，竞争时是非公平的（不一定是先进 EntryList 的线程先成为 Owner，JDK 底层实现决定的）&lt;/li&gt;
&lt;li&gt;图中 WaitSet 中的 Thread-0，Thread-1 是之前获得过锁，但条件不满足进入 WAITING 状态的线程，后面讲 wait-notify 时会分析&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;synchronized 必须是进入同一个对象的 Monitor 才有上述的效果&lt;/li&gt;
&lt;li&gt;不加 synchronized 的对象不会关联 Monitor，不遵从以上规则
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;原理 - synchronized&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;static final Object lock = new Object();
static int counter = 0;

public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反编译为字节码后，对应的字节码为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(java.lang.String[]);
	descriptor: ([Ljava/lang/String;)V
	flags: ACC_PUBLIC, ACC_STATIC
	Code:
	 	stack=2, locals=3, args_size=1
	 	0: getstatic     #2            // &amp;lt;- lock引用 （synchronized开始）      
		3: dup
 		4: astore_1                    // lock引用 -&amp;gt; slot 1      
		5: monitorenter                // 将 lock对象 MarkWord 置为 Monitor 指针   
		6: getstatic     #3			   // &amp;lt;- i
		9: iconst_1                    // 准备常数 1    
		10: iadd                       // +1     
		11: putstatic    #3            // -&amp;gt; i  
		14: aload_1                    // &amp;lt;- lock引用     
		15: monitorexit                // 将 lock对象 MarkWord 重置, 唤醒 EntryList       
		16: goto         24
		19: astore_2                   // e -&amp;gt; slot 2       
		20: aload_1                    // &amp;lt;- lock引用       
		21: monitorexit                // 将 lock对象 MarkWord 重置, 唤醒 EntryList       
		22: aload_2                    // &amp;lt;- slot 2 (e)       
		23: athrow                     // throw e       
		24: return
 Exception table:
	 from  	  to  target type
	 	6     16	19    any  
	   19     22	19	  any
 LineNumberTable:
	 line 8: 0
	 line 9: 6
	 line 10: 14
	 line 11: 24
 LocalVariableTable:
 	Start  Length  Slot  Name   Signature
    	0      25      0  args	 [Ljava/lang/String;
 StackMapTable: number_of_entries = 2
 	frame_type = 255 /* full_frame */
 		offset_delta = 19
 		locals = [ class &quot;[Ljava/lang/String;&quot;, class java/lang/Object ]
 		stack = [ class java/lang/Throwable ]
	frame_type = 250 /* chop */
 		offset_delta = 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
方法级别的 synchronized 不会在字节码指令中有所体现
:::&lt;/p&gt;
&lt;h4&gt;原理 - synchronized 进阶&lt;/h4&gt;
&lt;h5&gt;轻量级锁&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;轻量级锁相比较重量级锁，性能有了一定提升。因为不再需要 Monitor 锁，只是用线程栈中的锁记录对象来充当轻量级锁。但轻量级锁还是有一定缺点，可以使用偏向锁进行进一步优化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果一个对象有多线程要进行加锁，但加锁的时间是错开的（也就是没有竞争），那么可以使用轻量级锁来优化。&lt;/p&gt;
&lt;p&gt;轻量级锁对使用者是透明的，即语法仍然是synchronized&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JDK6 之后，使用 synchronized进行加锁时，会优先加轻量级锁。如果有竞争，轻量级锁会升级成重量级锁&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设有两个方法同步块，利用同一个对象加锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final Object obj = new Object();

public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;图示：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建锁记录（Lock Record）对象，&lt;strong&gt;每个线程的栈帧都会包含一个锁记录的结构&lt;/strong&gt;，内部可以存储锁定对象的Mark Word&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::info
锁记录对象对我们来说也和 Monitor 一样是不可见的。不是 Java 层面的，是操作系统层面的。&lt;/p&gt;
&lt;p&gt;锁记录对象由两部分组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对象指针（Object reference）：将来锁住的对象的内存地址&lt;/li&gt;
&lt;li&gt;锁记录地址和状态信息：用来记录将来锁住的对象的 Mark Work，方便将来解锁时恢复待解锁对象的对象头数据。会和锁住的对象的 Mark Work 通过 CAS 进行交换
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-21_21-46-45.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;让锁记录中 &lt;code&gt;Object reference&lt;/code&gt; 指向锁对象，并尝试用 &lt;code&gt;CAS&lt;/code&gt; 替换 &lt;code&gt;Object&lt;/code&gt; 的 &lt;code&gt;Mark Word&lt;/code&gt;，将 &lt;code&gt;Mark Word&lt;/code&gt; 的值存入锁记录
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-21_21-51-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 CAS 替换成功，对象头中存储了锁记录地址和状态 00，表示由该线程给对象加锁，这时图示如下
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-21_21-49-32.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 CAS 失败，有两种情况&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是其它线程已经持有了该 Object 的轻量级锁，这时表明有竞争，进入锁膨胀过程&lt;/li&gt;
&lt;li&gt;如果是自己执行了 &lt;code&gt;synchronized&lt;/code&gt; 锁重入，那么再添加一条 &lt;code&gt;Lock Record&lt;/code&gt; 作为重入的计数（图中有两个锁记录对象，计数为 2）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-21_21-53-03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当退出 &lt;code&gt;synchronized&lt;/code&gt; 代码块（解锁时）如果有取值为 &lt;code&gt;null&lt;/code&gt; 的锁记录，表示有重入，这时重置锁记录，表示重入计数减一&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-21_21-54-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当退出 &lt;code&gt;synchronized&lt;/code&gt; 代码块（解锁时）锁记录的值不为 &lt;code&gt;NULL&lt;/code&gt;，这时使用 &lt;code&gt;CAS&lt;/code&gt; 将 &lt;code&gt;Mark Word&lt;/code&gt; 的值恢复给对象头
&lt;ul&gt;
&lt;li&gt;成功，则解锁成功&lt;/li&gt;
&lt;li&gt;失败，说明轻量级锁进行了锁膨胀或已经升级为重量级锁，进入重量级锁解锁流程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;锁膨胀&lt;/h5&gt;
&lt;p&gt;如果在尝试加轻量级锁的过程中，CAS 操作无法成功，这时一种情况就是有其它线程为此对象加上了轻量级锁（产生了竞争），这时需要进行锁膨胀，将轻量级锁升级为重量级锁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;图示：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当 Thread-1 准备对 obj 对象进行轻量级加锁时，此时 Thread-0 已经对该对象加了轻量级锁
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-21_22-10-16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这时 Thread-1 加轻量级锁会失败，进入锁膨胀流程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;即为 &lt;code&gt;obj&lt;/code&gt; 对象申请 &lt;code&gt;Monitor&lt;/code&gt; 锁，并让 &lt;code&gt;obj&lt;/code&gt; 指向重量级锁地址&lt;/li&gt;
&lt;li&gt;然后自己进入 &lt;code&gt;Monitor&lt;/code&gt; 的 &lt;code&gt;EntryList&lt;/code&gt; 等待队列，进入 &lt;code&gt;BLOCKED&lt;/code&gt; 阻塞状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-21_22-11-08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Thread-0 退出同步块解锁时，使用 &lt;code&gt;CAS&lt;/code&gt; 将 &lt;code&gt;Mark Word&lt;/code&gt; 的值恢复给对象头，此时由于轻量级锁进行了锁膨胀，会解锁失败。这时会进入重量级解锁流程，即按照 &lt;code&gt;Monitor&lt;/code&gt; 地址找到 &lt;code&gt;Monitor 对象&lt;/code&gt;，设置 &lt;code&gt;Owner&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;，唤醒 &lt;code&gt;EntryList&lt;/code&gt; 等待队列中处于 &lt;code&gt;BLOCKED&lt;/code&gt; 状态的线程&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;自旋优化&lt;/h5&gt;
&lt;p&gt;:::info
自旋：在发生重量级锁竞争的过程中，当前线程先不要进入阻塞，而是进行几次循环。可以避免线程的上下文切换&lt;/p&gt;
&lt;p&gt;进入阻塞再恢复,会发生上下文切换,比较耗费性能
:::&lt;/p&gt;
&lt;p&gt;重量级锁竞争的时候，还可以使用自旋（循环尝试获取重量级锁）来进行优化，如果当前线程自旋成功（即此时持锁线程已经退出了同步块，释放了锁），这时当前线程就可以避免阻塞，直接成为 Monitor 重量级锁中新的 Owner。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自旋重试成功的情况&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;线程 1（core 1 上）&lt;/th&gt;
&lt;th&gt;对象 Mark&lt;/th&gt;
&lt;th&gt;线程 2（core 2 上）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;10（重量锁）&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问同步块，获取 monitor&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;成功（加锁）&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;访问同步块，获取 monitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;自旋重试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行完毕&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;自旋重试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;成功（解锁）&lt;/td&gt;
&lt;td&gt;01（无锁）&lt;/td&gt;
&lt;td&gt;自旋重试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;成功（加锁）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;自旋重试失败的情况&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;线程 1（core 1 上）&lt;/th&gt;
&lt;th&gt;对象 Mark&lt;/th&gt;
&lt;th&gt;线程 2（core 2 上）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;10（重量锁）&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问同步块，获取 monitor&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;成功（加锁）&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;访问同步块，获取 monitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;自旋重试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;自旋重试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;自旋重试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行同步块&lt;/td&gt;
&lt;td&gt;10（重量锁）重量锁指针&lt;/td&gt;
&lt;td&gt;阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自旋会占用 CPU 时间，单核 CPU 自旋就是浪费，多核 CPU 自旋才能发挥优势。&lt;/li&gt;
&lt;li&gt;在 Java 6 之后自旋锁是自适应的，比如对象刚刚的一次自旋操作成功过，那么认为这次自旋成功的可能性会高，就多自旋几次；反之，就少自旋甚至不自旋，总之，比较智能。&lt;/li&gt;
&lt;li&gt;Java 7 之后不能手动控制是否开启自旋功能
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;偏向锁&lt;/h5&gt;
&lt;p&gt;:::warning
从 JDK18 开始，偏向锁已经被彻底废弃！
:::&lt;/p&gt;
&lt;p&gt;轻量级锁在没有竞争时（就自己这个线程），每次重入仍然需要执行 CAS 操作。 会浪费 CPU 的性能。
Java 6 中引入了偏向锁来做进一步优化：只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头，如果后续再发生锁冲入，之后发现这个线程 ID 是自己的就表示没有竞争，不用重新 CAS。以后只要不发生竞争，这个对象就归该线程所有&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里的线程 ID 是操作系统赋予的 ID&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final Object obj = new Object();

public static void m1() {
    synchronized( obj ) {
        // 同步块 A
        m2();
    }
}

public static void m2() {
    synchronized( obj ) {
        // 同步块 B
        m3();
    }
}

public static void m3() {
    synchronized( obj ) {
        // 同步块 C
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-21_22-55-57.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-21_22-58-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h6&gt;偏向锁-状态&lt;/h6&gt;
&lt;p&gt;回忆一下对象头格式&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mark Word (64 bits)&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unused:25&lt;/code&gt; | &lt;code&gt;hashcode:31&lt;/code&gt; | &lt;code&gt;unused:1&lt;/code&gt; | &lt;code&gt;age:4&lt;/code&gt; | &lt;code&gt;biased_lock:0&lt;/code&gt; | &lt;code&gt;01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Normal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;thread:54&lt;/code&gt; | &lt;code&gt;epoch:2&lt;/code&gt; | &lt;code&gt;unused:1&lt;/code&gt; | &lt;code&gt;age:4&lt;/code&gt; | &lt;code&gt;biased_lock:1&lt;/code&gt; | &lt;code&gt;01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Biased&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ptr_to_lock_record:62&lt;/code&gt; | &lt;code&gt;00&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lightweight Locked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ptr_to_heavyweight_monitor:62&lt;/code&gt; | &lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Heavyweight Locked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;11&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Marked for GC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个对象创建时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果开启了偏向锁（默认开启），那么对象创建后，MarkWord值为0x05，即最后3位为101，这时它的thread、epoch、age都为0&lt;/li&gt;
&lt;li&gt;偏向锁是默认是延迟的，不会在程序启动时立即生效，如果想避免延迟，可以加VM参数 &lt;code&gt;-XX:BiasedLockingStartupDelay=0&lt;/code&gt; 来 禁用延迟&lt;/li&gt;
&lt;li&gt;如果没有开启偏向锁，那么对象创建后，MarkWord 值为0x01即最后3位为001，这时它的hashcode、age都为0，第一次用到hashcode时才会赋值&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;测试延迟特性&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Sl4j
public class TestBiased {
    public static void main(String[] args) {
        Dog dog = new Dog();
        // toPrintableSimple 扩展了 jol 让它的输出更简洁
        log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true));  

        Thread.sleep(4000);
        log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true)); 
    }
}

class Dog {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;c.TestBiased [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
c.TestBiased [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;此时，第一次打印我们发现 dog 对象的 MarkWord 最后三位是 001 ，不是我们预期的 101 。这是因为：
&lt;strong&gt;偏向锁默认是有延迟的，不会在程序启动时立即生效&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二次打印因为我们让程序启动后休眠了 4s ，偏向锁在此期间已经生效了，所以会发现此时 dog 对象的 MarkWord 最后三位已经是 101&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;测试偏向锁&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Sl4j
public class TestBiased {
    public static void main(String[] args) {
        // 通过 VM Options 属性设置禁用延迟
        Dog dog = new Dog();
        log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true));

        synchronized(dog) {
            log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true)); 
        }

        log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true)); 
    }
}

class Dog {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;c.TestBiased [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
c.TestBiased [main] - 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 
c.TestBiased [main] - 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一次打印只是表示该对象启用了偏向锁。（因为前 54 位线程 ID 全是 0）&lt;/li&gt;
&lt;li&gt;第二次打印因为使用了 &lt;code&gt;synchronized&lt;/code&gt; 来加锁，所以当前线程执行到此时，会优先给 dog 对象加偏向锁（不会考虑加轻量级锁或者重量级锁）。加锁后打印出来的 MarkWord 前 54 位是关联的操作系统的线程 ID&lt;/li&gt;
&lt;li&gt;第三次打印，在加锁完后，打印出来的 MarkWord 没有变化，这是因为“偏向”。dog 对象一开始被主线程给加了锁，以后这个 dog 对象就从属于这个线程。所以 dog 的 MarkWord 头里始终存储的是主线程 ID。除非有其他线程又用了此 dog 对象才会发生改变（处于偏向锁的对象解锁后，线程 ID 仍存储于对象头也就是偏向此线程）&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;测试禁用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过在代码运行时添加 VM 参数：&lt;code&gt;-XX:-UseBiasedLocking&lt;/code&gt;来禁用偏向锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c.TestBiased [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
c.TestBiased [main] - 00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 
c.TestBiased [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一次打印表示没有开启偏向锁&lt;/li&gt;
&lt;li&gt;第二次打印表示在加锁过程中，加了轻量级锁（00 代表轻量级锁）&lt;/li&gt;
&lt;li&gt;第三次打印表示加锁完成后，又变成了初始状态，此时 MarkWord 里面的线程 ID 也会重置&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;测试 hashcode&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Sl4j
public class TestBiased {
    public static void main(String[] args) {
        // 通过 VM Options 属性设置禁用延迟
        Dog dog = new Dog();
        dog.hashCode();  // TODO 会禁用该对象的偏向锁
        log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true));

        synchronized(dog) {
            log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true)); 
        }

        log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true)); 
    }
}

class Dog {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;c.TestBiased [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
c.TestBiased [main] - 00000000 00000000 00000000 00000000 00100000 00010100 11110101 10011000 
c.TestBiased [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;正常状态对象初始化后是没有 hashcode 的，第一次调用才生成&lt;/li&gt;
&lt;li&gt;调用了 hashCode() 后会撤销该对象的偏向锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么调用 hashCode() 后就会禁用偏向锁？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为如果对象是处于偏向锁状态，MarkWord 内部存储完 54 位的操作系统线程 ID，没有足够的位置来存储 hashcode 码（31 位）&lt;/p&gt;
&lt;p&gt;当一个可偏向的对象，调用了 hashCode() 方法后，就会撤销当前对象的偏向状态，变成正常状态的对象（MarkWord 后三位变为001）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么使用轻量级锁或重量级锁可以正常使用 hashCode()？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;轻量级锁将对象的 &lt;code&gt;hashcode&lt;/code&gt; 存放在线程栈桢中的锁记录对象中&lt;/li&gt;
&lt;li&gt;重量级锁将对象的 &lt;code&gt;hashcode&lt;/code&gt; 存放在 &lt;code&gt;Monitor&lt;/code&gt; 对象中，解锁的时候会还原回来&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;偏向锁-撤销&lt;/h6&gt;
&lt;p&gt;&lt;strong&gt;其它线程使用对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;两个线程访问同一个对象时是错开的（不能存在线程交错情况），会将偏向锁升级为轻量级锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Sl4j
public class TestBiased {
    public static void main(String[] args) {
        Dog dog = new Dog();

        // 必须让两个线程交错开。必须是 t1 线程将锁解开后，t2 线程再去加锁
        // 否则如果有线程交错的情况，就会升级成重量级锁 
        new Thread(() -&amp;gt; {
            log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true));
            synchronized(dog) {  // TODO 此时，加的是偏向锁
                log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true)); 
            }
            log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true));

            synchronized(TestBiased.class) {
                TestBiased.class.notify();
            }
        }, &quot;t1&quot;).start();

        new Thread(() -&amp;gt; {

            // 等待 t1 线程中类对象解锁，这样就可以保证 dog 对象的两个锁是交错开的
            synchronized(TestBiased.class) {
                TestBiased.class.wait();
            }
            
            log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true));
            synchronized(dog) {  // TODO 此时，偏向锁升级为轻量级锁
                log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true)); 
            }
            log.debug(ClassLayout.parseInstance(dog).toPrintableSimple(true)); 
        }, &quot;t2&quot;).start();
    }
}

class Dog {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;c.TestBiased [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
c.TestBiased [t1] - 00000000 00000000 00000000 00000000 00011111 10110110 11101000 00000101 
c.TestBiased [t1] - 00000000 00000000 00000000 00000000 00011111 10110110 11101000 00000101
c.TestBiased [t2] - 00000000 00000000 00000000 00000000 00011111 10110110 11101000 00000101 
c.TestBiased [t2] - 00000000 00000000 00000000 00000000 00100000 01001011 11110011 00100000 
c.TestBiased [t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;t1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次打印，dog 对象的 MarkWord 前几位全是 0，最后三位为 101：偏向锁。表示 t1 线程中的 dog 对象启用了偏向锁，但还没加锁&lt;/li&gt;
&lt;li&gt;第二次打印，t1 线程给 dog 对象加上了偏向锁。此时 dog 对象的 MarkWord 前 54 位关联的就是 t1 线程的 ID (64 位虚拟机下，前 54 位是线程 ID，32 位虚拟机下，前 23 位是线程 ID)&lt;/li&gt;
&lt;li&gt;第三次打印，解锁后，由于偏向锁特性，t1 线程 ID 仍然会保留在 dog 对象的 MarkWord 里&lt;/li&gt;
&lt;li&gt;执行完后，t1 线程唤醒 t2 线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;t2&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次打印，dog 对象没加锁之前，还是上次的状态，MarkWord 头的前 54 位还关联着 t1 的线程 ID&lt;/li&gt;
&lt;li&gt;第二次打印，本来 dog 对象是偏向于 t1 线程的，但由于 t2 线程此时也需要给 dog 对象加锁，就会导致偏向锁失效，偏向锁会升级为轻量级锁，此时 MarkWord 头最后 2 位变为 000：轻量级锁。前 62 位是锁记录指针&lt;/li&gt;
&lt;li&gt;第三次打印，解锁后，dog 对象上的偏向状态变成了 0，变成不可偏向状态。且锁记录指针被清空&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;批量重偏向&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果对象虽然被多个线程访问，但没有竞争，这时偏向了线程 T1 的对象仍有机会重新偏向 T2，重偏向会重置对象的 Thread ID&lt;/p&gt;
&lt;p&gt;当撤销偏向锁阈值超过 20 次后，jvm 会这样觉得，我是不是偏向错了呢，于是会在给这些对象加锁时重新偏向至加锁线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void test3() throws InterruptedException {
    
    List&amp;lt;Dog&amp;gt; list = new Vector&amp;lt;&amp;gt;();
    
    Thread t1 = new Thread(() -&amp;gt; {
        for (int i = 0; i &amp;lt; 30; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
        }
        synchronized (list) {
            list.notify();
        }
    }, &quot;t1&quot;);
    t1.start();

    Thread t2 = new Thread(() -&amp;gt; {
        synchronized (list) {
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }  
        log.debug(&quot;===============&amp;gt; &quot;);
        for (int i = 0; i &amp;lt; 30; i++) {
            Dog d = list.get(i);
            log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }, &quot;t2&quot;);
    t2.start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意观察 t2 - 19 处的变化，此时批量重偏向成 t2 线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - ===============&amp;gt; 
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 7 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 8 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 9 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 10 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 11 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 12 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 13 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 14 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 15 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 16 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;批量撤销&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当撤销偏向锁阈值超过 40 次后，jvm 会这样觉得，自己确实偏向错了，根本就不该偏向。于是整个类的所有对象都会变为不可偏向的，新建的该类型对象也是不可偏向的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static Thread t1,t2,t3;

public static void main(String[] args) {
    test4();
}

/**
*	核心代码
*/
private static void test4() throws InterruptedException {
    List&amp;lt;Dog&amp;gt; list = new Vector&amp;lt;&amp;gt;();
    
    int loopNumber = 39;
    t1 = new Thread(() -&amp;gt; {
        for (int i = 0; i &amp;lt; loopNumber; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {  // TODO 偏向锁
                log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
        }
        LockSupport.unpark(t2);
    }, &quot;t1&quot;);
    t1.start();
    
    t2 = new Thread(() -&amp;gt; {
        LockSupport.park();
        log.debug(&quot;===============&amp;gt; &quot;);
        for (int i = 0; i &amp;lt; loopNumber; i++) {
            Dog d = list.get(i);
            log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {  // TODO 前 19 次(0~18)撤销偏向锁变为轻量级锁。从第20次开始会重偏向为偏向锁
                log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
        LockSupport.unpark(t3);
    }, &quot;t2&quot;);
    t2.start();
    
    t3 = new Thread(() -&amp;gt; {
        LockSupport.park();
        log.debug(&quot;===============&amp;gt; &quot;);
        for (int i = 0; i &amp;lt; loopNumber; i++) {
            Dog d = list.get(i);
            log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {  // TODO 此时，t2线程已经撤销偏向锁20次(0~18),t3线程从19~38次执行撤销偏向锁。最后第40次Dog类的所有对象都变成不可偏向的
                log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(i + &quot;\t&quot; + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }, &quot;t3&quot;);
    t3.start();
    
    t3.join();
    // 新建的对象由于批量撤销达到阈值40次变成不可偏向的状态
    log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参考博客：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/LemonFive/p/11246086.html&quot;&gt;Java对象头的组成&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/LemonFive/p/11248248.html&quot;&gt;偏向锁批量重偏向与批量撤销&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/farmerjohngit/myblog/issues/12&quot;&gt;死磕Synchronized底层实现&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;锁消除&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;锁消除是指虚拟机即时编译器在运行时，对一些代码上要求同步的锁进行消除。锁消除的主要原因是因为Java虚拟机的&lt;strong&gt;即时编译器&lt;/strong&gt;在运行时，会根据程序的运行情况，去除一些不必要的锁，以提高程序的运行效率。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
    static int x = 0;
    
    @Benchmark
    public void a() throws Exception {
        x++;
    }
    
    @Benchmark
    public void b() throws Exception {
        // 这里的 obj 是局部变量,不会被共享,JIT 做热点代码优化时会做锁消除
        Object obj = new Object();
        synchronized (obj) {  
            x++;
        }
    }
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行命令：&lt;code&gt;java -jar benchmarks.jar&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Benchmark            Mode  Samples  Score  Score error  Units 
c.i.MyBenchmark.a    avgt        5  1.542        0.056  ns/op 
c.i.MyBenchmark.b    avgt        5  1.518        0.091  ns/op 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现 a 方法和 b 方法的执行耗时几乎是一样的，甚至 b 方法加锁后的执行方法比 a 方法没加锁还要快
按理说：加锁是有一定的性能损耗的，就算是做了轻量级锁、偏向锁的优化，也还是会存在性能损耗
这是因为 Java 程序运行时，有一个 &lt;code&gt;JIT（即时编译器）&lt;/code&gt;，它会对 Java 字节码进行进一步优化。因为局部变量 obj 没有逃离 b 方法的作用范围，所以 JIT 在做热点代码优化时会做锁消除&lt;/p&gt;
&lt;p&gt;锁消除优化开关默认打开，通过下方命令运行 jar 包可以关闭锁消除优化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -XX:-EliminateLocks -jar benchmarks.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Benchmark            Mode  Samples   Score  Score error  Units 
c.i.MyBenchmark.a    avgt        5   1.507        0.108  ns/op 
c.i.MyBenchmark.b    avgt        5  16.976        1.572  ns/op 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们发现 b 方法比 a 方法性能要差十几倍&lt;/p&gt;
&lt;h3&gt;wait notify&lt;/h3&gt;
&lt;h4&gt;原理 —— wait / notify&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-23_00-01-32.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Owner 线程发现条件不满足，会调用 wait 方法，即可进入 WaitSet 变为 WAITING 状态&lt;/li&gt;
&lt;li&gt;BLOCKED 和 WAITING 的线程都处于阻塞状态，不占用 CPU 时间片&lt;/li&gt;
&lt;li&gt;BLOCKED 线程会在 Owner 线程释放锁时唤醒&lt;/li&gt;
&lt;li&gt;WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒，但唤醒后并不意味着立刻获得锁，仍需进入 EntryList 重新竞争&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;相关API&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;obj.wait() ：让进入 object 监视器的线程到 waitSet 中等待，此时线程状态变为 WAITING 状态&lt;/li&gt;
&lt;li&gt;obj.wai(long n) ：让进入 object 监视器的线程到 waitSet 中有时限的等待，到 n 毫秒后结束等待，或是被 notify 唤醒&lt;/li&gt;
&lt;li&gt;obj.notify() ：在 object 上正在 waitSet 等待的线程中挑一个唤醒&lt;/li&gt;
&lt;li&gt;obj.notifyAll() ：把 object 上正在 waitSet 等待的线程全部唤醒&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们都是线程之间进行协作的手段，都属于 Object 对象的方法。必须要先获得此对象的锁，才能调用这几个方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test18&quot;)
public class Test18 {

    static final Object lock = new Object();

    public static void main(String[] args) {

        // 这段代码会报错，因为都还没有获得 lock 对象的锁
        /*
        try {
            lock.wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        */

        synchronized (lock) {  // 先获得对象的锁
            try {
                lock.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.TestWaitNotify&quot;)
public class TestWaitNotify {
    final static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -&amp;gt; {
            synchronized (lock) {
                log.debug(&quot;执行...&quot;);
                try {
                    lock.wait();  // 让线程在 lock 上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug(&quot;执行其他代码...&quot;);
            }
        }, &quot;t1&quot;).start();

        new Thread(() -&amp;gt; {
            synchronized (lock) {
                log.debug(&quot;执行...&quot;);
                try {
                    lock.wait();  // 让线程在 lock 上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug(&quot;执行其他代码...&quot;);

            }
        }, &quot;t2&quot;).start();

        // 主线程 2s 后执行
        TimeUnit.SECONDS.sleep(2);
        log.debug(&quot;唤醒 obj 上其他的线程&quot;);
        synchronized (lock) {
            lock.notify();  // 随机唤醒 obj 上一个线程
            // lock.notifyAll();  // 唤醒 obj 上所有等待的线程
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主线程执行 notify() 的一种结果情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;15:36:01.915 c.TestWaitNotify [t1] - 执行...
15:36:01.923 c.TestWaitNotify [t2] - 执行...
15:36:03.915 c.TestWaitNotify [main] - 唤醒 obj 上其他的线程
15:36:03.915 c.TestWaitNotify [t1] - 执行其他代码...   (随机唤醒了 t1 线程)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主线程执行 notifyAll() 的结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;19:58:15.457 [Thread-0] c.TestWaitNotify - 执行.... 
19:58:15.460 [Thread-1] c.TestWaitNotify - 执行.... 
19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码.... 
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wait()&lt;/code&gt;方法会释放对象的锁，进入 WaitSet 等待区，从而让其他线程就机会获取对象的锁。无限制等待，直到&lt;code&gt;notify()&lt;/code&gt; 为止&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wait(long n) &lt;/code&gt;有时限的等待, 到 n 毫秒后结束等待，或是被 notify&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用&lt;code&gt;wait()&lt;/code&gt;后进入&lt;code&gt;WAITING&lt;/code&gt;状态&lt;/li&gt;
&lt;li&gt;调用&lt;code&gt;wait(timeout)&lt;/code&gt;后，进入&lt;code&gt;TIMED_WAITING&lt;/code&gt;状态&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;wait/notify 正确使用&lt;/h4&gt;
&lt;h5&gt;sleep与wait&lt;/h5&gt;
&lt;p&gt;sleep(long n) 和 wait(long n) 的区别&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;sleep 是 Thread 方法，而 wait 是 Object 的方法&lt;/li&gt;
&lt;li&gt;sleep 不需要强制和 synchronized 配合使用，但 wait 需要 和 synchronized 一起用(synchronized之后对象才有monitor)&lt;/li&gt;
&lt;li&gt;sleep 在睡眠的同时，不会释放对象锁的，但 wait 在等待的时候会释放对象锁&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test10 {
    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -&amp;gt; {
            synchronized (lock) {
                try {
//                    Thread.sleep(20000);      // 不会释放锁
                    lock.wait(20000);   // 会释放锁
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, &quot;t1&quot;).start();

        Thread.sleep(1000);
        synchronized (lock) {
            log.info(&quot;主线程获得锁了了&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;思考下面的解决方案好不好，为什么？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;线程：
- 小南：想干活，但需要烟。
- 其它人：不管烟，直接干活。
- 送烟的：3秒后把 hasCigarette 改成 true。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;new Thread(() -&amp;gt; {
    synchronized (room) {
        log.debug(&quot;有烟没？[{}]&quot;, hasCigarette);
        if (!hasCigarette) {
            log.debug(&quot;没烟，先歇会！&quot;);
            sleep(2);
        }
        log.debug(&quot;有烟没？[{}]&quot;, hasCigarette);
        if (hasCigarette) {
            log.debug(&quot;可以开始干活了&quot;);
        }
    }
}, &quot;小南&quot;).start();
for (int i = 0; i &amp;lt; 5; i++) {
    new Thread(() -&amp;gt; {
        synchronized (room) {
            log.debug(&quot;可以开始干活了&quot;);
        }
    }, &quot;其它人&quot;).start();
}
sleep(1);
new Thread(() -&amp;gt; {
    // 这里能不能加 synchronized (room)？
    hasCigarette = true;
    log.debug(&quot;烟到了噢！&quot;);
}, &quot;送烟的&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;20:49:49.883 [小南] c.TestCorrectPosture - 有烟没？[false] 
20:49:49.887 [小南] c.TestCorrectPosture - 没烟，先歇会！
20:49:50.882 [送烟的] c.TestCorrectPosture - 烟到了噢！
20:49:51.887 [小南] c.TestCorrectPosture - 有烟没？[true] 
20:49:51.887 [小南] c.TestCorrectPosture - 可以开始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;其它干活的线程，都要一直阻塞，效率太低&lt;/li&gt;
&lt;li&gt;小南线程必须睡足 2s 后才能醒来，就算烟提前送到，也无法立刻醒来&lt;/li&gt;
&lt;li&gt;不能加，因为加了 synchronized (room) 后，就好比小南在里面反锁了门睡觉，烟根本没法送进门，main 没加 synchronized 就好像 main 线程是翻窗户进来的&lt;/li&gt;
&lt;li&gt;解决方法，使用 wait - notify 机制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;step2&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;思考下面的实现行吗，为什么？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Thread(() -&amp;gt; {
    synchronized (room) {
        log.info(&quot;有烟没？[{}]&quot;, hasCigarette);
        if (!hasCigarette) {
            log.info(&quot;没烟，先歇会！&quot;);
            try {
                room.wait(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.info(&quot;有烟没？[{}]&quot;, hasCigarette);
        if (hasCigarette) {
            log.info(&quot;可以开始干活了&quot;);
        }
    }
}, &quot;小南&quot;).start();
for (int i = 0; i &amp;lt; 5; i++) {
    new Thread(() -&amp;gt; {
        synchronized (room) {
            log.info(&quot;可以开始干活了&quot;);
        }
    }, &quot;其它人&quot;).start();
}
sleep(1);
new Thread(() -&amp;gt; {
    synchronized (room) {
        hasCigarette = true;
        log.info(&quot;烟到了噢！&quot;);
        room.notify();
    }
}, &quot;送烟的&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改进点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- wait() → 释放锁 + 进入等待队列(waitSet)。
- notify() → 随机唤醒一个等待中的线程，但不会立刻执行，还要竞争锁。
- 小南有两种唤醒方式：超时（2s） 或 送烟的 notify()。
- 其它人不会被小南卡住，因为小南 wait 的时候已经释放锁了。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;解决了其它干活的线程阻塞的问题&lt;/li&gt;
&lt;li&gt;但如果有其它线程也在等待条件呢？如果有多个线程，那唤醒的如果不是期望唤醒的线程呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step3&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;错误唤醒也叫虚假唤醒&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 小南：等烟
- 小女：等外卖
- 送外卖的：只送外卖，调用一次 notify()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;共享状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static boolean hasCigarette = false;
static boolean hasTakeout = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;new Thread(() -&amp;gt; {
    synchronized (room) {
        log.debug(&quot;有烟没？[{}]&quot;, hasCigarette);
        if (!hasCigarette) {
            log.debug(&quot;没烟，先歇会！&quot;);
            try {
                room.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug(&quot;有烟没？[{}]&quot;, hasCigarette);
        if (hasCigarette) {
            log.debug(&quot;可以开始干活了&quot;);
        } else {
            log.debug(&quot;没干成活...&quot;);
        }
    }
}, &quot;小南&quot;).start();
new Thread(() -&amp;gt; {
    synchronized (room) {
        Thread thread = Thread.currentThread();
        log.debug(&quot;外卖送到没？[{}]&quot;, hasTakeout);
        if (!hasTakeout) {
            log.debug(&quot;没外卖，先歇会！&quot;);
            try {
                room.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug(&quot;外卖送到没？[{}]&quot;, hasTakeout);
        if (hasTakeout) {
            log.debug(&quot;可以开始干活了&quot;);
        } else {
            log.debug(&quot;没干成活...&quot;);
        }
    }
}, &quot;小女&quot;).start();
sleep(1);
new Thread(() -&amp;gt; {
    synchronized (room) {
        hasTakeout = true;
        log.debug(&quot;外卖到了噢！&quot;);
        room.notify();
    }
}, &quot;送外卖的&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;notify() &lt;/code&gt;只能唤醒一个等待线程，而唤醒的是谁不可控。&lt;/li&gt;
&lt;li&gt;notify 不能指定唤醒谁，结果可能「唤错人」，导致逻辑不符合预期。
输出&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;20:53:12.173 [小南] c.TestCorrectPosture - 有烟没？[false] 
20:53:12.176 [小南] c.TestCorrectPosture - 没烟，先歇会！
20:53:12.176 [小女] c.TestCorrectPosture - 外卖送到没？[false] 
20:53:12.176 [小女] c.TestCorrectPosture - 没外卖，先歇会！
20:53:13.174 [送外卖的] c.TestCorrectPosture - 外卖到了噢！
20:53:13.174 [小南] c.TestCorrectPosture - 有烟没？[false] 
20:53:13.174 [小南] c.TestCorrectPosture - 没干成活... 
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;notify 只能随机唤醒一个 WaitSet 中的线程，这时如果有其它线程也在等待，那么就可能唤醒不了正确的线 程，称之为【虚假唤醒】&lt;/li&gt;
&lt;li&gt;解决方法，改为 notifyAll&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step4&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Thread(() -&amp;gt; {
    synchronized (room) {
        hasTakeout = true;
        log.debug(&quot;外卖到了噢！&quot;);
        room.notifyAll();
    }
}, &quot;送外卖的&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;20:55:23.978 [小南] c.TestCorrectPosture - 有烟没？[false] 
20:55:23.982 [小南] c.TestCorrectPosture - 没烟，先歇会！
20:55:23.982 [小女] c.TestCorrectPosture - 外卖送到没？[false] 
20:55:23.982 [小女] c.TestCorrectPosture - 没外卖，先歇会！
20:55:24.979 [送外卖的] c.TestCorrectPosture - 外卖到了噢！
20:55:24.979 [小女] c.TestCorrectPosture - 外卖送到没？[true] 
20:55:24.980 [小女] c.TestCorrectPosture - 可以开始干活了
20:55:24.980 [小南] c.TestCorrectPosture - 有烟没？[false] 
20:55:24.980 [小南] c.TestCorrectPosture - 没干成活... 
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;notifyAll()&lt;/code&gt; 仅解决某个线程的唤醒问题，但使用 if + wait 判断仅有一次机会，一旦条件不成立，就没有重新判断的机会了&lt;/li&gt;
&lt;li&gt;解决方法，用 while + wait，当条件不成立，再次 wait&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step5&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将 if 改为 while&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!hasCigarette) {
    log.debug(&quot;没烟，先歇会！&quot;);
    try {
        room.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改动后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (!hasCigarette) {
    log.debug(&quot;没烟，先歇会！&quot;);
    try {
        room.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

// 其余不变
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;20:58:34.322 [小南] c.TestCorrectPosture - 有烟没？[false] 
20:58:34.326 [小南] c.TestCorrectPosture - 没烟，先歇会！
20:58:34.326 [小女] c.TestCorrectPosture - 外卖送到没？[false] 
20:58:34.326 [小女] c.TestCorrectPosture - 没外卖，先歇会！
20:58:35.323 [送外卖的] c.TestCorrectPosture - 外卖到了噢！
20:58:35.324 [小女] c.TestCorrectPosture - 外卖送到没？[true] 
20:58:35.324 [小女] c.TestCorrectPosture - 可以开始干活了
20:58:35.324 [小南] c.TestCorrectPosture - 没烟，先歇会！
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;if + wait 被唤醒后直接往下走， 判断仅有一次机会，一旦条件不成立，就没有重新判断的机会了&lt;/li&gt;
&lt;li&gt;while + wait 被唤醒后重新判断条件，条件不满足就继续等待，直到满足为止。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;wait notify使用公式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;synchronized(lock) {
    while(条件不成立) {
        lock.wait();
    }
    // 干活
}
//另一个线程
synchronized(lock) {
    lock.notifyAll();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;设计模式-保护性暂停&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;同步模式之保护性暂停&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;即 &lt;code&gt;Guarded Suspension&lt;/code&gt;，用在一个线程等待另一个线程的执行结果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有一个结果需要从一个线程传递到另一个线程，让他们关联同一个 GuardedObject&lt;/li&gt;
&lt;li&gt;如果有结果不断从一个线程到另一个线程那么可以使用消息队列（见生产者/消费者）&lt;/li&gt;
&lt;li&gt;JDK 中，join 的实现、Future 的实现，采用的就是此模式&lt;/li&gt;
&lt;li&gt;因为要等待另一方的结果，因此归类到同步模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-25_23-13-16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;保护性暂停实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test11 {
    public static void main(String[] args) {
        // 线程1等待线程2的下载结果
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -&amp;gt; {
            // 等待结果
            log.info(&quot;等待结果&quot;);
            Object o = guardedObject.get();
            log.info(&quot;结果：{}&quot;, JSON.toJSONString(o));
        }, &quot;t1&quot;).start();

        new Thread(() -&amp;gt; {
            log.info(&quot;执行下载。。&quot;);
            try {
                guardedObject.complete(Downloder.download());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }, &quot;t2&quot;).start();
    }
}

class GuardedObject {
    /**
     * 结果
     */
    private Object response;

    /**
     * 获取结果
     *
     * @return
     */
    public Object get() {
        synchronized (this) {
            while (response == null) {
                // 没有结果
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            return response;
        }
    }

    /**
     * 产生结果
     *
     * @param response
     */
    public void complete(Object response) {
        synchronized (this) {
            // 结果给成员变量
            this.response = response;
            this.notifyAll();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Downloder {
    public static List&amp;lt;String&amp;gt; download() throws IOException {
        HttpURLConnection conn = (HttpURLConnection) new URL(&quot;https://www.baidu.com/&quot;).openConnection();
        List&amp;lt;String&amp;gt; lines = new ArrayList&amp;lt;&amp;gt;();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                lines.add(line);
            }
        }
        return lines;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;23:33:57.971 [t1] INFO com.thread.concurrent1.Test11 -- 等待结果
23:33:57.971 [t2] INFO com.thread.concurrent1.Test11 -- 执行下载。。
23:33:59.085 [t1] INFO com.thread.concurrent1.Test11 -- 结果：[&quot;&amp;lt;!DOCTYPE html&amp;gt;&quot;,&quot;&amp;lt;!--STATUS OK--&amp;gt;&amp;lt;html&amp;gt; &amp;lt;head&amp;gt;&amp;lt;meta http-equiv=content-type content=text/html;charset=utf-8&amp;gt;&amp;lt;meta http-equiv=X-UA-Compatible content=IE=Edge&amp;gt;&amp;lt;meta content=always name=referrer&amp;gt;&amp;lt;link rel=stylesheet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果用join的话，他必须等待线程结束，而用保护性暂停模式，线程2，执行完下载后，可以继续干其他事情&lt;/p&gt;
&lt;p&gt;join的话，等待结果的变量必须设置为全局的，不能像现在这样都写为局部的，比如：&lt;code&gt;Object o = guardedObject.get();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;保护性暂停-扩展-增加超时&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;想象一个场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程 t1 需要一个计算结果（比如从网上下载的文件内容）。&lt;/li&gt;
&lt;li&gt;线程 t2 负责去执行这个耗时的计算（下载文件）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;t1 不能一直空转浪费 CPU 等待 t2，它需要一种高效的机制：如果结果没准备好，t1 就应该“休息”（阻塞）；当 t2 准备好结果后，它需要有办法“叫醒”正在休息的 t1。&lt;/p&gt;
&lt;p&gt;GuardedObject 就是这个高效的协调机制，它像一个“带锁的信箱”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;t1 (消费者)：去看信箱 (get)。如果信是空的 (response == null)，它就锁上信箱，坐在旁边睡觉 (wait)。&lt;/li&gt;
&lt;li&gt;t2 (生产者)：拿到信后 (download())，打开信箱 (synchronized)，把信放进去 (complete)，然后大喊一声“信来了！” (notifyAll)，叫醒正在睡觉的 t1。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test11 {
    public static void main(String[] args) {
        // 线程1等待线程2的下载结果
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -&amp;gt; {
            // 等待结果
            log.info(&quot;等待结果&quot;);
            Object o = guardedObject.get(3000);
            log.info(&quot;结果：{}&quot;, JSON.toJSONString(o));
        }, &quot;t1&quot;).start();

        new Thread(() -&amp;gt; {
            log.info(&quot;执行下载。。&quot;);
            try {
                guardedObject.complete(Downloder.download());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }, &quot;t2&quot;).start();
    }
}

class GuardedObject {
    /**
     * 结果
     */
    private Object response;

    /**
     * 获取结果
     *
     * @return
     */
    public Object get(long timeout) {
        synchronized (this) {
            // 开始时间
            long begin = System.currentTimeMillis();
            // 经历时间
            long passedTime = 0;
            while (response == null) {
                // 这一轮循环应该等待时间
                long waitTime = timeout - passedTime;
                // 经历的时间超过了最大等待时间，退出循环
                if (waitTime &amp;lt;= 0) {
                    break;
                }
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 求得经历时间
                passedTime = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    /**
     * 产生结果
     *
     * @param response
     */
    public void complete(Object response) {
        synchronized (this) {
            // 结果给成员变量
            this.response = response;
            this.notifyAll();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;while (response == null):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;至关重要：为什么用 while 而不是 if？这是为了防止 “虚假唤醒”（Spurious Wakeup）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚假唤醒&lt;/strong&gt;：线程有时可能在没有被 notify() 的情况下从 wait() 状态中醒来。如果用 if，线程醒来后就不会再次检查 response == null 这个条件，可能会直接往下执行，拿到一个 null 的结果，这是错误的。&lt;/li&gt;
&lt;li&gt;while 循环确保了线程每次被唤醒后，都会重新检查条件 (response == null)。只有当条件确实不满足时（即 response 已经有值了），才会跳出循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;超时判断 (waitTime &amp;lt;= 0)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;passedTime 记录了已经等待了多久。&lt;/li&gt;
&lt;li&gt;waitTime 是本轮循环还需要等待多久。&lt;/li&gt;
&lt;li&gt;如果 waitTime 小于等于 0，说明总的等待时间已经超过了 timeout，就没必要再等了，直接 break 循环。此时 response 仍然是 null，方法最终会返回 null，表示超时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;总结：整个程序的执行流程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;main 线程启动 t1 和 t2。&lt;/li&gt;
&lt;li&gt;t1 先运行，进入 get 方法，获取 guardedObject 的锁。&lt;/li&gt;
&lt;li&gt;t1 发现 response 是 null，进入 while 循环。&lt;/li&gt;
&lt;li&gt;t1 计算出 waitTime (约3000ms)，然后调用 guardedObject.wait(3000)。它释放了锁，并进入休眠状态。&lt;/li&gt;
&lt;li&gt;t2 开始运行，执行 Downloder.download()。&lt;/li&gt;
&lt;li&gt;download() 执行完毕后，t2 进入 complete 方法，它成功获取了 guardedObject 的锁（因为 t1 已经释放了）。&lt;/li&gt;
&lt;li&gt;t2 将下载结果赋给 response，然后调用 guardedObject.notifyAll()。&lt;/li&gt;
&lt;li&gt;notifyAll() 发出信号，唤醒正在 guardedObject 上等待的 t1。&lt;/li&gt;
&lt;li&gt;t2 执行完 synchronized 代码块，释放锁。&lt;/li&gt;
&lt;li&gt;t1 被唤醒后，重新获取 guardedObject 的锁。&lt;/li&gt;
&lt;li&gt;t1 从 wait() 方法返回，继续 while 循环。它再次检查 response == null，发现条件不成立（因为 response 已经有值了），跳出循环。&lt;/li&gt;
&lt;li&gt;t1 执行 return response，释放锁，并打印出最终得到的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
&lt;strong&gt;虚假唤醒&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;虚假唤醒指的是：线程在调用 wait() 后，没有人调用 notify / notifyAll，也没有超时/中断，但是线程却“自己醒了”。&lt;/p&gt;
&lt;p&gt;正确做法：必须用 while 检查条件，而不是 if。这样即使虚假唤醒了，线程也会再检查一次条件，发现条件没满足会继续 wait。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;举个生活例子 🚗&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你在车站等公交（wait()）。&lt;/li&gt;
&lt;li&gt;司机来喊你上车（notify()）。&lt;/li&gt;
&lt;li&gt;突然你自己睡醒了，以为公交到了，结果一看——啥也没有（虚假唤醒）。&lt;/li&gt;
&lt;li&gt;所以你不能光凭“醒了”就走，而要 再看一下公交是不是来了（条件检查）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV16J411h7Rd?t=24.1&amp;amp;p=101&quot;&gt;04.052&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;join()原理 暂时略。。。&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;(异步)模式之生产者/消费者&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与前面的保护性暂停中的 GuardObject 不同，不需要产生结果和消费结果的线程一一对应&lt;/li&gt;
&lt;li&gt;消费队列可以用来平衡生产和消费的线程资源&lt;/li&gt;
&lt;li&gt;生产者仅负责产生结果数据，不关心数据该如何处理，而消费者专心处理结果数据&lt;/li&gt;
&lt;li&gt;消息队列是有容量限制的，满时不会再加入数据，空时不会再消耗数据&lt;/li&gt;
&lt;li&gt;JDK 中各种阻塞队列，采用的就是这种模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-27_21-49-42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现与测试&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test12 {
    public static void main(String[] args) {
        MessageQueue messageQueue = new MessageQueue(2);

        for (int i = 0; i &amp;lt; 3; i++) {
            int id = i;
            new Thread(() -&amp;gt; {
                messageQueue.put(new Message(id, &quot;值:&quot; + id));
            }, &quot;生产者&quot; + i).start();
        }
        new Thread(() -&amp;gt; {
            try {
                while (true) {
                    Thread.sleep(1000);
                    Message message = messageQueue.tack();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, &quot;消费者&quot;).start();

    }
}

/**
 * 消息队列类
 * 与RabbitMQ不同，该测试是Java中线程间通信
 * RabbitMQ是进程间通信
 */
@Slf4j(topic = &quot;MessageQueue&quot;)
class MessageQueue {
    // 消息的队列集合
    private LinkedList&amp;lt;Message&amp;gt; list = new LinkedList&amp;lt;&amp;gt;();
    // 队列容量
    private Integer capcity;

    public MessageQueue(Integer capcity) {
        this.capcity = capcity;
    }

    // 获取消息
    public Message tack() {
        synchronized (list) {
            while (list.isEmpty()) {
                try {
                    log.info(&quot;对列为空，消费者线程等待&quot;);
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 从队列头部获取消息返回
            Message message = list.removeFirst();
            log.info(&quot;已消费消息：{}&quot;, JSON.toJSONString(message));
            list.notifyAll();
            return message;
        }
    }

    // 存入消息
    public void put(Message message) {
        synchronized (list) {
            // 检查队列是否已满
            while (list.size() == capcity) {
                try {
                    log.info(&quot;队列满了，生产者线程等待&quot;);
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 添加从尾部加
            list.addLast(message);
            log.info(&quot;已生产消息：{}&quot;, JSON.toJSONString(message));
            list.notifyAll();
        }
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
final class Message {
    private Integer id;
    private Object value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;22:29:57.899 [生产者2] INFO MessageQueue -- 已生产消息：{&quot;id&quot;:2,&quot;value&quot;:&quot;值:2&quot;}
22:29:57.903 [生产者1] INFO MessageQueue -- 已生产消息：{&quot;id&quot;:1,&quot;value&quot;:&quot;值:1&quot;}
22:29:57.903 [生产者0] INFO MessageQueue -- 队列满了，生产者线程等待
22:29:58.663 [消费者] INFO MessageQueue -- 已消费消息：{&quot;id&quot;:2,&quot;value&quot;:&quot;值:2&quot;}
22:29:58.664 [生产者0] INFO MessageQueue -- 已生产消息：{&quot;id&quot;:0,&quot;value&quot;:&quot;值:0&quot;}
22:29:59.676 [消费者] INFO MessageQueue -- 已消费消息：{&quot;id&quot;:1,&quot;value&quot;:&quot;值:1&quot;}
22:30:00.685 [消费者] INFO MessageQueue -- 已消费消息：{&quot;id&quot;:0,&quot;value&quot;:&quot;值:0&quot;}
22:30:01.699 [消费者] INFO MessageQueue -- 对列为空，消费者线程等待

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tack()（获取消息）和 put()（存入消息）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;capcity = 2&lt;/code&gt;，有 3 个生产者 P0,P1,P2 和 1 个消费者 C：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;P0、P1 很快把两个消息放入队列（队列满）。&lt;/li&gt;
&lt;li&gt;P2 调用 put 时发现 list.size()==capcity，进入 wait() 阻塞。&lt;/li&gt;
&lt;li&gt;Consumer 线程每隔 1s 调 tack()：如果队列非空，消费一个（removeFirst），然后 notifyAll()。&lt;/li&gt;
&lt;li&gt;notifyAll() 将唤醒 P2，使其能够获得锁并把消息放入队列。如此循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Park &amp;amp; Unpark&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;基本使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它们是 LockSupport 类中的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 暂停当前线程
LockSupport.park(); 
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;先 park 再 unpark&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread t1 = new Thread(() -&amp;gt; {
    log.debug(&quot;start...&quot;);
    sleep(1);
    log.debug(&quot;park...&quot;);
    LockSupport.park(); // 此时状态是wait
    log.debug(&quot;resume...&quot;);
},&quot;t1&quot;);
t1.start();

sleep(2);
log.debug(&quot;unpark...&quot;);
LockSupport.unpark(t1);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;18:42:52.585 c.TestParkUnpark [t1] - start... 
18:42:53.589 c.TestParkUnpark [t1] - park... 
18:42:54.583 c.TestParkUnpark [main] - unpark... 
18:42:54.583 c.TestParkUnpark [t1] - resume...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;code&gt;unpark()&lt;/code&gt;既可以在&lt;code&gt;park()&lt;/code&gt;之前调用，也可以在&lt;code&gt;part()&lt;/code&gt;之后调用，都是用来恢复某个线程的运行。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先 unpark 再 park&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread t1 = new Thread(() -&amp;gt; {
    log.debug(&quot;start...&quot;);
    sleep(2);
    log.debug(&quot;park...&quot;);
    LockSupport.park();
    log.debug(&quot;resume...&quot;);
}, &quot;t1&quot;);
t1.start();

sleep(1);
log.debug(&quot;unpark...&quot;);
LockSupport.unpark(t1);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;18:43:50.765 c.TestParkUnpark [t1] - start... 
18:43:51.764 c.TestParkUnpark [main] - unpark... 
18:43:52.769 c.TestParkUnpark [t1] - park... 
18:43:52.769 c.TestParkUnpark [t1] - resume...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;与 Object 的 wait &amp;amp; notify 相比&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;wait, notify 和 notifyAll 必须配合 Object Monitor 一起使用, 而 park, unpark 不必&lt;/li&gt;
&lt;li&gt;park &amp;amp; unpark 是以线程为单位来【阻塞】和【唤醒(指定)】线程, 而 notify 只能随机唤醒一个等待线程, notifyAll 是唤醒所有等待线程, 就不那么【精确】&lt;/li&gt;
&lt;li&gt;park &amp;amp; unpark 可以先 unpark, 而 wait &amp;amp; notify 不能先 notify&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;原理之 park &amp;amp; unpark&lt;/h5&gt;
&lt;p&gt;每个线程都有自己的一个(C代码实现的) Parker 对象&lt;/p&gt;
&lt;p&gt;由三部分组成 &lt;code&gt;_counter&lt;/code&gt; ， &lt;code&gt;_cond(condition条件变量)&lt;/code&gt;  和 &lt;code&gt;_mutex&lt;/code&gt; (互斥锁)&lt;/p&gt;
&lt;p&gt;打个比喻&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程就像一个旅人,Parker就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0为耗尽1为充足)&lt;/li&gt;
&lt;li&gt;调用 park 就是要看需不需要停下来歇息
&lt;ul&gt;
&lt;li&gt;如果备用干粮耗尽(_counter为0),那么钻进帐篷歇息(等待补充干粮,否则容易半路饿死)&lt;/li&gt;
&lt;li&gt;如果备用干粮充足(_counter为1),那么不需停留,继续前进(兜里有粮,心里不慌)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;调用 unpark,就好比令干粮充足(使_counter为1)
&lt;ul&gt;
&lt;li&gt;如果这时线程还在帐篷,就唤醒让他继续前进&lt;/li&gt;
&lt;li&gt;如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留,继续前进
&lt;ul&gt;
&lt;li&gt;因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮,也就是&lt;strong&gt;多次unpark后只会让紧跟着的一次park失效&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;_counter（许可）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;_counter是一个整型变量，用来记录所谓的“许可”。在Parker对象中，默认初始化为0。&lt;/li&gt;
&lt;li&gt;当调用LockSupport.park()时，如果_counter为0，则表示没有许可，线程将被阻塞。&lt;/li&gt;
&lt;li&gt;当调用LockSupport.unpark()时，_counter被设置为1，表示发放了一个许可，如果此时有线程因缺少许可而被阻塞，它将被唤醒并继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;_cond（条件变量）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;_cond是POSIX线程库中条件变量的数组，用于线程的等待和唤醒。&lt;/li&gt;
&lt;li&gt;当_counter为0时，线程会通过_cond进入等待状态。如果有其他线程调用unpark()，_cond上等待的线程将被唤醒。&lt;/li&gt;
&lt;li&gt;在Parker对象中，可能包含多个条件变量，用于处理不同类型的等待（如相对时间和绝对时间的等待）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;POSIX 线程库（POSIX Threads，简称 pthreads）&lt;/p&gt;
&lt;p&gt;pthread 就是 POSIX 定义的线程编程标准接口。&lt;/p&gt;
&lt;p&gt;POSIX：Portable Operating System Interface，可移植操作系统接口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;_mutex(mutual exclusion)（互斥锁）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;_mutex是POSIX线程库中的互斥锁，用于保护对_counter和_cond的访问，确保线程安全。&lt;/li&gt;
&lt;li&gt;在park()操作中，线程首先尝试获取_mutex，如果成功，则检查_counter。如果_counter为0，则线程将在_cond上等待，并释放_mutex。&lt;/li&gt;
&lt;li&gt;在unpark()操作中，线程首先获取_mutex，然后设置_counter为1，并唤醒在_cond上等待的线程。之后释放_mutex。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;先调用park 再调用unpark&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-27_23-08-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当前线程调用 Unsafe.park() 方法&lt;/li&gt;
&lt;li&gt;检查 _counter ，本情况为 0，这时，获得 _mutex 互斥锁&lt;/li&gt;
&lt;li&gt;线程进入 _cond 条件变量阻塞&lt;/li&gt;
&lt;li&gt;设置 _counter = 0&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-27_23-36-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 Unsafe.unpark(Thread_0) 方法，设置 _counter 为 1&lt;/li&gt;
&lt;li&gt;唤醒 _cond 条件变量中的 Thread_0&lt;/li&gt;
&lt;li&gt;Thread_0 恢复运行&lt;/li&gt;
&lt;li&gt;设置 _counter 为 0&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;先调用unpark 再调用park&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-27_23-49-15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 Unsafe.unpark(Thread_0) 方法，设置 _counter 为 1&lt;/li&gt;
&lt;li&gt;当前线程调用 Unsafe.park() 方法&lt;/li&gt;
&lt;li&gt;检查 _counter ，本情况为 1，这时线程无需阻塞，继续运行&lt;/li&gt;
&lt;li&gt;设置 _counter 为 0&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;线程状态转换&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page75_image.png&quot; alt=&quot;&quot; /&gt;
阻塞状态是说，如果调用了操作系统的一些跟阻塞IO相关的API，他就会陷入阻塞，但在Java的层面看不出来，Java总是显示Runnable状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NEW --&amp;gt; RUNNABLE&lt;/p&gt;
&lt;p&gt;当调用 t.start() 方法时，由 NEW --&amp;gt; RUNNABLE&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况2&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; WAITING&lt;/p&gt;
&lt;p&gt;t 线程用 synchronized(obj) 获取了对象锁后&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 obj.wait() 方法时，t 线程从 RUNNABLE --&amp;gt; WAITING&lt;/li&gt;
&lt;li&gt;调用 obj.notify()， obj.notifyAll()， t.interrupt() 时
&lt;ul&gt;
&lt;li&gt;竞争锁成功，t 线程从 WAITING --&amp;gt; RUNNABLE&lt;/li&gt;
&lt;li&gt;竞争锁失败，t 线程从 WAITING --&amp;gt; BLOCKED&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::info
idea调试的时候RUNNABLE状态idea显示的是RUNNING，实际上是RUNNABLE状态。&lt;/p&gt;
&lt;p&gt;BLOCKED状态idea调试时显示的是Monitor
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TestWaitNotify {
    final static Object obj = new Object();
    
    public static void main(String[] args) {
        
        new Thread(() -&amp;gt; {
            synchronized (obj) {
                log.debug(&quot;执行....&quot;);
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug(&quot;其它代码....&quot;); // 断点
            }
        },&quot;t1&quot;).start();
        
        new Thread(() -&amp;gt; {
            synchronized (obj) {
                log.debug(&quot;执行....&quot;);
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug(&quot;其它代码....&quot;); // 断点
            }
        },&quot;t2&quot;).start();
        
        sleep(0.5);
        log.debug(&quot;唤醒 obj 上其它线程&quot;);
        synchronized (obj) {
            obj.notifyAll(); // 唤醒obj上所有等待线程 断点
        }
        
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;情况3&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; WAITING&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;当前线程&lt;/strong&gt;调用 &lt;code&gt;t.join()&lt;/code&gt; 方法时，&lt;strong&gt;当前线程&lt;/strong&gt;(调用join方法的线程)从 RUNNABLE --&amp;gt; WAITING
&lt;ul&gt;
&lt;li&gt;注意是当前线程在 t 线程对象的监视器上等待&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;t 线程&lt;/strong&gt;运行结束，或调用了&lt;strong&gt;当前线程&lt;/strong&gt;的 interrupt() 时，&lt;strong&gt;当前线程&lt;/strong&gt;从 WAITING --&amp;gt; RUNNABLE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;情况4&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; WAITING&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --&amp;gt; WAITING&lt;/li&gt;
&lt;li&gt;调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ，会让目标线程从 WAITING --&amp;gt;RUNNABLE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
interrupt 会强制唤醒线程，并设置中断标记。&lt;/p&gt;
&lt;p&gt;wait/sleep/join 遇到中断会抛 InterruptedException(isInterrupted() = false)；park 不会抛，但会记录中断状态(isInterrupted() = true)。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况5&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; TIMED_WAITING&lt;/p&gt;
&lt;p&gt;t线程用 synchronized(obj) 获取了对象锁后&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 obj.wait(long n) 方法时，t线程从 RUNNABLE --&amp;gt; TIMED_WAITING&lt;/li&gt;
&lt;li&gt;t线程等待时间超过了n毫秒，或调用 obj.notify() ， obj.notifyAll() ， t.interrupt() 时
&lt;ul&gt;
&lt;li&gt;竞争锁成功，t线程从TIMED_WAITING --&amp;gt; RUNNABLE&lt;/li&gt;
&lt;li&gt;竞争锁失败，t线程从TIMED_WAITING --&amp;gt; BLOCKED&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;情况6&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; TIMED_WAITING&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前线程调用 t.join(long n) 方法时，当前线程从 RUNNABLE --&amp;gt; TIMED_WAITING
&lt;ul&gt;
&lt;li&gt;注意是当前线程在 t 线程对象的监视器上等待&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当前线程等待时间超过了 n 毫秒，或 t 线程运行结束，或调用了当前线程的 interrupt() 时，当前线程从 TIMED_WAITING --&amp;gt; RUNNABLE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;情况7&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; TIMED_WAITING&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前线程调用 Thread.sleep(long n) ，当前线程从 RUNNABLE --&amp;gt; TIMED_WAITING&lt;/li&gt;
&lt;li&gt;当前线程等待时间超过了 n 毫秒，当前线程从TIMED_WAITING --&amp;gt; RUNNABLE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;情况8&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; TIMED_WAITING&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时，当前线 程从 RUNNABLE --&amp;gt; TIMED_WAITING&lt;/li&gt;
&lt;li&gt;调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ，或是等待超时，会让目标线程从 TIMED_WAITING--&amp;gt; RUNNABLE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;情况9&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; BLOCKED&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;t 线程&lt;/strong&gt;用synchronized(obj) 获取对象锁时如果竞争失败，从RUNNABLE --&amp;gt; BLOCKED&lt;/li&gt;
&lt;li&gt;持 obj 锁线程的同步代码块执行完毕，会唤醒该对象上所有 BLOCKED的线程重新竞争，如果其中 &lt;strong&gt;t 线程&lt;/strong&gt;竞争 成功，从 BLOCKED --&amp;gt; RUNNABLE ，其它失败的线程仍然BLOCKED&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;情况10&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RUNNABLE --&amp;gt; TERMINATED&lt;/p&gt;
&lt;p&gt;当前线程所有代码运行完毕，进入 TERMINATED&lt;/p&gt;
&lt;h3&gt;多把锁 活跃性&lt;/h3&gt;
&lt;h4&gt;多把锁&lt;/h4&gt;
&lt;p&gt;多把不相干的锁&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一间大屋子有两个功能：睡觉、学习，互不相干。&lt;/p&gt;
&lt;p&gt;现在小南要学习，小女要睡觉，但如果只用一间屋子（一个对象锁）的话，那么并发度很低&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class BigRoom {
    
    public void sleep() {
        synchronized (this) {
            log.debug(&quot;sleeping 2 小时&quot;);
            Sleeper.sleep(2);
        }
    }
    
    public void study() {
        synchronized (this) {
            log.debug(&quot;study 1 小时&quot;);
            Sleeper.sleep(1);
        }
    }
    
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BigRoom bigRoom = new BigRoom();

new Thread(() -&amp;gt; {
    bigRoom.study();
},&quot;小南&quot;).start();

new Thread(() -&amp;gt; {
    bigRoom.sleep();
},&quot;小女&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;12:13:54.471 [小南] c.BigRoom - study 1 小时
12:13:55.476 [小女] c.BigRoom - sleeping 2 小时
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现并发度很低&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解决方法是准备多个房间（多个对象锁）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class BigRoom {
    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();
    
    public void sleep() {
        synchronized (bedRoom) {
            log.debug(&quot;sleeping 2 小时&quot;);
            Sleeper.sleep(2);
        }
    }
    
    public void study() {
        synchronized (studyRoom) {
            log.debug(&quot;study 1 小时&quot;);
            Sleeper.sleep(1);
        }
    }
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;12:15:35.069 [小南] c.BigRoom - study 1 小时
12:15:35.069 [小女] c.BigRoom - sleeping 2 小时
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将锁的粒度细分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;好处，是可以增强并发度&lt;/li&gt;
&lt;li&gt;坏处，如果一个线程需要同时获得多把锁，就容易发生死锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;活跃性&lt;/h4&gt;
&lt;p&gt;活跃性就是指，线程内的代码本来是有限的，但是因为某种原因，线程代码一直执行不完，这就叫做线程活跃性。&lt;/p&gt;
&lt;p&gt;活跃性分别有三种现象：死锁、活锁、饥饿&lt;/p&gt;
&lt;h5&gt;死锁&lt;/h5&gt;
&lt;p&gt;一个线程需要同时获取多把锁，这时就容易发生死锁&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;t1 线程&lt;/code&gt; 获得 &lt;code&gt;A对象&lt;/code&gt; 锁，接下来想获取 &lt;code&gt;B对像&lt;/code&gt; 的锁&lt;/p&gt;
&lt;p&gt;&lt;code&gt;t2 线程&lt;/code&gt; 获得 &lt;code&gt;B对象&lt;/code&gt; 锁，接下来想获取 &lt;code&gt;A对象&lt;/code&gt; 的锁&lt;/p&gt;
&lt;p&gt;例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test13 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();

        Thread t1 = new Thread(() -&amp;gt; {
            synchronized (A) {
                log.info(&quot;lock A&quot;);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    log.info(&quot;lock B&quot;);
                    log.info(&quot;操作...&quot;);
                }
            }
        }, &quot;t1&quot;);

        Thread t2 = new Thread(() -&amp;gt; {
            synchronized (B) {
                log.info(&quot;lock B&quot;);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A) {
                    log.info(&quot;lock A&quot;);
                    log.info(&quot;操作...&quot;);
                }
            }
        }, &quot;t2&quot;);

        t1.start();
        t2.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;21:39:09.858 [t2] INFO com.thread.concurrent1.Test13 -- lock B
21:39:09.858 [t1] INFO com.thread.concurrent1.Test13 -- lock A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序并没有运行结束，是造成了死锁&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定位死锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;检测死锁可以使用 jconsole工具，或者使用 jps 定位进程 id，再用 jstack 定位死锁：&lt;/p&gt;
&lt;p&gt;程序运行后(造成死锁)，在idea控制台中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS D:\workspace\idea\thread-pool&amp;gt; jps
528 Launcher
30420 Jps
25992
43144 Test13
20716 RemoteMavenServer36
PS D:\workspace\idea\thread-pool&amp;gt; 

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到该进行id，使用jstack命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS D:\workspace\idea\thread-pool&amp;gt; jstack 43144
2025-08-28 21:43:19
Full thread dump Java HotSpot(TM) 64-Bit Server VM (21.0.5+9-LTS-239 mixed mode, sharing):

Threads class SMR info:
_java_thread_list=0x0000017cf0f6a940, length=14, elements={
0x0000017cf08b42a0, 0x0000017cf08b4cf0, 0x0000017cf08b5c00, 0x0000017cf08b70f0,
0x0000017cf08b9f50, 0x0000017cf08beae0, 0x0000017ceb6b8290, 0x0000017cf08cbf90,
0x0000017cf09992c0, 0x0000017cf0c83a30, 0x0000017cf0b2c110, 0x0000017cf106c5d0,
0x0000017cf106cc30, 0x0000017cca2f3c10
}

&quot;Reference Handler&quot; #9 [7632] daemon prio=10 os_prio=2 cpu=0.00ms elapsed=119.14s tid=0x0000017cf08b42a0 nid=7632 waiting on condition  [0x0000001fae5ff000]
   java.lang.Thread.State: RUNNABLE
        at java.lang.ref.Reference.waitForReferencePendingList(java.base@21.0.5/Native Method)
        at java.lang.ref.Reference.processPendingReferences(java.base@21.0.5/Reference.java:246)
        at java.lang.ref.Reference$ReferenceHandler.run(java.base@21.0.5/Reference.java:208)

&quot;Finalizer&quot; #10 [6188] daemon prio=8 os_prio=1 cpu=0.00ms elapsed=119.14s tid=0x0000017cf08b4cf0 nid=6188 in Object.wait()  [0x0000001fae6fe000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait0(java.base@21.0.5/Native Method)
        - waiting on &amp;lt;0x0000000718e0c2e8&amp;gt; (a java.lang.ref.NativeReferenceQueue$Lock)
        at java.lang.Object.wait(java.base@21.0.5/Object.java:366)
        at java.lang.Object.wait(java.base@21.0.5/Object.java:339)
        at java.lang.ref.NativeReferenceQueue.await(java.base@21.0.5/NativeReferenceQueue.java:48)
        at java.lang.ref.ReferenceQueue.remove0(java.base@21.0.5/ReferenceQueue.java:158)
        at java.lang.ref.NativeReferenceQueue.remove(java.base@21.0.5/NativeReferenceQueue.java:89)
        - locked &amp;lt;0x0000000718e0c2e8&amp;gt; (a java.lang.ref.NativeReferenceQueue$Lock)
        at java.lang.ref.Finalizer$FinalizerThread.run(java.base@21.0.5/Finalizer.java:173)

&quot;Signal Dispatcher&quot; #11 [38120] daemon prio=9 os_prio=2 cpu=0.00ms elapsed=119.14s tid=0x0000017cf08b5c00 nid=38120 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

&quot;Attach Listener&quot; #12 [42088] daemon prio=5 os_prio=2 cpu=15.62ms elapsed=119.14s tid=0x0000017cf08b70f0 nid=42088 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

&quot;Service Thread&quot; #13 [43176] daemon prio=9 os_prio=0 cpu=0.00ms elapsed=119.14s tid=0x0000017cf08b9f50 nid=43176 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

&quot;Monitor Deflation Thread&quot; #14 [40404] daemon prio=9 os_prio=0 cpu=0.00ms elapsed=119.14s tid=0x0000017cf08beae0 nid=40404 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

&quot;C2 CompilerThread0&quot; #15 [24392] daemon prio=9 os_prio=2 cpu=78.12ms elapsed=119.13s tid=0x0000017ceb6b8290 nid=24392 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

&quot;C1 CompilerThread0&quot; #23 [51772] daemon prio=9 os_prio=2 cpu=0.00ms elapsed=119.13s tid=0x0000017cf08cbf90 nid=51772 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

&quot;Common-Cleaner&quot; #27 [18664] daemon prio=8 os_prio=1 cpu=0.00ms elapsed=119.09s tid=0x0000017cf09992c0 nid=18664 waiting on condition  [0x0000001faeefe000]
   java.lang.Thread.State: TIMED_WAITING (parking)
        at jdk.internal.misc.Unsafe.park(java.base@21.0.5/Native Method)
        - parking to wait for  &amp;lt;0x0000000718c0a990&amp;gt; (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(java.base@21.0.5/LockSupport.java:269)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@21.0.5/AbstractQueuedSynchronizer.java:1852)
        at java.lang.ref.ReferenceQueue.await(java.base@21.0.5/ReferenceQueue.java:71)
        at java.lang.ref.ReferenceQueue.remove0(java.base@21.0.5/ReferenceQueue.java:143)
        at java.lang.ref.ReferenceQueue.remove(java.base@21.0.5/ReferenceQueue.java:218)
        at jdk.internal.ref.CleanerImpl.run(java.base@21.0.5/CleanerImpl.java:140)
        at java.lang.Thread.runWith(java.base@21.0.5/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.5/Thread.java:1583)
        at jdk.internal.misc.InnocuousThread.run(java.base@21.0.5/InnocuousThread.java:186)

&quot;Monitor Ctrl-Break&quot; #28 [30652] daemon prio=5 os_prio=0 cpu=15.62ms elapsed=118.98s tid=0x0000017cf0c83a30 nid=30652 runnable  [0x0000001faf2fe000]
   java.lang.Thread.State: RUNNABLE
        at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.5/Native Method)
        at sun.nio.ch.SocketDispatcher.read(java.base@21.0.5/SocketDispatcher.java:46)
        at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.5/NioSocketImpl.java:256)
        at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.5/NioSocketImpl.java:307)
        at sun.nio.ch.NioSocketImpl.read(java.base@21.0.5/NioSocketImpl.java:346)
        at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.5/NioSocketImpl.java:796)
        at java.net.Socket$SocketInputStream.read(java.base@21.0.5/Socket.java:1099)
        at sun.nio.cs.StreamDecoder.readBytes(java.base@21.0.5/StreamDecoder.java:350)
        at sun.nio.cs.StreamDecoder.implRead(java.base@21.0.5/StreamDecoder.java:393)
        at sun.nio.cs.StreamDecoder.lockedRead(java.base@21.0.5/StreamDecoder.java:217)
        at sun.nio.cs.StreamDecoder.read(java.base@21.0.5/StreamDecoder.java:171)
        at java.io.InputStreamReader.read(java.base@21.0.5/InputStreamReader.java:188)
        at java.io.BufferedReader.fill(java.base@21.0.5/BufferedReader.java:160)
        at java.io.BufferedReader.implReadLine(java.base@21.0.5/BufferedReader.java:370)
        at java.io.BufferedReader.readLine(java.base@21.0.5/BufferedReader.java:347)
        at java.io.BufferedReader.readLine(java.base@21.0.5/BufferedReader.java:436)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:53)

&quot;Notification Thread&quot; #29 [52816] daemon prio=9 os_prio=0 cpu=0.00ms elapsed=118.98s tid=0x0000017cf0b2c110 nid=52816 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

&quot;t1&quot; #30 [35856] prio=5 os_prio=0 cpu=0.00ms elapsed=118.81s tid=0x0000017cf106c5d0 nid=35856 waiting for monitor entry  [0x0000001faf4ff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.thread.concurrent1.Test13.lambda$main$0(Test13.java:29)
        - waiting to lock &amp;lt;0x000000071876c538&amp;gt; (a java.lang.Object)
        - locked &amp;lt;0x000000071876c528&amp;gt; (a java.lang.Object)
        at com.thread.concurrent1.Test13$$Lambda/0x0000017c8101c768.run(Unknown Source)
        at java.lang.Thread.runWith(java.base@21.0.5/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.5/Thread.java:1583)

&quot;t2&quot; #31 [38604] prio=5 os_prio=0 cpu=0.00ms elapsed=118.81s tid=0x0000017cf106cc30 nid=38604 waiting for monitor entry  [0x0000001faf5ff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.thread.concurrent1.Test13.lambda$main$1(Test13.java:44)
        - waiting to lock &amp;lt;0x000000071876c528&amp;gt; (a java.lang.Object)
        - locked &amp;lt;0x000000071876c538&amp;gt; (a java.lang.Object)
        at com.thread.concurrent1.Test13$$Lambda/0x0000017c8101c980.run(Unknown Source)
        at java.lang.Thread.runWith(java.base@21.0.5/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.5/Thread.java:1583)

&quot;DestroyJavaVM&quot; #32 [52200] prio=5 os_prio=0 cpu=46.88ms elapsed=118.81s tid=0x0000017cca2f3c10 nid=52200 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

&quot;VM Thread&quot; os_prio=2 cpu=0.00ms elapsed=119.15s tid=0x0000017ceb6a4940 nid=40420 runnable

&quot;GC Thread#0&quot; os_prio=2 cpu=0.00ms elapsed=119.17s tid=0x0000017cca5e5290 nid=37688 runnable

&quot;G1 Main Marker&quot; os_prio=2 cpu=0.00ms elapsed=119.17s tid=0x0000017cca5f5800 nid=36744 runnable

&quot;G1 Conc#0&quot; os_prio=2 cpu=0.00ms elapsed=119.17s tid=0x0000017cca5f69b0 nid=18468 runnable

&quot;G1 Refine#0&quot; os_prio=2 cpu=0.00ms elapsed=119.17s tid=0x0000017ceb55a7f0 nid=14228 runnable

&quot;G1 Service&quot; os_prio=2 cpu=0.00ms elapsed=119.17s tid=0x0000017ceb55e030 nid=43776 runnable

&quot;VM Periodic Task Thread&quot; os_prio=2 cpu=0.00ms elapsed=119.16s tid=0x0000017ceb691980 nid=36304 waiting on condition

JNI global refs: 23, weak refs: 0


Found one Java-level deadlock:
=============================
&quot;t1&quot;:
  waiting to lock monitor 0x0000017cf0f48a60 (object 0x000000071876c538, a java.lang.Object),
  which is held by &quot;t2&quot;

&quot;t2&quot;:
  waiting to lock monitor 0x0000017cf0f47800 (object 0x000000071876c528, a java.lang.Object),
  which is held by &quot;t1&quot;

Java stack information for the threads listed above:
===================================================
&quot;t1&quot;:
        at com.thread.concurrent1.Test13.lambda$main$0(Test13.java:29)
        - waiting to lock &amp;lt;0x000000071876c538&amp;gt; (a java.lang.Object)
        - locked &amp;lt;0x000000071876c528&amp;gt; (a java.lang.Object)
        at com.thread.concurrent1.Test13$$Lambda/0x0000017c8101c768.run(Unknown Source)
        at java.lang.Thread.runWith(java.base@21.0.5/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.5/Thread.java:1583)
&quot;t2&quot;:
        at com.thread.concurrent1.Test13.lambda$main$1(Test13.java:44)
        - waiting to lock &amp;lt;0x000000071876c528&amp;gt; (a java.lang.Object)
        - locked &amp;lt;0x000000071876c538&amp;gt; (a java.lang.Object)
        at com.thread.concurrent1.Test13$$Lambda/0x0000017c8101c980.run(Unknown Source)
        at java.lang.Thread.runWith(java.base@21.0.5/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.5/Thread.java:1583)

Found 1 deadlock.


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过查看&lt;code&gt;Found one Java-level deadlock:&lt;/code&gt;以下字样，我们能够知道第几行发生了死锁，以及哪个线程发生了死锁&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用jconsole定位死锁&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;cmd窗口输入&lt;code&gt;jconsole&lt;/code&gt;命令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行连接
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-28_22-10-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击线程，点检测死锁
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-28_22-11-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看死锁信息
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-28_22-12-57.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;哲学家就餐&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page82_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有五位哲学家，围坐在圆桌旁。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;他们只做两件事，思考和吃饭，思考一会吃口饭，吃完饭后接着思考。&lt;/li&gt;
&lt;li&gt;吃饭时要用两根筷子吃，桌上共有5根筷子，每位哲学家左右手边各有一根筷子。&lt;/li&gt;
&lt;li&gt;如果筷子被身边的人拿着，自己就得等待&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class TestDeadLock {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick(&quot;1&quot;);
        Chopstick c2 = new Chopstick(&quot;2&quot;);
        Chopstick c3 = new Chopstick(&quot;3&quot;);
        Chopstick c4 = new Chopstick(&quot;4&quot;);
        Chopstick c5 = new Chopstick(&quot;5&quot;);

        new Philosopher(&quot;苏格拉底&quot;, c1, c2).start();
        new Philosopher(&quot;柏拉图&quot;, c2, c3).start();
        new Philosopher(&quot;亚里士多德&quot;, c3, c4).start();
        new Philosopher(&quot;赫拉克利特&quot;, c4, c5).start();
        new Philosopher(&quot;阿基米德&quot;, c5, c1).start();
    }

}

class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return &quot;筷子{&quot; + name + &apos;}&apos;;
    }
}

@Slf4j
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    private void eat() throws InterruptedException {
        log.debug(&quot;eating...&quot;);
        Thread.sleep(1000);
    }

    @SneakyThrows
    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    eat();
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;12:33:15.575 [苏格拉底] c.Philosopher - eating... 
12:33:15.575 [亚里士多德] c.Philosopher - eating... 
12:33:16.580 [阿基米德] c.Philosopher - eating... 
12:33:17.580 [阿基米德] c.Philosopher - eating... 
// 卡在这里, 不向下运行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 jconsole 检测死锁，发现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种线程没有按预期结束，执行不下去的情况，归类为【活跃性】问题，除了死锁以外，还有活锁和饥饿者两种情况&lt;/p&gt;
&lt;h5&gt;活锁&lt;/h5&gt;
&lt;p&gt;活锁出现在两个线程互相改变对方的结束条件，最后谁也无法结束，例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    
    public static void main(String[] args) {
        new Thread(() -&amp;gt; {  // 它的目标是把 count 减到 0 然后退出循环。
            // 期望减到 0 退出循环
            while (count &amp;gt; 0) {
                sleep(0.2);
                count--;
                log.debug(&quot;count: {}&quot;, count);
            }
        }, &quot;t1&quot;).start();
        
        new Thread(() -&amp;gt; {  // 它的目标是把 count 加到 20 然后退出循环。
            // 期望超过 20 退出循环
            while (count &amp;lt; 20) {
                sleep(0.2);
                count++;
                log.debug(&quot;count: {}&quot;, count);
            }
        }, &quot;t2&quot;).start();
        
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们增加些不同的睡眠时间，来解决活锁问题；引入随机等待时间，减少线程间的同步碰撞。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;t1 的条件是 count &amp;gt; 0 → 始终为 true。&lt;/li&gt;
&lt;li&gt;t2 的条件是 count &amp;lt; 20 → 也始终为 true。
两个线程都无法结束，程序就会一直跑下去。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这个例子里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;t1 想减到 0，但每次减完又被 t2 加回来。&lt;/li&gt;
&lt;li&gt;t2 想加到 20，但每次加完又被 t1 减回来。
两个线程都没有停下，但结果就是任务始终完成不了。 这就是 &lt;strong&gt;活锁&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;饥饿&lt;/h5&gt;
&lt;p&gt;始终得不到 CPU 调度执行&lt;/p&gt;
&lt;p&gt;很多教程中把饥饿定义为，一个线程由于优先级太低，始终得不到 CPU 调度执行，也不能够结束，饥饿的情况不
易演示，讲读写锁时会涉及饥饿问题&lt;/p&gt;
&lt;p&gt;下面我讲一下我遇到的一个线程饥饿的例子，
先来看看使用顺序加锁的方式解决之前的死锁问题&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-28_23-06-48.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;顺序加锁的解决方案&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-28_23-07-17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但顺序加锁容易产生饥饿问题&lt;/p&gt;
&lt;p&gt;例如 哲学家就餐时&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class TestDeadLock {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick(&quot;1&quot;);
        Chopstick c2 = new Chopstick(&quot;2&quot;);
        Chopstick c3 = new Chopstick(&quot;3&quot;);
        Chopstick c4 = new Chopstick(&quot;4&quot;);
        Chopstick c5 = new Chopstick(&quot;5&quot;);

        new Philosopher(&quot;苏格拉底&quot;, c1, c2).start();
        new Philosopher(&quot;柏拉图&quot;, c2, c3).start();
        new Philosopher(&quot;亚里士多德&quot;, c3, c4).start();
        new Philosopher(&quot;赫拉克利特&quot;, c4, c5).start();
        //new Philosopher(&quot;阿基米德&quot;, c5, c1).start();
        new Philosopher(&quot;阿基米德&quot;, c1, c5).start();
    }

}

class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return &quot;筷子{&quot; + name + &apos;}&apos;;
    }
}

@Slf4j
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    private void eat() throws InterruptedException {
        log.info(&quot;eating...&quot;);
        Thread.sleep(1000);
    }

    @SneakyThrows
    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    eat();
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;23:10:38.331 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:38.331 [苏格拉底] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:39.337 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:39.337 [苏格拉底] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:40.351 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:40.351 [苏格拉底] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:41.363 [苏格拉底] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:41.363 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:42.374 [苏格拉底] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:42.374 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:43.384 [苏格拉底] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:43.384 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:44.399 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:45.399 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:46.407 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:47.413 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:48.423 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:49.432 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:50.441 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:51.457 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:52.468 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:53.477 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:54.487 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:55.501 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:56.514 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:57.525 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:58.535 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
23:10:59.543 [赫拉克利特] INFO com.thread.concurrent1.Philosopher -- eating...
......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总有一个人始终得不到cpu的调度；&lt;/p&gt;
&lt;h3&gt;ReentrantLock&lt;/h3&gt;
&lt;p&gt;相对于 synchronized 它具备如下特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可中断&lt;/li&gt;
&lt;li&gt;可以设置超时时间&lt;/li&gt;
&lt;li&gt;可以设置为公平锁(可防止线程饥饿)&lt;/li&gt;
&lt;li&gt;支持多个条件变量
与 synchronized 一样，都支持可重入&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;支持多个条件变量：像synchronized，当条件不满足时，会进入WaitSet进行等待，WaitSet就相当条件变量，条件不满足时，线程就会在这里等待；ReentrantLock是支持多个WaitSet的，不满足条件1的到一个WaitSet中等，不满足条件2的到另一个WaitSet中等；&lt;/p&gt;
&lt;p&gt;基本语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取锁
reentrantLock.lock();
try {
    // 临界区
} finally {
    // 释放锁
    reentrantLock.unlock();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;可重入&lt;/h4&gt;
&lt;p&gt;可重入是指同一个线程如果首次获得了这把锁，那么因为它是这把锁的拥有者，因此有权利再次获取这把锁&lt;/p&gt;
&lt;p&gt;如果是不可重入锁，那么第二次获得锁时，自己也会被锁挡住&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
    method1();
}

public static void method1() {
    lock.lock();
    try {
        log.debug(&quot;execute method1&quot;);
        method2();
    } finally {
        lock.unlock();
    }
}

public static void method2() {
    lock.lock();
    try {
        log.debug(&quot;execute method2&quot;);
        method3();
    } finally {
        lock.unlock();
    }
}

public static void method3() {
    lock.lock();
    try {
        log.debug(&quot;execute method3&quot;);
    } finally {
        lock.unlock();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;17:59:11.862 [main] c.TestReentrant - execute method1 
17:59:11.865 [main] c.TestReentrant - execute method2 
17:59:11.865 [main] c.TestReentrant - execute method3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;可打断&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;lock.lockInterruptibly()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个方法和 &lt;code&gt;lock.lock()&lt;/code&gt; 的区别在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lock.lock()&lt;/code&gt; → 如果获取不到锁，就会一直等，不能被打断。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lock.lockInterruptibly()&lt;/code&gt; → 如果获取不到锁，就会进入等待队列，但是可以被其他线程 &lt;code&gt;interrupt()&lt;/code&gt; 打断。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test14 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            try {
                // 如果没有竞争那么此方法就会获取 Lock 对象锁
                // 如果有竞争就进入阻塞队列，可以被其它线程用 interruput 方法打断
                log.info(&quot;尝试获取锁&quot;);
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                log.info(&quot;没有获取锁，返回&quot;);
                e.printStackTrace();
                return;
            }
            try {
                log.info(&quot;获取到锁&quot;);
            } finally {
                lock.unlock();
            }
        }, &quot;t1&quot;);

        lock.lock();    // 主线程先获取锁

        t1.start();

        Thread.sleep(1000); // 一秒后主线程打断t1

        log.info(&quot;打断t1线程&quot;);
        t1.interrupt();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;00:18:22.742 [t1] INFO com.thread.concurrent1.Test14 -- 尝试获取锁
00:18:23.746 [main] INFO com.thread.concurrent1.Test14 -- 打断t1线程
00:18:23.746 [t1] INFO com.thread.concurrent1.Test14 -- 没有获取锁，返回
java.lang.InterruptedException
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1011)
	at java.base/java.util.concurrent.locks.ReentrantLock$Sync.lockInterruptibly(ReentrantLock.java:161)
	at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:372)
	at com.thread.concurrent1.Test14.lambda$main$0(Test14.java:25)
	at java.base/java.lang.Thread.run(Thread.java:1583)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子通过先让 main 拿锁、t1 等待，然后 main 打断 t1，展示了 lockInterruptibly() 的可中断性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;main 线程休眠 1 秒后打断 t1&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于 t1 正在等待锁，而且是通过 lockInterruptibly() 在等，所以它能响应中断。&lt;/li&gt;
&lt;li&gt;一旦被打断，lock.lockInterruptibly() 会抛出 InterruptedException。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
打断只是 &lt;strong&gt;抛出中断异常&lt;/strong&gt;或者 &lt;strong&gt;设置中断标记&lt;/strong&gt;，如何中断处理 线程自行决定，但是如果线程处于阻塞状态，则可以打断线程的阻塞状态，让线程立即退出阻塞状态，并抛出 &lt;code&gt;InterruptedException&lt;/code&gt; 异常。
:::&lt;/p&gt;
&lt;h4&gt;锁超时&lt;/h4&gt;
&lt;p&gt;在 ReentrantLock 里，除了常见的 lock()（阻塞直到获得锁）、lockInterruptibly()（阻塞但可中断），还有一个 非阻塞的获取锁方法：tryLock()。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;tryLock()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (lock.tryLock()) {
    try {
        // 获取到锁，执行临界区代码
    } finally {
        lock.unlock();
    }
} else {
    // 没拿到锁，立即返回 false
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;不会阻塞，如果当前锁空闲，就立刻获取到锁并返回 true；&lt;/li&gt;
&lt;li&gt;如果锁被别人占用，就立刻返回 false；&lt;/li&gt;
&lt;li&gt;适合“试探性”加锁，不会把线程卡死。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;tryLock(long timeout, TimeUnit unit)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (lock.tryLock(2, TimeUnit.SECONDS)) {
    try {
        // 获取到锁
    } finally {
        lock.unlock();
    }
} else {
    // 在 2 秒内没拿到锁，返回 false
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;最多等待指定时间；&lt;/li&gt;
&lt;li&gt;这段时间内如果锁释放了，就拿到锁返回 true；&lt;/li&gt;
&lt;li&gt;如果时间到了还没拿到锁，就返回 false；&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;行为&lt;/th&gt;
&lt;th&gt;可中断&lt;/th&gt;
&lt;th&gt;超时&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lock()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;一直阻塞直到获得锁&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lockInterruptibly()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;阻塞直到获得锁，但可被 &lt;code&gt;interrupt()&lt;/code&gt; 打断&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tryLock()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;立即尝试获取锁，失败就返回 &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tryLock(timeout)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最多等待一段时间，可被打断&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test15 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -&amp;gt; {
            log.info(&quot;尝试获取锁&quot;);
            try {
                if (!lock.tryLock(1, TimeUnit.MILLISECONDS)) {
                    log.info(&quot;获取不到锁，返回&quot;);
                    return;
                }
            } catch (InterruptedException e) {
                log.info(&quot;获取不到锁&quot;);
                throw new RuntimeException(e);
            }
            try {
                log.info(&quot;t1获取到锁&quot;);
            } finally {
                lock.unlock();
            }
        }, &quot;t1&quot;);

        lock.lock();
        log.info(&quot;main线程获取到锁&quot;);

        t1.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;23:39:47.740 [main] INFO com.thread.concurrent1.Test15 -- main线程获取到锁
23:39:47.745 [t1] INFO com.thread.concurrent1.Test15 -- 尝试获取锁
23:39:47.752 [t1] INFO com.thread.concurrent1.Test15 -- 获取不到锁，返回
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;main 线程一开始就拿到了这把 ReentrantLock。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此时锁被占用，其他线程想要再拿这把锁就得等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;t1 启动后，调用 &lt;code&gt;lock.tryLock(1, TimeUnit.MILLISECONDS)&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tryLock(timeout, unit)&lt;/code&gt; 的含义是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尝试获取锁，最多等 1ms，如果 1ms 内没拿到，就返回 false。&lt;/li&gt;
&lt;li&gt;如果等的过程中被 &lt;code&gt;interrupt()&lt;/code&gt; 打断，会抛 &lt;code&gt;InterruptedException&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于锁在 main 线程手里，而且 main 并没有释放锁，所以 t1 在 1ms 内肯定拿不到锁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以 tryLock 返回 false，t1 打印 &quot;获取不到锁，返回&quot;，然后结束。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;公平锁&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;公平锁 (Fair Lock) 是指按照线程请求锁的先后顺序来获取锁，即 &lt;strong&gt;先来先得 (FIFO)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果多个线程同时等待一把锁，那么第一个请求锁的线程会先被唤醒并获得锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与之对应的是 非公平锁 (Nonfair Lock)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程获取锁时，可能插队，不一定遵循先来后到。&lt;/li&gt;
&lt;li&gt;非公平锁在性能上通常更好&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ReentrantLock 构造函数可以指定是否公平：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 默认是非公平锁
ReentrantLock lock1 = new ReentrantLock();

// 显式指定公平锁
ReentrantLock lock2 = new ReentrantLock(true);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
公平锁能有效避免 线程饥饿 (Starvation)，因为总是先到先得。&lt;/p&gt;
&lt;p&gt;非公平锁有可能出现某些线程长时间拿不到锁（但实际 JVM 的调度通常能避免完全饿死）。
:::&lt;/p&gt;
&lt;h4&gt;条件变量&lt;/h4&gt;
&lt;p&gt;synchronized 中也有条件变量，就是我们讲原理时那个 waitSet 休息室，当条件不满足时进入 waitSet 等待&lt;/p&gt;
&lt;p&gt;ReentrantLock 的条件变量比 synchronized 强大之处在于，它是支持多个条件变量的，这就好比&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;synchronized 是那些不满足条件的线程都在一间休息室等消息&lt;/li&gt;
&lt;li&gt;而 ReentrantLock 支持多间休息室，有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;await 前需要获得锁&lt;/li&gt;
&lt;li&gt;await 执行后，会释放锁，进入 conditionObject 等待&lt;/li&gt;
&lt;li&gt;await 的线程被唤醒（或打断、或超时）去重新竞争 lock 锁&lt;/li&gt;
&lt;li&gt;竞争 lock 锁成功后，从 await 后继续执行
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;await() 行为：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用这个方法的线程放入该 &lt;code&gt;Condition&lt;/code&gt; 的等待队列并阻塞&lt;/li&gt;
&lt;li&gt;会释放锁&lt;/li&gt;
&lt;li&gt;被 signal/signalAll 唤醒后，不是立刻运行，而是先回到锁的队列去竞争锁；拿到锁后 await() 才返回，才会继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;signal() 行为：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;唤醒等待队列中的某个线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;signalAll() 行为：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;唤醒等待队列中的所有线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;简单示例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test16 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();

        lock.lock();

        try {
            condition1.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        condition1.signal();

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;condition1 和 condition2 是 依附在同一把 lock 上的两个条件队列。&lt;/li&gt;
&lt;li&gt;每个条件变量维护着自己的一组等待线程队列。&lt;/li&gt;
&lt;li&gt;一个 lock 可以对应多个 Condition，比 synchronized 的 wait/notify 更灵活。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用 await() 会做几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原子释放锁（把刚才的 lock 释放掉）。&lt;/li&gt;
&lt;li&gt;把当前线程（这里是 main 线程自己）加入到 condition1 的等待队列里。&lt;/li&gt;
&lt;li&gt;当前线程进入 等待状态，直到被别人用 condition1.signal() 或 condition1.signalAll() 唤醒。&lt;/li&gt;
&lt;li&gt;被唤醒后，会重新去竞争锁，竞争成功才会从 await() 返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这段代码：一旦调用 await()，main 线程就在这里阻塞了，后续代码不会执行，除非有其他线程来唤醒它。当前程序只有一个线程（main），它在 await() 那里已经阻塞住了，不会再往下走。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用示例&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有两类线程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个线程在等 烟。&lt;/li&gt;
&lt;li&gt;一个线程在等 早餐。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只有对应的“货物”来了以后，它们才能继续执行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们需要 两条等待队列：waitCigaretteQueue 和 waitbreakfastQueue&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static ReentrantLock lock = new ReentrantLock();

static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();

static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;

public static void main(String[] args) {
    
    new Thread(() -&amp;gt; {
        try {
            lock.lock();
            while (!hasCigrette) {
                try {
                    waitCigaretteQueue.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug(&quot;等到了它的烟&quot;);
        } finally {
            lock.unlock();
        }
    }).start();
    
    new Thread(() -&amp;gt; {
        try {
            lock.lock();
            while (!hasBreakfast) {
                try {
                    waitbreakfastQueue.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug(&quot;等到了它的早餐&quot;);
        } finally {
            lock.unlock();
        }
    }).start();
    
    sleep(1);
    sendBreakfast();
    sleep(1);
    sendCigarette();
}

private static void sendCigarette() {
    lock.lock();
    try {
        log.debug(&quot;送烟来了&quot;);
        hasCigrette = true;
        waitCigaretteQueue.signal();
    } finally {
        lock.unlock();
    }
}

private static void sendBreakfast() {
    lock.lock();
    try {
        log.debug(&quot;送早餐来了&quot;);
        hasBreakfast = true;
        waitbreakfastQueue.signal();
    } finally {
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;18:52:27.680 [main] c.TestCondition - 送早餐来了
18:52:27.682 [Thread-1] c.TestCondition - 等到了它的早餐
18:52:28.683 [main] c.TestCondition - 送烟来了
18:52:28.683 [Thread-0] c.TestCondition - 等到了它的烟
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;用 while 而不是 if，是因为被唤醒后要再次检查条件（可能被错误唤醒）。&lt;/li&gt;
&lt;li&gt;await() 调用时会：
&lt;ol&gt;
&lt;li&gt;释放锁；&lt;/li&gt;
&lt;li&gt;把当前线程放入对应的条件队列；&lt;/li&gt;
&lt;li&gt;阻塞等待 signal() 唤醒。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;只会唤醒在对应队列上等待的线程，不会影响其他队列的线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;和 Object.wait/notify 的对比&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object.wait/notify&lt;/code&gt; 只有一条等待队列，所有线程都混在一起。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Condition&lt;/code&gt; 可以为同一把锁创建多条队列，分类更细。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这段代码用 ReentrantLock + 多个 Condition 模拟了“送烟 &amp;amp; 送早餐”的场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;等烟的线程只在 waitCigaretteQueue 队列等；&lt;/li&gt;
&lt;li&gt;等早餐的线程只在 waitbreakfastQueue 队列等；&lt;/li&gt;
&lt;li&gt;主线程控制谁先来（先送早餐，再送烟）。
这样就能保证线程被精准唤醒，而不会互相干扰。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;同步模式之顺序控制&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV16J411h7Rd?t=11.0&amp;amp;p=128&quot;&gt;地址&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
</content:encoded></item><item><title>Java 并发编程基础</title><link>https://zzyang.top/posts/java-concurrency/</link><guid isPermaLink="true">https://zzyang.top/posts/java-concurrency/</guid><pubDate>Mon, 18 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;进程与线程&lt;/h2&gt;
&lt;h3&gt;进程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;程序由指令和数据组成，但这些指令要运行，数据要读写，就必须将指令加载至 CPU，数据加载至内存。在指令运行过程中还需要用到磁盘 、网络等设备。&lt;strong&gt;进程就是用来加载指令、管理内存、管理 IO 的&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当一个程序被运行，从磁盘加载这个程序的代码至内存，这时就开启了一个进程。&lt;/li&gt;
&lt;li&gt;进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程（例如记事本、画图、浏览器等），也有的程序只能启动一个实例进程（例如网易云音乐、360 安全卫士等）&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;进程可以理解为程序的执行过程，是动态的！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;线程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个进程之内可以分为一到多个线程。&lt;/li&gt;
&lt;li&gt;一个线程就是一个指令流，将指令流中的一条条指令以一定的顺序交给 CPU 执行&lt;/li&gt;
&lt;li&gt;Java 中，线程是最小的调度单位，进程作为资源分配的最小单位。 在 Windows 中进程是不活动的，只是作为线程的容器&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;进程基本上相互独立，而线程存在于进程内，是进程的一个子集&lt;/li&gt;
&lt;li&gt;进程拥有共享的资源，如内存空间等，供其内部的线程共享&lt;/li&gt;
&lt;li&gt;进程间通信较为复杂
&lt;ul&gt;
&lt;li&gt;同一台计算机的进程通信称为 IPC (Inter-process communication)&lt;/li&gt;
&lt;li&gt;不同计算机之间的进程通信，需要通过网络，并遵守共同的协议，例如 HTTP 协议&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线程通信相对简单，因为它们共享进程内的内存，多个线程可以访问同一个共享变量&lt;/li&gt;
&lt;li&gt;线程更轻量，线程上下文切换成本一般要比进程上下文切换低&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;💡面试题&lt;/h3&gt;
&lt;p&gt;什么是进程和线程？&lt;/p&gt;
&lt;p&gt;进程是程序的一次执行过程，是系统运行程序的基本单位，因此进程是动态的。系统运行一个程序即是一个进程从创建，运行到消亡的过程。&lt;/p&gt;
&lt;p&gt;在 Java 中，当我们启动 main 函数时其实就是启动了一个 JVM 的进程，而 main 函数所在的线程就是这个进程中的一个线程，也称主线程。&lt;/p&gt;
&lt;p&gt;如下图所示，在 Windows 中通过查看任务管理器的方式，我们就可以清楚看到 Windows 当前运行的进程（.exe 文件的运行）。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/xw_20250811211830.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;何为线程?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;线程与进程相似，但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的&lt;strong&gt;堆&lt;/strong&gt;和&lt;strong&gt;方法区&lt;/strong&gt;资源，但每个线程有自己的&lt;strong&gt;程序计数器&lt;/strong&gt;、&lt;strong&gt;虚拟机栈&lt;/strong&gt;和&lt;strong&gt;本地方法栈&lt;/strong&gt;，所以系统在产生一个线程，或是在各个线程之间做切换工作时，负担要比进程小得多，也正因为如此，线程也被称为轻量级进程。&lt;/p&gt;
&lt;h2&gt;并行与并发&lt;/h2&gt;
&lt;p&gt;并发是在同一时间段，并行是在同一时刻！&lt;/p&gt;
&lt;p&gt;:::tip
并发（Concurrent）：一个人同时做很多不同事情&lt;/p&gt;
&lt;p&gt;并行（Parallel）：一群人各自同时做很多事情
:::&lt;/p&gt;
&lt;h3&gt;并发&lt;/h3&gt;
&lt;p&gt;单核 CPU 下，线程实际还是 &lt;code&gt;串行执行&lt;/code&gt; 的。操作系统中有一个组件叫做&lt;em&gt;任务调度器&lt;/em&gt;，将 CPU 的时间片（windows 下时间片最小约为 15 毫秒）分给不同的程序使用，只是由于 CPU 在线程间（时间片很短）的切换非常快，人类感觉是同时运行的 。总结为一句话就是： &lt;em&gt;微观串行，宏观并行&lt;/em&gt; 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一般我们会将这种 &lt;em&gt;线程轮流使用CPU&lt;/em&gt; 的做法称为:  &lt;strong&gt;并发（ Concurrent）&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;时间片 1&lt;/th&gt;
&lt;th&gt;时间片 2&lt;/th&gt;
&lt;th&gt;时间片 3&lt;/th&gt;
&lt;th&gt;时间片 4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;core&lt;/td&gt;
&lt;td&gt;线程 1&lt;/td&gt;
&lt;td&gt;线程 2&lt;/td&gt;
&lt;td&gt;线程 3&lt;/td&gt;
&lt;td&gt;线程 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page7_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;并行&lt;/h3&gt;
&lt;p&gt;多核 cpu下，每个 &lt;code&gt;核（core）&lt;/code&gt; 都可以调度运行线程，这时候线程可以是 &lt;strong&gt;并行&lt;/strong&gt; 的&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;时间片 1&lt;/th&gt;
&lt;th&gt;时间片 2&lt;/th&gt;
&lt;th&gt;时间片 3&lt;/th&gt;
&lt;th&gt;时间片 4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;core 1&lt;/td&gt;
&lt;td&gt;线程 1&lt;/td&gt;
&lt;td&gt;线程 1&lt;/td&gt;
&lt;td&gt;线程 3&lt;/td&gt;
&lt;td&gt;线程 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;core 2&lt;/td&gt;
&lt;td&gt;线程 2&lt;/td&gt;
&lt;td&gt;线程 4&lt;/td&gt;
&lt;td&gt;线程 2&lt;/td&gt;
&lt;td&gt;线程 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page8_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在多核 CPU 下并发和并行是同时存在的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::info&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并发（concurrent）是同一时间应对（dealing with）多件事情的能力&lt;/li&gt;
&lt;li&gt;并行（parallel）是同一时间动手做（doing）多件事情的能力
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生活例子&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;家庭主妇做饭、打扫卫生、给孩子喂奶，她一个人轮流交替做这多件事，这时就是并发&lt;/li&gt;
&lt;li&gt;家庭主妇雇了个保姆，她们一起这些事，这时既有并发，也有并行（这时会产生竞争，例如锅只有一口，一个人用锅时，另一个人就得等待）&lt;/li&gt;
&lt;li&gt;雇了3个保姆，一个专做饭、一个专打扫卫生、一个专喂奶，互不干扰，这时是并行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;💡面试题&lt;/h3&gt;
&lt;p&gt;并发和并行的区别&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并发：两个及两个以上的作业在同一 时间段 内执行&lt;/li&gt;
&lt;li&gt;并行：两个及两个以上的作业在同一 时刻 执行
最关键的点是：是否是 同时 执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步和异步的区别&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同步：发出一个调用之后，在没有得到结果之前， 该调用就不可以返回，一直等待。&lt;/li&gt;
&lt;li&gt;异步：调用在发出之后，不用等待返回结果，该调用直接返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;线程基本应用&lt;/h2&gt;
&lt;h3&gt;异步调用&lt;/h3&gt;
&lt;p&gt;从方法调用方的角度来讲，如果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要等待结果返回，才能继续运行就是 &lt;strong&gt;同步&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;不需要等待结果返回，就能继续运行就是 &lt;strong&gt;异步&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：同步在多线程中还有另外一个意思，就是让多个线程步调一致&lt;/p&gt;
&lt;p&gt;1）设计
多线程可以让方法执行变成异步的（即不要一直干等着）。比如读取磁盘文件时，假设读取操作需要花费 5s，如果没有线程调度机制，那么这 5s 调用者其他的事都做不了，其余代码都得暂停。&lt;/p&gt;
&lt;p&gt;2）结论&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在项目中，如果需要进行一些费时操作，比如视频文件需要转换格式等操作，这时开一个新线程处理视频转换，避免阻塞住主线程&lt;/li&gt;
&lt;li&gt;Tomcat 的异步 Servlet 也是类似的目的，让用户线程处理耗时较长的操作，避免阻塞 Tomcat 的工作线程&lt;/li&gt;
&lt;li&gt;UI 程序中，开线程进行其他操作，避免阻塞 UI 线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步等待&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Sync&quot;)
public class Sync {
    public static void main(String[] args) {
        FileReader.read(Constants.MP4_FULL_PATH);  // 同步调用
        log.debug(&quot;do other things ...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;异步不等待&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Async&quot;)
public class Async {
    public static void main(String[] args) {
        // 异步调用
        new Thread(new Runnable() {
            public void run() {
                FileReader.read(Constants.MP4_FULL_PATH);
            }
        }).start();

        log.debug(&quot;do other things ...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;提高运行效率&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;充分利用多核 cpu 的优势，提高运行效率。想象下面的场景：执行 3 个计算，最后将计算结果汇总。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;计算 1 花费 10 ms&lt;/p&gt;
&lt;p&gt;计算 2 花费 11 ms&lt;/p&gt;
&lt;p&gt;计算 3 花费 9 ms&lt;/p&gt;
&lt;p&gt;汇总需要 1 ms&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;如果是串行执行，那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms&lt;/li&gt;
&lt;li&gt;但如果是四核 cpu，各个核心分别使用线程 1 执行计算 1，线程 2 执行计算 2，线程 3 执行计算 3，那么 3 个 线程是并行的，花费时间只取决于最长的那个线程运行的时间，即 11ms最后加上汇总时间只会花费 12ms&lt;/li&gt;
&lt;li&gt;需要在多核 cpu 才能提高效率，单核仍然是轮流执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;创建与运行线程&lt;/h2&gt;
&lt;h3&gt;使用Thread&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Java 程序在启动时，都会创建一个主方法线程（也称：主线程）。默认就已经有一个主线程在运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 创建线程对象
Thread t = new Thread(&quot;t1&quot;) {
    @Override
    public void run() {
        // 要执行的任务
    }
};
// 启动线程
t.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;指定名称&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test1&quot;)
public class DirectUseThreadTest {
    public static void main(String[] args) {
        // 创建线程
        Thread t1 = new Thread(&quot;t1&quot;) {  // 指定名称（方式一）
            @Override
            public void run() {
                log.debug(&quot;running ...&quot;);
            }
        };

        t1.setName(&quot;t1&quot;);  // 指定名称（方式二）

        // 启动线程
        t1.start();

        // 主线程打印
        log.debug(&quot;running ...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lambda写法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class ThreadDemo {
    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            log.info(&quot;running&quot;);
        }, &quot;t1&quot;).start();

        log.info(&quot;main&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用Runable配合Thread&lt;/h3&gt;
&lt;p&gt;把【线程】和【任务】(要执行的代码) 分开&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thread 代表线程&lt;/li&gt;
&lt;li&gt;Runnable 代表可运行的任务（线程要执行的代码）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Runnable runnable = new Runnable() {
    @Override
    public void run() {
        // 要执行的任务
    }
};
// 创建线程对象
Thread t = new Thread(runnable);
// 启动线程
t.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 创建任务对象
Runnable task2 = new Runnable() {
    @Override
    public void run() {
        log.debug(&quot;hello&quot;);
    }
};

// 参数1 是任务对象; 参数2 是线程名字
Thread t2 = new Thread(task2, &quot;t2&quot;);
t2.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;lambda表达式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建任务对象
Runnable task2 = () -&amp;gt; log.debug(&quot;hello&quot;);

// 参数1 是任务对象; 参数2 是线程名字，推荐
Thread t2 = new Thread(task2, &quot;t2&quot;);
t2.start();



public static void test2() {
    Runnable runnable = () -&amp;gt; log.info(&quot;runnable running&quot;);
    new Thread(runnable, &quot;t2&quot;).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理 —— Thread 与 Runnable 的关系&lt;/p&gt;
&lt;p&gt;Thread 核心源码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Runnable target;

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际，就是在 Thread 中的 run 方法里面调用了 Runnable 方法的 run 方法来执行任务。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种创建线程的方法是把线程和任务的创建合并在了一起，第二种创建线程的方法是把线程和任务分开创建了&lt;/li&gt;
&lt;li&gt;用 Runnable 更容易与线程池等高级 API 配合&lt;/li&gt;
&lt;li&gt;用 Runnable 让任务类脱离了 Thread 继承体系，更灵活&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;FutureTask配合Thread&lt;/h3&gt;
&lt;p&gt;FutureTask能够接收Callable类型的参数，用来&lt;strong&gt;处理有返回结果的情况&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test3&quot;)
public class FutureAndCallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1.创建任务对象
        FutureTask&amp;lt;Integer&amp;gt; task = new FutureTask&amp;lt;&amp;gt;(new Callable&amp;lt;&amp;gt;() {
            @Override
            public Integer call() throws Exception {
                log.debug(&quot;running ...&quot;);
                Thread.sleep(4000);
                return 100;
            }
        });

        // 2.创建线程对象，并关联任务
        Thread t = new Thread(task, &quot;t3&quot;);

        t.start();

        // 主线程运行到此处时，就会一直阻塞。直到 task 执行完毕后返回结果
        log.debug(&quot;{}&quot;,  task.get());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;11:46:34.082 c.Test3 [t3] - running ...
11:50:35.093 c.Test3 [main] - 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;lambda:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask&amp;lt;Integer&amp;gt; task = new FutureTask&amp;lt;&amp;gt;(() -&amp;gt; {
            TimeUnit.SECONDS.sleep(1);
            log.info(&quot;running...&quot;);
            return 100;
        });
        new Thread(task, &quot;t1&quot;).start();
        Integer i = task.get();
        log.info(&quot;i:{}&quot;, i);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查看进程线程的方法&lt;/h2&gt;
&lt;p&gt;Windows&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务管理器可以查看进程和线程数，也可以用来杀死进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tasklist&lt;/code&gt; 查看进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;taskkill&lt;/code&gt; 杀死进程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;linux&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ps -ef&lt;/code&gt; 查看所有进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps -fT -p &amp;lt;PID&amp;gt;&lt;/code&gt; 查看某个进程（PID）的所有线程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kill&lt;/code&gt; 杀死进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top&lt;/code&gt; 按大写 H 切换是否显示线程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top -H -p &amp;lt;PID&amp;gt;&lt;/code&gt; 查看某个进程（PID）中的所有线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jps&lt;/code&gt; 命令查看所有 Java 进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jstack &amp;lt;PID&amp;gt;&lt;/code&gt; 查看某个 Java 进程（PID）在运行 jstack 时的所有线程状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jconsole&lt;/code&gt; 来查看某个 Java 进程中线程的运行情况（图形界面）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Jconsole 使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;jconsole 远程监控需要先进行如下配置：&lt;/p&gt;
&lt;p&gt;需要以如下方式运行你的 java 类(在服务器上执行的命令)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -Djava.rmi.server.hostname=ip地址 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=连接端口 -Dcom.sun.management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.jmxremote.authenticate=是否认证 java类
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在windows环境下，cmd中输入&lt;code&gt;jconsole&lt;/code&gt;，连接远程的服务器即可，例如&lt;code&gt;192.168.1.100:1099&lt;/code&gt;；要有ip加端口号进行连接。&lt;/p&gt;
&lt;h2&gt;线程运行的原理&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV16J411h7Rd?spm_id_from=333.788.videopod.episodes&amp;amp;vd_source=da7c7c4a886275716b7ca33f532f1905&amp;amp;p=21&quot;&gt;原理视频&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;栈和栈帧&lt;/h3&gt;
&lt;p&gt;Java Virtual Machine Stacks（Java 虚拟机栈）&lt;/p&gt;
&lt;p&gt;我们都知道 &lt;strong&gt;JVM 中由堆、栈、方法区所组成&lt;/strong&gt;，其中栈内存是给谁用的呢？其实就是线程 (对象都是在堆中创建的)，每个线程启动后，虚拟机就会为其分配一块栈内存。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个栈由多个栈帧（Frame）组成，对应着每次方法调用时所占用的内存&lt;/li&gt;
&lt;li&gt;每个线程只能有一个活动栈帧，对应着当前正在执行的那个方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在idea中debug的时候，就能看到栈帧信息；每执行一个方法就会生成一个栈帧，一个线程的栈中可以有多个栈帧（对应方法调用链，如 main → methodA → methodB）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;栈帧是先进后出&lt;/strong&gt;；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每个线程都有自己独立的栈&lt;/strong&gt;；这个栈是线程私有的，与其他线程隔离。&lt;/p&gt;
&lt;h3&gt;线程上下文切换&lt;/h3&gt;
&lt;p&gt;Thread Context Switch（线程上下文切换）&lt;/p&gt;
&lt;p&gt;以下一些原因会导致 CPU 不再执行当前的线程，转而执行另一个线程的代码：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程的 cpu 时间片用完&lt;/li&gt;
&lt;li&gt;垃圾回收（会暂停当前所有的工作线程，让垃圾回收的线程来运行）&lt;/li&gt;
&lt;li&gt;有更高优先级的线程需要运行&lt;/li&gt;
&lt;li&gt;线程自发调用了 sleep()、yield()、wait()、join()、park()、synchronized、lock() 等方法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当上下文切换发生时，需要由操作系统保存当前线程的状态，并恢复另一个线程的状态，Java 中对应的概念就是&lt;strong&gt;程序计数器（Program Counter Register）&lt;/strong&gt;，它的作用是记住下一条 jvm 指令的执行地址，它是线程私有的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态包括程序计数器、虚拟机栈中每个栈帧的信息，如局部变量、操作数栈、返回地址等&lt;/li&gt;
&lt;li&gt;频繁的上下文切换会影响性能&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;相关面试题🤏🏻&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;什么是线程上下文切换？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;线程在执行过程中会有自己的运行条件和状态（也称上下文），比如程序计数器，栈信息等。当出现如下情况的时候，线程会从占用 CPU 状态中退出。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主动让出 CPU，比如调用了sleep(), wait() 等。&lt;/li&gt;
&lt;li&gt;时间片用完，因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。&lt;/li&gt;
&lt;li&gt;调用了阻塞类型的系统中断，比如请求 IO，线程被阻塞。&lt;/li&gt;
&lt;li&gt;被终止或结束运行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这其中前三种都会发生线程切换，线程切换意味着需要保存当前线程的上下文，留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 &lt;strong&gt;上下文切换&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;上下文切换是现代操作系统的基本功能，因其每次需要保存信息恢复信息，这将会占用 CPU，内存等系统资源进行处理，也就意味着效率会有一定损耗，如果频繁切换就会造成整体效率低下。&lt;/p&gt;
&lt;p&gt;:::tip
&lt;strong&gt;“时间片用完”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;“时间片”（Time Slice）是操作系统分配给每个线程/进程的一小段 CPU 使用时间，比如 10 毫秒。
当一个线程开始运行，它只能使用 CPU 一段时间（即这个“时间片”）。
一旦这个时间到了，时间片用完，操作系统就会中断它，进行调度。这种机制叫做 &lt;strong&gt;时间片轮转调度&lt;/strong&gt;（Round-Robin Scheduling）, 时间片的长度由操作系统决定，通常是几毫秒到几十毫秒&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“导致其他线程或进程饿死”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;“饿死”（Starvation）是一个术语，意思是：某些线程长期得不到 CPU 时间，无法执行。&lt;/p&gt;
&lt;p&gt;比如你有 10
个线程，但有一个“霸道”线程一直运行，其他 9 个一直等，永远等不到机会 —— 它们就“饿死了”。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;线程常见方法&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;是否是静态方法&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;th&gt;注意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;start()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;启动一个新线程，在新的线程运行 run 方法中的代码&lt;/td&gt;
&lt;td&gt;start 方法只是让线程进入就绪，里面代码不一定立刻运行（CPU 的时间片还没分给它）。每个线程对象的 start 方法只能调用一次，如果调用了多次会出现 IllegalThreadStateException&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;run()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;新线程启动后会调用的方法&lt;/td&gt;
&lt;td&gt;如果在构造 Thread 对象时传递了 Runnable 参数，则线程启动后会调用 Runnable 中的 run 方法，否则默认不执行任何操作。但可以创建 Thread 的子类对象，来覆盖默认行为&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;join()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;等待线程运行结束&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;join(long n)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;等待线程运行结束,最多等待 n 毫秒&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getId()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;获取线程长整型的 id&lt;/td&gt;
&lt;td&gt;id 唯一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getName()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;获取线程名&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;setName(String)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;修改线程名&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getPriority()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;获取线程优先级&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;setPriority(int)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;修改线程优先级&lt;/td&gt;
&lt;td&gt;java 中规定线程优先级是 1~10 的整数，较大的优先级能提高该线程被 CPU 调度的机率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getState()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;获取线程状态&lt;/td&gt;
&lt;td&gt;Java 中线程状态是用 6 个 enum 表示，分别为：NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isInterrupted()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;判断是否被打断，&lt;/td&gt;
&lt;td&gt;不会清除 打断标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isAlive()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;线程是否存活（还没有运行完毕）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;interrupt()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;打断线程&lt;/td&gt;
&lt;td&gt;如果被打断线程正在 sleep，wait，join 会导致被打断的线程抛出 InterruptedException，并清除 打断标记；如果打断的正在运行的线程，则会设置 打断标记；park 的线程被打断，也会设置 打断标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;interrupted()&lt;/td&gt;
&lt;td&gt;static&lt;/td&gt;
&lt;td&gt;判断当前线程是否被打断&lt;/td&gt;
&lt;td&gt;会清除 打断标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;currentThread()&lt;/td&gt;
&lt;td&gt;static&lt;/td&gt;
&lt;td&gt;获取当前正在执行的线程&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sleep(long n)&lt;/td&gt;
&lt;td&gt;static&lt;/td&gt;
&lt;td&gt;让当前执行的线程休眠n毫秒，休眠时让出 cpu 的时间片给其它线程&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yield()&lt;/td&gt;
&lt;td&gt;static&lt;/td&gt;
&lt;td&gt;提示线程调度器让出当前线程对 CPU 的使用&lt;/td&gt;
&lt;td&gt;主要是为了测试和调试&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;还有一些不推荐使用的方法，这些方法已过时，容易破坏同步代码块，造成线程死锁&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;是否是静态方法&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;stop()&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;停止线程运行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;suspend()&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;挂起（暂停）线程运行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resume()&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;恢复线程运行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;start、run&lt;/h3&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;直接调用run()方法，相当于&lt;strong&gt;同步&lt;/strong&gt;。是在主线程中执行run()方法，并没有启动新的线程来执行！&lt;/li&gt;
&lt;li&gt;通过start()方法来启动线程，相当于&lt;strong&gt;异步&lt;/strong&gt;。通过新的线程来间接执行run()中的代码！&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;start用来启动线程，run是线程启动后，要执行的方法。&lt;/p&gt;
&lt;p&gt;start方法只是让线程进入就绪，里面代码不一定立刻运行（CPU 的时间片还没分给它）。每个线程对象的 start 方法只能调用一次，如果调用了多次会出现 IllegalThreadStateException。&lt;/p&gt;
&lt;p&gt;直接调用run的话不会生成一个新的线程 而是在当前的线程里面执行。直接调用run方法，相当于是同步的，不是异步了&lt;/p&gt;
&lt;p&gt;两者区别代码演示如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test4&quot;)
public class ThreadRunTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(&quot;running...&quot;);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, &quot;t1&quot;);

        t1.run();
        log.debug(&quot;do other things...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;22:36:54.587 c.Test4 [main] - running...
22:36:56.596 c.Test4 [main] - do other things...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现线程一直在【main】线程中执行，run()方法调用还是同步的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test4&quot;)
public class ThreadRunTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(&quot;running...&quot;);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, &quot;t1&quot;);

        // 将 run() 改成 start() 	
        t1.start();
        log.debug(&quot;do other things...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;22:47:58.687 c.Test4 [main] - do other things...
22:47:58.687 c.Test4 [t1] - running...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现run()方法中的代码在t1线程中执行，是异步调用的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;线程执行前后状态信息变化&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test5&quot;)
public class ThreadStateTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(&quot;running...&quot;);
            }
        }, &quot;t1&quot;);

        // 查看线程执行前后的状态信息
        System.out.println(t1.getState());
        t1.start();
//        t1.start();   不能被多次调用，否则会报错
        System.out.println(t1.getState());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NEW  
RUNNABLE
22:55:49.182 c.Test5 [t1] - running...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NEW：初始状态，线程被创建出来但没有被调用start() 。&lt;/li&gt;
&lt;li&gt;RUNNABLE：运行状态，线程被调用了start()等待运行的状态。
:::
不调start就是初始状态，调用了start就是runnable&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;sleep()&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;sleep()方法&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用&lt;code&gt;sleep()&lt;/code&gt;会让当前线程从 &lt;code&gt;Running&lt;/code&gt; 状态进入 &lt;code&gt;Timed Waiting&lt;/code&gt; 状态（运行 -&amp;gt; 阻塞）&lt;/li&gt;
&lt;li&gt;其它线程可以用interrupt方法打断正在睡眠的线程，这时&lt;code&gt;sleep()&lt;/code&gt;会抛出&lt;code&gt;InterruptedException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;睡眠结束后的线程未必会立刻得到执行 (cpu有可能正在执行其他线程的代码，等到任务调度器把新的时间片分给该线程，才会继续运行)&lt;/li&gt;
&lt;li&gt;建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性&lt;/li&gt;
&lt;li&gt;sleep() 会让出 CPU 资源，进入“阻塞”状态。但不释放锁;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class TestSleep {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, &quot;t1&quot;);

        t1.start();
        log.info(&quot;t1:{}&quot;, t1.getState());
        TimeUnit.MILLISECONDS.sleep(500);
        log.info(&quot;t1:{}&quot;, t1.getState());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;20:56:18.937 [main] INFO com.thread.concurrent1.TestSleep -- t1:RUNNABLE
20:56:19.448 [main] INFO com.thread.concurrent1.TestSleep -- t1:TIMED_WAITING
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;interrupt() 方法演示&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class TestSleep {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                log.info(&quot;wake up&quot;);
                throw new RuntimeException(e);
            }
        }, &quot;t1&quot;);

        t1.start();
        TimeUnit.SECONDS.sleep(1);
        log.info(&quot;interrupted...&quot;);
        t1.interrupt();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;21:01:29.040 [main] INFO com.thread.concurrent1.TestSleep -- interrupted...
21:01:29.043 [t1] INFO com.thread.concurrent1.TestSleep -- wake up
Exception in thread &quot;t1&quot; java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
	at com.thread.concurrent1.TestSleep.lambda$main$0(TestSleep.java:23)
	at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep0(Native Method)
	at java.base/java.lang.Thread.sleep(Thread.java:558)
	at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
	at com.thread.concurrent1.TestSleep.lambda$main$0(TestSleep.java:20)
	... 1 more
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;yield()&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;yield()方法&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用yield()会让当前线程从 Running 状态进入 Runnable 就绪状态，然后调度执行其它线程&lt;/li&gt;
&lt;li&gt;具体的实现依赖于操作系统的任务调度器 （有可能没有其他线程需要使用cpu时间片，那么系统又把执行权交给你了）&lt;/li&gt;
&lt;li&gt;和sleep()一样，不会释放任何已持有的锁（如 synchronized）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;就绪状态(yield)有机会获得时间片，阻塞状态(sleep)不能获得时间片&lt;/p&gt;
&lt;h3&gt;线程优先级&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;setPrority(int newPrority)&lt;/code&gt;：设置线程优先级&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程优先级会提示（hint）调度器优先调度该线程，但它仅仅是一个提示，调度器可以忽略它&lt;/li&gt;
&lt;li&gt;如果 cpu 比较忙，那么优先级高的线程会获得更多的时间片，但 cpu 闲时，优先级几乎没作用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class TestPrority {
    public static void main(String[] args) {
        Runnable task1 = () -&amp;gt; {
            int count = 0;
            for (; ; ) {
                System.out.println(&quot;----&amp;gt;1 &quot; + count++);
            }
        };
        Runnable task2 = () -&amp;gt; {
            int count = 0;
            for (; ; ) {
                // Thread.yield();
                System.out.println(&quot;              ----&amp;gt;2 &quot; + count++);
            }
        };
        Thread t1 = new Thread(task1, &quot;t1&quot;);
        Thread t2 = new Thread(task2, &quot;t2&quot;);
        // t1.setPriority(Thread.MIN_PRIORITY);
        // t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;----&amp;gt;1 173105
----&amp;gt;1 173106
----&amp;gt;1 173107
----&amp;gt;1 173108
----&amp;gt;1 173109
----&amp;gt;1 173110
              ----&amp;gt;2 171147
              ----&amp;gt;2 171148
              ----&amp;gt;2 171149
              ----&amp;gt;2 171150
              ----&amp;gt;2 171151
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看出输出的数字都是比较相近的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class TestPrority {
    public static void main(String[] args) {
        Runnable task1 = () -&amp;gt; {
            int count = 0;
            for (; ; ) {
                System.out.println(&quot;----&amp;gt;1 &quot; + count++);
            }
        };
        Runnable task2 = () -&amp;gt; {
            int count = 0;
            for (; ; ) {
                Thread.yield();
                System.out.println(&quot;              ----&amp;gt;2 &quot; + count++);
            }
        };
        Thread t1 = new Thread(task1, &quot;t1&quot;);
        Thread t2 = new Thread(task2, &quot;t2&quot;);
        // t1.setPriority(Thread.MIN_PRIORITY);
        // t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;----&amp;gt;1 167177
----&amp;gt;1 167178
----&amp;gt;1 167179
----&amp;gt;1 167180
----&amp;gt;1 167181
----&amp;gt;1 167182
----&amp;gt;1 167183
----&amp;gt;1 167184
----&amp;gt;1 167185
----&amp;gt;1 167186
              ----&amp;gt;2 79648
              ----&amp;gt;2 79649
              ----&amp;gt;2 79650
              ----&amp;gt;2 79651
              ----&amp;gt;2 79652
              ----&amp;gt;2 79653
              ----&amp;gt;2 79654
              ----&amp;gt;2 79655
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为有yield的存在，相差很大&lt;/p&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class TestPrority {
    public static void main(String[] args) {
        Runnable task1 = () -&amp;gt; {
            int count = 0;
            for (; ; ) {
                System.out.println(&quot;----&amp;gt;1 &quot; + count++);
            }
        };
        Runnable task2 = () -&amp;gt; {
            int count = 0;
            for (; ; ) {
//                Thread.yield();
                System.out.println(&quot;              ----&amp;gt;2 &quot; + count++);
            }
        };
        Thread t1 = new Thread(task1, &quot;t1&quot;);
        Thread t2 = new Thread(task2, &quot;t2&quot;);
         t1.setPriority(Thread.MIN_PRIORITY);
         t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;----&amp;gt;1 117769
----&amp;gt;1 117770
----&amp;gt;1 117771
----&amp;gt;1 117772
----&amp;gt;1 117773
----&amp;gt;1 117774
----&amp;gt;1 117775
----&amp;gt;1 117776
----&amp;gt;1 117777
----&amp;gt;1 117778
----&amp;gt;1 117779
----&amp;gt;1 117780
              ----&amp;gt;2 125324
              ----&amp;gt;2 125325
              ----&amp;gt;2 125326
              ----&amp;gt;2 125327
              ----&amp;gt;2 125328
              ----&amp;gt;2 125329
              ----&amp;gt;2 125330
              ----&amp;gt;2 125331
              ----&amp;gt;2 125332
              ----&amp;gt;2 125333
              ----&amp;gt;2 125334
              ----&amp;gt;2 125335
              ----&amp;gt;2 125336
              ----&amp;gt;2 125337
              ----&amp;gt;2 125338
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优先级的存在，也会相差比较大&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;p&gt;不管是 yield() 还是优先级，他们都不能真正的去控制线程的调度，最终还是由操作系统的任务调度器来决定具体哪个线程分到更多的时间片。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;应用 —— 解除对 CPU 的使用&lt;/h4&gt;
&lt;p&gt;sleep() 实现&lt;/p&gt;
&lt;p&gt;在没有利用 &lt;code&gt;CPU&lt;/code&gt; 来计算时，不要让&lt;code&gt;while(true)&lt;/code&gt;空转浪费 &lt;code&gt;CPU&lt;/code&gt;，这时可以使用&lt;code&gt;yield()&lt;/code&gt;或 &lt;code&gt;sleep()&lt;/code&gt; 方法来让出 &lt;code&gt;CPU&lt;/code&gt; 的使用权给其他的程序！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while(true) {
    try {
        Thread.sleep(50);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Linux 下可通过 top 命令来查看对 CPU 的占用率。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在单核 CPU 下，此代码对 CPU 的占用率高达 90%。其他程序几乎用不上此 CPU。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-13_21-46-57.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在单核 CPU 下，加上sleep()方法睡眠后，Java 程序对 CPU 的占用大大降低，避免空转占用 CPU。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-13_21-48-08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;可以用wait()或条件变量达到类似的效果&lt;/li&gt;
&lt;li&gt;不同的是，后两种都需要加锁，并且需要相应的唤醒操作，一般适用于要进行同步的场景&lt;/li&gt;
&lt;li&gt;sleep()适用于无需锁同步的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;join()&lt;/h3&gt;
&lt;h4&gt;为什么需要join()&lt;/h4&gt;
&lt;p&gt;下面的代码，打印出来的r值是多少？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int r = 0;
public static void main(String[] args) throws InterruptedException {
    test1();
}

private static void test1() throws InterruptedException {
    log.debug(&quot;开始&quot;);
    Thread t1 = new Thread(() -&amp;gt; {
        log.debug(&quot;开始&quot;);
        sleep(1);
        log.debug(&quot;结束&quot;);
        r = 10;
    });
    t1.start();
    log.debug(&quot;结果为:{}&quot;, r);
    log.debug(&quot;结束&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;23:07:21.660 c.Test10 [main] - 开始
23:07:21.663 c.Test10 [Thread-0] - 开始
23:07:21.663 c.Test10 [main] - 结果为:0
23:07:21.664 c.Test10 [main] - 结束
23:07:22.670 c.Test10 [Thread-0] - 结束
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为主线程和线程 t1 是并行执行的，t1 线程需要 1 秒之后才能算出 r=10&lt;/li&gt;
&lt;li&gt;而主线程一开始就要打印 r 的结果，所以就会打印出 r=0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用join()，加在 t1.start() 之后即可&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test10&quot;)
public class Test10 {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        log.debug(&quot;开始&quot;);
        Thread t1 = new Thread(() -&amp;gt; {
            log.debug(&quot;开始&quot;);
            sleep(1);
            log.debug(&quot;结束&quot;);
            r = 10;
        });
        t1.start();
        
        // 哪个线程调用就等待哪个线程结束 (t1线程调用就等待t1线程结束)
        // 主线程等待 t1 线程结束
        t1.join();

        
        log.debug(&quot;结果为:{}&quot;, r);
        log.debug(&quot;结束&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;同步应用&lt;/h4&gt;
&lt;p&gt;以调用方角度来讲，如果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要等待结果返回，才能继续运行就是同步&lt;/li&gt;
&lt;li&gt;不需要等待结果返回，就能继续运行就是异步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-13_22-17-27.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;等待多个线程结果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;问：下面代码 cost 大约多少秒？
答：2s 左右&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test2();
}
private static void test2() throws InterruptedException {
    Thread t1 = new Thread(() -&amp;gt; {
        sleep(1);
        r1 = 10;
    });
    Thread t2 = new Thread(() -&amp;gt; {
        sleep(2);
        r2 = 20;
    });
    long start = System.currentTimeMillis();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis();
    log.debug(&quot;r1: {} r2: {} cost: {}&quot;, r1, r2, end - start);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;23:35:51.088 c.TestJoin [main] - r1: 10 r2: 20 cost: 2006
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个 join：等待 t1 时, t2 并没有停止, 而在运行&lt;/li&gt;
&lt;li&gt;第二个 join：1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果颠倒两个 join 呢？&lt;/p&gt;
&lt;p&gt;最终都是输出：&lt;code&gt;20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005&lt;/code&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-13_22-19-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;有时效的join()&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;如果等够时间，会提前结束join()的等待。&lt;/li&gt;
&lt;li&gt;如果没等够时间，则主线程继续往下执行，无影响。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;等够时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test1 {
    static int r1 = 0;
    static int r2 = 0;
    public static void main(String[] args) throws InterruptedException {
        test3();
    }

    public static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            try {
                sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            r1 = 10;
        });
        long start = System.currentTimeMillis();
        t1.start();
        // 线程执行结束会导致 join 结束
        t1.join(3000);
        long end = System.currentTimeMillis();
        log.info(&quot;r1: {} r2: {} cost: {}&quot;, r1, r2, end - start);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;22:32:19.077 [main] INFO com.thread.concurrent1.Test1 -- r1: 10 r2: 0 cost: 2013
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没等够时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test1 {
    static int r1 = 0;
    static int r2 = 0;
    public static void main(String[] args) throws InterruptedException {
        test3();
    }

    public static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            try {
                sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            r1 = 10;
        });
        long start = System.currentTimeMillis();
        t1.start();
        // 线程执行结束会导致 join 结束
        t1.join(1500);
        long end = System.currentTimeMillis();
        log.info(&quot;r1: {} r2: {} cost: {}&quot;, r1, r2, end - start);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;22:31:05.884 [main] INFO com.thread.concurrent1.Test1 -- r1: 0 r2: 0 cost: 1505
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;等朋友,五分钟你不下来,我就走了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;interrupt&lt;/h3&gt;
&lt;h4&gt;interrupt打断阻塞&lt;/h4&gt;
&lt;p&gt;打断等待状态/阻塞状态的线程, 会抛出异常信息表示被打断，此时会清空打断状态，打断状态为false&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, &quot;t1&quot;);
        t1.start();

        Thread.sleep(1000);

        t1.interrupt();
        log.info(&quot;打断标记:{}&quot;, t1.isInterrupted());  // isInterrupted()  判断线程是否被打断 | true：被打断  false：未被打断
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Exception in thread &quot;t1&quot; java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
	at com.thread.concurrent1.Test2.lambda$main$0(Test2.java:20)
	at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep0(Native Method)
	at java.base/java.lang.Thread.sleep(Thread.java:509)
	at com.thread.concurrent1.Test2.lambda$main$0(Test2.java:18)
	... 1 more
22:42:56.962 [main] INFO com.thread.concurrent1.Test2 -- 打断标记:false
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;打断运行状态的线程&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            while (true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if (interrupted) {
                    log.info(&quot;被打断了&quot;);
                    break;
                }
            }
        }, &quot;t1&quot;);
        t1.start();
        Thread.sleep(1000);
        log.info(&quot;interrupt...&quot;);
        t1.interrupt();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;21:49:42.708 [main] INFO com.thread.concurrent1.Test3 -- interrupt...
21:49:42.711 [t1] INFO com.thread.concurrent1.Test3 -- 被打断了
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;设计模式 - 两阶段终止&lt;/h4&gt;
&lt;p&gt;两阶段终止（Two Phase Termination）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有两个线程t1、t2。如何在线程t1中优雅的终止t2？&lt;/p&gt;
&lt;p&gt;【优雅】指的是给t2料理后事的机会&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;错误思路：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用线程对象的&lt;code&gt;stop()&lt;/code&gt;方法停止线程&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stop()&lt;/code&gt;方法会真正杀死线程，如果这时线程锁住了共享资源，那么当它被杀死后就没有机会释放锁，其它线程将永远无法获取这个锁&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;使用&lt;code&gt;System.exit(int)&lt;/code&gt;方法停止线程&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;目的仅是停止一个线程，但这种方法会让整个程序都停止&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;正确做法：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用两阶段终止模式来优雅的结束线程
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-14_21-56-11.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;interrupt() 实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
        twoPhaseTermination.start();
        Thread.sleep(3500);
        twoPhaseTermination.stop();
    }
}

@Slf4j
class TwoPhaseTermination {
    private Thread monitor;

    public void start() {
        monitor = new Thread(() -&amp;gt; {
            while (true) {
                Thread currentThread = Thread.currentThread();
                if (currentThread.isInterrupted()) {
                    log.info(&quot;料理后事&quot;);
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.info(&quot;监控中。。。&quot;);
                } catch (InterruptedException e) {
                    // 重置打断标记 因为 sleep() 方法被打断后，会抛出异常信息并清除打断标记，此时打断标记是 false。需要重新打断一次
                    currentThread.interrupt();
                    throw new RuntimeException(e);
                }
            }
        });
        monitor.start();
    }

    public void stop() {
        monitor.interrupt();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;打断park&lt;/h4&gt;
&lt;p&gt;park() 方法：阻塞线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;park()不是 Thread 类中提供的方法，是LockSupport工具类中提供的方法。&lt;/li&gt;
&lt;li&gt;作用也是让当前线程停下来，进入阻塞状态&lt;/li&gt;
&lt;li&gt;可以通过interrupt()方法来打断正在park的线程，打断状态变为true&lt;/li&gt;
&lt;li&gt;park()是不可重入的。一旦打断状态变为true后，再次调用interrupt()方法会失效。一个线程不可多次调用park()&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;LockSupport.park()&lt;/code&gt;：让此方法所在的线程阻塞。可以通过&lt;code&gt;interrupt()&lt;/code&gt;方法来打断正在阻塞的线程，也可通过&lt;code&gt;unpark()&lt;/code&gt;方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LockSupport.unpark(线程名)&lt;/code&gt;：让指定的线程执行，取消阻塞&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test5 {
    public static void main(String[] args) throws InterruptedException {
        test3();
    }

    private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            log.info(&quot;park...&quot;);
            LockSupport.park();
            log.info(&quot;unpark...&quot;);
            log.info(&quot;打断状态：{}&quot;, Thread.currentThread().isInterrupted());

            LockSupport.park();
            log.info(&quot;unpark...&quot;);
        }, &quot;t1&quot;);
        t1.start();

        // 主线程睡眠 1s 后，打断正在 park 的线程
        sleep(1);
        t1.interrupt();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;22:27:34.498 [t1] INFO com.thread.concurrent1.Test5 -- park...
22:27:34.503 [t1] INFO com.thread.concurrent1.Test5 -- unpark...
22:27:34.504 [t1] INFO com.thread.concurrent1.Test5 -- 打断状态：true
22:27:34.507 [t1] INFO com.thread.concurrent1.Test5 -- unpark...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;主线程与守护线程&lt;/h3&gt;
&lt;p&gt;默认情况下，Java 进程需要等待所有线程都运行结束，才会结束&lt;/p&gt;
&lt;p&gt;有一种特殊的线程叫做&lt;strong&gt;守护线程&lt;/strong&gt;，&lt;strong&gt;只要其它非守护线程运行结束了，即使守护线程的代码没有执行完，也会强制结束&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;setDaemon(Boolean)&lt;/code&gt;：设置为守护线程。默认为 false，非守护线程&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;演示守护线程强制结束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class Test6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    log.info(&quot;t1停止&quot;);
                    break;
                }
            }
        }, &quot;t1&quot;);
        t1.setDaemon(true);
        t1.start();

        Thread.sleep(2000);

        log.info(&quot;主线程结束&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;22:43:42.720 [main] INFO com.thread.concurrent1.Test6 -- 主线程结束
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;垃圾回收器线程就是一种就常见的守护线程&lt;/li&gt;
&lt;li&gt;Tomcat 中的 Acceptor（接收请求）和 Poller（分发请求）线程都是守护线程，所以 &lt;code&gt;Tomcat&lt;/code&gt; 接收到 &lt;code&gt;shutdown&lt;/code&gt; 命令后，不会等待它们处理完当前请求，会让它们强制结束&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线程状态&lt;/h3&gt;
&lt;p&gt;线程状态从不同的维度来描述有不同的状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 操作系统 层面来描述，有五种线程状态。&lt;/li&gt;
&lt;li&gt;从 JAVA API 层面来描述，有六钟线程状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;五种状态 —— 操作系统层面&lt;/h4&gt;
&lt;p&gt;这是从 &lt;code&gt;操作系统&lt;/code&gt; 层面来描述的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page28_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;【初始状态】仅是在语言层面创建了线程对象，还未与操作系统线程关联&lt;/li&gt;
&lt;li&gt;【可运行状态】（也称为：就绪状态）指该线程已经被创建（与操作系统线程关联），可以由 CPU 调度执行&lt;/li&gt;
&lt;li&gt;【运行状态】指获取了 CPU 时间片运行中的状态
&lt;ul&gt;
&lt;li&gt;当 CPU 时间片用完，会从【运行状态】转换至【可运行状态】，会导致线程的上下文切换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;【阻塞状态】
&lt;ul&gt;
&lt;li&gt;如果调用了阻塞 API，如 BIO 读写文件，这时该线程实际不会用到 CPU，会导致线程上下文切换，进入【阻塞状态】&lt;/li&gt;
&lt;li&gt;等 BIO 操作完毕，会由操作系统唤醒阻塞的线程，转换至【可运行状态】&lt;/li&gt;
&lt;li&gt;与【可运行状态】的区别是，对【阻塞状态】的线程来说只要它们一直不唤醒，调度器就一直不会考虑调度它们。调度器只会调度【可运行状态】的线程，给他们分配时间片&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;【终止状态】表示线程已经执行完毕，生命周期已经结束，不会再转换为其它状态&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;六种状态 —— JAVA API 层面&lt;/h4&gt;
&lt;p&gt;这是从 Java API 层面来描述的&lt;/p&gt;
&lt;p&gt;根据&lt;code&gt;Thread.State&lt;/code&gt;枚举，分为六种状态
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B_page29_image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NEW&lt;/code&gt;：线程刚被创建，但是还没有调用&lt;code&gt;start()&lt;/code&gt;方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RUNNABLE&lt;/code&gt;： 当调用了&lt;code&gt;start()&lt;/code&gt;方法之后，注意，Java API 层面的RUNNABLE状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】（由于 BIO 导致的线程阻塞，在 Java 里无法区分，仍然认为是可运行）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BLOCKED&lt;/code&gt; ，&lt;code&gt;WAITING&lt;/code&gt;，&lt;code&gt;TIMED_WAITING&lt;/code&gt;：都是 Java API 层面对【阻塞状态】的细分，后面会在状态转换一节详述&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TERMINATED&lt;/code&gt;：当线程代码运行结束，表示线程已经执行完毕&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
Java中的RUNNABLE，即有可能分到了时间片，也可能没有分到时间片，也有可能陷入了操作系统的io阻塞，这三种状态在Java中都叫&lt;code&gt;RUNNABLE&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;比如：在读取文件时，在操作系统层面就变成了 阻塞状态。但是在 Java 层面还是 Runnable 可运行状态
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.TestState&quot;)
public class TestState {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -&amp;gt; {
            log.debug(&quot;running...&quot;);
        }, &quot;t1&quot;);


        Thread t2 = new Thread(() -&amp;gt; {
            while (true) {

            }
        }, &quot;t2&quot;);
        t2.start();


        Thread t3 = new Thread(() -&amp;gt; {
            log.debug(&quot;running...&quot;);
        }, &quot;t3&quot;);
        t3.start();


        Thread t4 = new Thread(() -&amp;gt; {
            synchronized (TestState.class) {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, &quot;t4&quot;);
        t4.start();


        Thread t5 = new Thread(() -&amp;gt; {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, &quot;t5&quot;);
        t5.start();


        Thread t6 = new Thread(() -&amp;gt; {
            synchronized (TestState.class) {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, &quot;t6&quot;);
        t6.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug(&quot;t1 state {}&quot;, t1.getState());  // NEW 新建状态
        log.debug(&quot;t2 state {}&quot;, t2.getState());  // RUNNABLE 可运行状态
        log.debug(&quot;t3 state {}&quot;, t3.getState());  // TERMINATED 终止状态
        log.debug(&quot;t4 state {}&quot;, t4.getState());  // TIME_WAITING 超时等待状态
        log.debug(&quot;t5 state {}&quot;, t5.getState());  // WAITING 等待状态
        log.debug(&quot;t6 state {}&quot;, t6.getState());  // BLOCKED 阻塞状态。需要等待锁的释放
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;23:12:49.760 [t3] INFO com.thread.concurrent1.Test7 -- running...
23:12:50.261 [main] INFO com.thread.concurrent1.Test7 -- t1 state NEW
23:12:50.263 [main] INFO com.thread.concurrent1.Test7 -- t2 state RUNNABLE
23:12:50.263 [main] INFO com.thread.concurrent1.Test7 -- t3 state TERMINATED
23:12:50.263 [main] INFO com.thread.concurrent1.Test7 -- t4 state TIMED_WAITING
23:12:50.263 [main] INFO com.thread.concurrent1.Test7 -- t5 state WAITING
23:12:50.263 [main] INFO com.thread.concurrent1.Test7 -- t6 state BLOCKED
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;相关面试题🤏🏻&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;说说线程的生命状态和状态？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NEW: 初始状态，线程被创建出来但没有被调用start() 。&lt;/li&gt;
&lt;li&gt;RUNNABLE: 运行状态，线程被调用了start()等待运行的状态。&lt;/li&gt;
&lt;li&gt;BLOCKED：阻塞状态，需要等待锁释放。&lt;/li&gt;
&lt;li&gt;WAITING：等待状态，表示该线程需要等待其他线程做出一些特定动作（通知或中断）。比如join()方法。&lt;/li&gt;
&lt;li&gt;TIME_WAITING：超时等待状态，可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。比如sleep&lt;/li&gt;
&lt;li&gt;TERMINATED：终止状态，表示该线程已经运行完毕。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;应用 - 统筹（烧水泡茶）&lt;/h3&gt;
&lt;p&gt;华罗庚《统筹方法》&lt;/p&gt;
&lt;p&gt;统筹方法，是一种安排工作进程的数学方法。它的实用范围极广泛，在企业管理和基本建设中，以及关系复杂的科研项目的组织与管理中，都可以应用。&lt;/p&gt;
&lt;p&gt;怎样应用呢？主要是把工序安排好。
比如，想泡壶茶喝。当时的情况是：开水没有；水壶要洗，茶壶、茶杯要洗；火已生了，茶叶也有了。怎么办？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;办法甲：洗好水壶，灌上凉水，放在火上；在等待水开的时间里，洗茶壶、洗茶杯、拿茶叶；等水开
了，泡茶喝。&lt;/li&gt;
&lt;li&gt;办法乙：先做好一些准备工作，洗水壶，洗茶壶茶杯，拿茶叶；一切就绪，灌水烧水；坐待水开了，泡茶喝。&lt;/li&gt;
&lt;li&gt;办法丙：洗净水壶，灌上凉水，放在火上，坐待水开；水开了之后，急急忙忙找茶叶，洗茶壶茶杯，泡茶喝。
哪一种办法省时间？我们能一眼看出，第一种办法好，后两种办法都窝了工。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是小事，但这是引子，可以引出生产管理等方面有用的方法来。&lt;/p&gt;
&lt;p&gt;水壶不洗，不能烧开水，因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯，就不能泡茶，因而这些又是泡茶的前提。它们的相互关系，可以用下边的箭头图来表示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-15_20-56-52.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从这个图上可以一眼看出，办法甲总共要16分钟（而办法乙、丙需要20分钟）。如果要缩短工时、提高工作效率，应当主要抓烧开水这个环节，而不是抓拿茶叶等环节。同时，洗茶壶茶杯、拿茶叶总共不过4分钟，大可利用“等水开”的时间来做。&lt;/p&gt;
&lt;p&gt;是的，这好像是废话，卑之无甚高论。有如走路要用两条腿走，吃饭要一口一口吃，这些道理谁都懂得。但稍有变化，临事而迷的情况，常常是存在的。在近代工业的错综复杂的工艺过程中，往往就不是像泡茶喝这么简单了。任务多了，几百几千，甚至有好几万个任务。关系多了，错综复杂，千头万绪，往往出现“万事俱备，只欠东风”的情况。由于一两个零件没完成，耽误了一台复杂机器的出厂时间。或往往因为抓的不是关键，连夜三班，急急忙忙，完成这一环节之后，还得等待旁的环节才能装配。&lt;/p&gt;
&lt;p&gt;洗茶壶，洗茶杯，拿茶叶，或先或后，关系不大，而且同是一个人的活儿，因而可以合并成为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-15_20-58-18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看来这是“小题大做”，但在工作环节太多的时候，这样做就非常必要了。&lt;/p&gt;
&lt;p&gt;这里讲的主要是时间方面的事，但在具体生产实践中，还有其他方面的许多事。这种方法虽然不一定能直接解决所有问题，但是，我们利用这种方法来考虑问题，也是不无裨益的。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;题目：
阅读华罗庚《统筹方法》，给出烧水泡茶的多线程解决方案。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参考上图二，用两个线程（两个人协作）模拟烧水泡茶过程
&lt;ul&gt;
&lt;li&gt;文中办法乙、丙都相当于任务串行&lt;/li&gt;
&lt;li&gt;而上图一相当于启动了 4 个线程，有点浪费&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;用 sleep(n) 模拟洗茶壶、洗水壶等耗费的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解法一、join()&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.Test16&quot;)
public class Test16 {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -&amp;gt; {
            // 洗水壶 1s
            log.debug(&quot;洗水壶&quot;);
            Sleeper.sleep(1);

            // 烧开水 5s
            log.debug(&quot;烧开水&quot;);
            Sleeper.sleep(5);
        }, &quot;t1&quot;);

        t1.start();

        Thread t2 = new Thread(() -&amp;gt; {
            // 洗茶壶 1s
            log.debug(&quot;洗茶壶&quot;);
            Sleeper.sleep(1);

            // 洗茶杯 2s
            log.debug(&quot;洗茶杯&quot;);
            Sleeper.sleep(2);

            // 拿茶叶 1s
            log.debug(&quot;拿茶叶&quot;);
            Sleeper.sleep(1);

            // 等待 t1 运行结束
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug(&quot;泡茶&quot;);
        }, &quot;t2&quot;);

        t2.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;22:42:41.457 c.Test16 [t2] - 洗茶壶
22:42:41.457 c.Test16 [t1] - 洗水壶
22:42:42.468 c.Test16 [t2] - 洗茶杯
22:42:42.468 c.Test16 [t1] - 烧开水
22:42:44.473 c.Test16 [t2] - 拿茶叶
22:42:47.483 c.Test16 [t2] - 泡茶
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;缺陷：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上面模拟的是 t2 等 t1 的水烧开了，小王泡茶，如果反过来要实现 t1 等 t2 的茶叶拿来了，老王泡茶呢？代码最好能适应两种情况&lt;/li&gt;
&lt;li&gt;上面的两个线程其实是各执行各的，如果要模拟 t1 把水壶交给 t2 泡茶，或模拟 t2 把茶叶交给 t1 泡茶呢?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;小结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;本章的重点在于掌握&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程创建&lt;/li&gt;
&lt;li&gt;线程重要 api，如 start，run，sleep，join，interrupt 等&lt;/li&gt;
&lt;li&gt;线程状态 -&amp;gt; 操作系统：5 种 Java API：6 种&lt;/li&gt;
&lt;li&gt;应用方面
&lt;ul&gt;
&lt;li&gt;异步调用：主线程执行期间，其它线程异步执行耗时操作&lt;/li&gt;
&lt;li&gt;提高效率：并行计算，缩短运算时间&lt;/li&gt;
&lt;li&gt;同步等待：join&lt;/li&gt;
&lt;li&gt;统筹规划：合理使用线程，得到最优效果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;原理方面
&lt;ul&gt;
&lt;li&gt;线程运行流程：栈、栈帧、上下文切换、程序计数器&lt;/li&gt;
&lt;li&gt;Thread 两种创建方式的源码&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;模式方面
&lt;ul&gt;
&lt;li&gt;终止模式之两阶段终止&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>MySQL - 多版本并发控制</title><link>https://zzyang.top/posts/mysql-mvcc/</link><guid isPermaLink="true">https://zzyang.top/posts/mysql-mvcc/</guid><pubDate>Thu, 07 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是MVCC&lt;/h2&gt;
&lt;p&gt;MVCC （&lt;code&gt;Multiversion Concurrency Control&lt;/code&gt;），多版本并发控制。顾名思义，MVCC 是通过数据行的多个版本管理来实现数据库的 &lt;code&gt;并发控制&lt;/code&gt; 。这项技术使得在InnoDB的事务隔离级别下执行 &lt;code&gt;一致性读&lt;/code&gt; 操作有了保证。换言之，就是为了查询一些正在被另一个事务更新的行，并且可以看到它们被更新之前的值，这样 在做查询的时候就不用等待另一个事务释放锁。&lt;/p&gt;
&lt;p&gt;MVCC没有正式的标准，在不同的DBMS中MVCC的实现方式可能是不同的，也不是普遍使用的（大家可以参考相关的DBMS文档）。这里讲解InnoDB中MVCC的实现机制（MySQL其他的存储引擎并不支持它）。&lt;/p&gt;
&lt;h2&gt;快照读与当前读&lt;/h2&gt;
&lt;h3&gt;快照读&lt;/h3&gt;
&lt;p&gt;快照读又叫一致性读，读取的是快照数据。&lt;strong&gt;不加锁的简单的 SELECT 都属于快照读&lt;/strong&gt;，即不加锁的非阻塞 读；比如这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM player WHERE ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之所以出现快照读的情况，是基于提高并发性能的考虑，快照读的实现是基于MVCC，它在很多情况下， 避免了加锁操作，降低了开销。&lt;/p&gt;
&lt;p&gt;既然是基于多版本，那么快照读可能读到的并不一定是数据的最新版本，而有可能是之前的历史版本。&lt;/p&gt;
&lt;p&gt;快照读的前提是隔离级别不是串行级别，串行级别下的快照读会退化成当前读。&lt;/p&gt;
&lt;h3&gt;当前读&lt;/h3&gt;
&lt;p&gt;当前读读取的是记录的最新版本（最新数据，而不是历史版本的数据），读取时还要保证其他并发事务 不能修改当前记录，会对读取的记录进行加锁。加锁的 SELECT，或者对数据进行增删改都会进行&lt;em&gt;当前读&lt;/em&gt;。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;复习&lt;/h3&gt;
&lt;p&gt;再谈隔离级别&lt;/p&gt;
&lt;p&gt;我们知道事务有 4 个隔离级别，可能存在三种并发问题：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_21-45-21.png&quot; alt=&quot;&quot; /&gt;
在 MySQL 中，默认的隔离级别是可重复读，可以解决脏读和不可重复读的问题，如果仅从定义的角度来看，它并不能解决幻读问题。如果我们想要解决幻读问题，就需要采用串行化的方式，也就是将隔离级别提升到最高，但这样一来就会大幅降低数据库的事务并发能力。&lt;/p&gt;
&lt;p&gt;MVCC 可以不采用锁机制，而是通过乐观锁的方式来解决不可重复读和幻读问题！它可以在大多数情况下替代行级锁，降低系统的开销。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_21-44-44.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;隐藏字段、Undo Log版本链&lt;/h4&gt;
&lt;p&gt;回顾一下undo日志的版本链，对于使用 InnoDB 存储引擎的表来说，它的聚簇索引记录中都包含两个必要的隐藏列。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trx_id&lt;/code&gt; ：每次一个事务对某条聚簇索引记录进行改动时，都会把该事务的 &lt;code&gt;事务id&lt;/code&gt; 赋值给 &lt;code&gt;trx_id&lt;/code&gt; 隐藏列。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;roll_pointer&lt;/code&gt; ：每次对某条聚簇索引记录进行改动时，都会把旧的版本写入到 &lt;code&gt;undo日志&lt;/code&gt; 中，然 后这个隐藏列就相当于一个指针，可以通过它来找到该记录修改前的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：student表数据如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SELECT * FROM student;
+--------+--------+--------+
| id     | name   | class  |
+--------+--------+--------+
| 1      | 张三   | 一班   |
+--------+--------+--------+
1 row in set (0.07 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设插入该记录的&lt;code&gt;事务id&lt;/code&gt;为&lt;code&gt;8&lt;/code&gt;，那么此刻该条记录的示意图如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_21-56-02.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;insert undo只在事务回滚时起作用，当事务提交后，该类型的undo日志就没用了，它占用的Undo Log Segment也会被系统回收（也就是该undo日志占用的Undo页面链表要么被重用，要么被释放）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设之后两个事务id分别为 10 、 20 的事务对这条记录进行 UPDATE 操作，操作流程如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;发生时间顺序&lt;/th&gt;
&lt;th&gt;事务10&lt;/th&gt;
&lt;th&gt;事务20&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;BEGIN;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;BEGIN;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;UPDATE student SET name=&quot;李四&quot; WHERE id=1;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;UPDATE student SET name=&quot;王五&quot; WHERE id=1;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;COMMIT;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;UPDATE student SET name=&quot;钱七&quot; WHERE id=1;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;UPDATE student SET name=&quot;宋八&quot; WHERE id=1;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;COMMIT;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;能不能在两个事务中交叉更新同一条记录呢？不能！这不就是一个事务修改了另一个未提交事务修改过的数据，脏写。&lt;/p&gt;
&lt;p&gt;InnoDB使用锁来保证不会有脏写情况的发生，也就是在第一个事务更新了某条记录后，就会给这条记录加锁，另一个事务再次更新时就需要等待第一个事务提交了，把锁释放之后才可以继续更新。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;每次对记录进行改动，都会记录一条undo日志，每条undo日志也都有一个 &lt;code&gt;roll_pointer&lt;/code&gt; 属性 （ &lt;code&gt;INSERT&lt;/code&gt; 操作对应的undo日志没有该属性，因为该记录并没有更早的版本），可以将这些 &lt;code&gt;undo日志&lt;/code&gt; 都连起来，串成一个链表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_21-57-40.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对该记录每次更新后，都会将旧值放到一条 &lt;code&gt;undo日志&lt;/code&gt; 中，就算是该记录的一个旧版本，随着更新次数 的增多，所有的版本都会被 &lt;code&gt;roll_pointer&lt;/code&gt; 属性连接成一个链表，我们把这个链表称之为 &lt;code&gt;版本链&lt;/code&gt; ，版本链的头节点就是当前记录最新的值。&lt;/p&gt;
&lt;p&gt;每个版本中还包含生成该版本时对应的&lt;code&gt;事务id&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;MVCC实现原理之ReadView&lt;/h3&gt;
&lt;p&gt;MVCC 的实现依赖于：&lt;code&gt;隐藏字段&lt;/code&gt;、&lt;code&gt;Undo Log&lt;/code&gt;、&lt;code&gt;Read View&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;什么是ReadView&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 MVCC 机制中，多个事务对同一个行记录进行更新会产生多个历史快照，这些历史快照保存在 Undo Log 里。如果一个事务想要查询这个行记录，需要读取哪个版本的行记录呢？这时就需要用到 ReadView 了，它帮我们解决了行的可见性问题。&lt;/p&gt;
&lt;p&gt;ReadView 就是某个事务在使用 MVCC 机制进行快照读操作时产生的读视图。当事务启动时，会生成数据库系统当前的一个快照，InnoDB 为每个事务构造了一个数组，用来记录并维护系统当前&lt;code&gt;活跃事务&lt;/code&gt;的 ID（“活跃”指的就是，启动了但还没提交）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设计思路&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;READ UNCOMMITTED&lt;/code&gt; 隔离级别的事务，由于可以读到未提交事务修改过的记录，所以直接读取记录的最新版本就好了。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;SERIALIZABLE&lt;/code&gt; 隔离级别的事务，InnoDB规定使用加锁的方式来访问记录。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;READ COMMITTED&lt;/code&gt; 和 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 隔离级别的事务，都必须保证读到 &lt;code&gt;已经提交了的&lt;/code&gt; 事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交，是不能直接读取最新版本的记录的，核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的，这是ReadView要解决的主要问题。&lt;/p&gt;
&lt;p&gt;这个ReadView中主要包含4个比较重要的内容，分别如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;creator_trx_id&lt;/code&gt; ，创建这个 Read View 的事务 ID。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：只有在对表中的记录做改动时（执行INSERT、DELETE、UPDATE这些语句时）才会为 事务分配事务id，否则在一个只读事务中的事务id值都默认为0。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;trx_ids&lt;/code&gt; ，表示在生成ReadView时当前系统中活跃的读写事务的 &lt;code&gt;事务id列表&lt;/code&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;up_limit_id&lt;/code&gt; ，活跃的事务中最小的事务 ID。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;low_limit_id&lt;/code&gt; ，表示生成ReadView时系统中应该分配给下一个事务的 id 值。low_limit_id 是系 统最大的事务id值，这里要注意是系统中的事务id，需要区别于正在活跃的事务ID。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：low_limit_id并不是trx_ids中的最大值，事务id是递增分配的。比如，现在有id为1， 2，3这三个事务，之后id为3的事务提交了。那么一个新的读事务在生成ReadView时， trx_ids就包括1和2，up_limit_id的值就是1，low_limit_id的值就是4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;举例：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;trx_ids 为 trx2、trx3、trx5 和 trx8 的集合，系统的最大事务 ID（low_limit_id）为 trx8+1（如果之前没有其他的新增事务），活跃的最小事务 ID（up_limit_id）为 trx2。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_22-28-51.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;ReadView的规则&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;有了这个ReadView，这样在访问某条记录时，只需要按照下边的步骤判断记录的某个版本是否可见。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果被访问版本的&lt;code&gt;trx_id&lt;/code&gt;属性值与ReadView中的 &lt;code&gt;creator_trx_id&lt;/code&gt; 值相同，意味着当前事务在访问它自己修改过的记录，所以该版本可以被当前事务访问。&lt;/li&gt;
&lt;li&gt;如果被访问版本的&lt;code&gt;trx_id&lt;/code&gt;属性值小于ReadView中的 &lt;code&gt;up_limit_id&lt;/code&gt; 值，表明生成该版本的事务在当前事务生成ReadView前已经提交，所以该版本可以被当前事务访问。&lt;/li&gt;
&lt;li&gt;如果被访问版本的&lt;code&gt;trx_id&lt;/code&gt;属性值大于或等于ReadView中的 &lt;code&gt;low_limit_id&lt;/code&gt; 值，表明生成该版本的事务在当前事务生成ReadView后才开启，所以该版本不可以被当前事务访问。&lt;/li&gt;
&lt;li&gt;如果被访问版本的&lt;code&gt;trx_id&lt;/code&gt;属性值在ReadView的 &lt;code&gt;up_limit_id&lt;/code&gt; 和 &lt;code&gt;low_limit_id&lt;/code&gt; 之间，那就需要判断一下trx_id属性值是不是在 trx_ids 列表中。
&lt;ul&gt;
&lt;li&gt;如果在，说明创建ReadView时生成该版本的事务还是活跃的，该版本不可以被访问。&lt;/li&gt;
&lt;li&gt;如果不在，说明创建ReadView时生成该版本的事务已经被提交，该版本可以被访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;MVCC整体操作流程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;了解了这些概念之后，我们来看下当查询一条记录的时候，系统如何通过MVCC找到它：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先获取事务自己的版本号，也就是&lt;code&gt;事务ID&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;获取 ReadView；&lt;/li&gt;
&lt;li&gt;查询得到的数据，然后与 &lt;code&gt;ReadView&lt;/code&gt; 中的事务版本号进行比较；&lt;/li&gt;
&lt;li&gt;如果不符合 &lt;code&gt;ReadView&lt;/code&gt; 规则，就需要从 &lt;code&gt;Undo Log&lt;/code&gt; 中获取历史快照；&lt;/li&gt;
&lt;li&gt;最后返回符合规则的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果某个版本的数据对当前事务不可见的话，那就顺着版本链找到下一个版本的数据，继续按照上边的步骤判断可见性，依此类推，直到版本链中的最后一个版本。如果最后一个版本也不可见的话，那么就意味着该条记录对该事务完全不可见，查询结果就不包含该记录。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;InnoDB 中，MVCC 是通过 Undo Log + Read View 进行数据读取，Undo Log 保存了历史快照，而 Read View 规则帮我们判断当前版本的数据是否可见。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在隔离级别为读已提交（Read Committed）时，一个事务中的每一次 SELECT 查询都会重新获取一次 Read View。&lt;/p&gt;
&lt;p&gt;如表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;事务&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;begin;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;select * from student where id &amp;gt;2;&lt;/td&gt;
&lt;td&gt;获取一次Read View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.........&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;select * from student where id &amp;gt;2;&lt;/td&gt;
&lt;td&gt;获取一次Read View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;commit;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，此时同样的查询语句都会重新获取一次 Read View，这时如果 Read View 不同，就可能产生不可重复读或者幻读的情况。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当隔离级别为可重复读的时候，就避免了不可重复读，这是因为一个事务只在第一次 SELECT 的时候会获取一次 Read View，而后面所有的 SELECT 都会复用这个 Read View，如下表所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_22-31-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;举例说明&lt;/h3&gt;
&lt;p&gt;假设现在student表中只有一条由&lt;code&gt;事务id&lt;/code&gt;为&lt;code&gt;8&lt;/code&gt;的事务插入的一条记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SELECT * FROM student;
+--------+--------+---------+
| id     | name   | class   |
+--------+--------+---------+
| 1      | 张三   | 一班    |
+--------+--------+---------+
1 row in set (0.07 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MVCC 只能在 &lt;code&gt;READ COMMITTED&lt;/code&gt; 和 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 两个隔离级别下工作。接下来看一下 &lt;code&gt;READ COMMITTED&lt;/code&gt; 和 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 所谓的生成 &lt;code&gt;ReadView&lt;/code&gt; 的时机不同到底不同在哪里。&lt;/p&gt;
&lt;h4&gt;READ COMMITTED隔离级别下&lt;/h4&gt;
&lt;p&gt;READ COMMITTED ：&lt;strong&gt;每次读取数据前都生成一个ReadView&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;现在有两个 &lt;code&gt;事务id&lt;/code&gt; 分别为 &lt;code&gt;10&lt;/code&gt; 、 &lt;code&gt;20&lt;/code&gt; 的事务在执行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Transaction 10
BEGIN;
UPDATE student SET name=&quot;李四&quot; WHERE id=1;
UPDATE student SET name=&quot;王五&quot; WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：事务执行过程中，只有在第一次真正修改记录时（比如使用INSERT、DELETE、UPDATE语句），才会被分配一个单独的事务id，这个事务id是递增的。所以我们才在事务2中更新一些别的表的记录，目的是让它分配事务id。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此刻，表student 中 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt; 的记录得到的版本链表如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_22-50-36.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;假设现在有一个使用 &lt;code&gt;READ COMMITTED&lt;/code&gt; 隔离级别的事务开始执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1：Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为&apos;张三&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;SELECT1&lt;/code&gt; 的执行过程如下：&lt;/p&gt;
&lt;p&gt;步骤1：在执行 &lt;code&gt;SELECT&lt;/code&gt; 语句时会先生成一个 &lt;code&gt;ReadView&lt;/code&gt;，&lt;code&gt;ReadView&lt;/code&gt; 的 &lt;code&gt;trx_ids&lt;/code&gt; 列表的内容就是 &lt;code&gt;[10, 20]&lt;/code&gt;，&lt;code&gt;up_limit_id&lt;/code&gt; 为 &lt;code&gt;10&lt;/code&gt;，&lt;code&gt;low_limit_id&lt;/code&gt; 为 &lt;code&gt;21&lt;/code&gt;，&lt;code&gt;creator_trx_id&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;步骤2：从版本链中挑选可见的记录，从图中看出，最新版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;王五&apos;&lt;/code&gt;，该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;10&lt;/code&gt;，在 &lt;code&gt;trx_ids&lt;/code&gt; 列表内，所以不符合可见性要求，根据 &lt;code&gt;roll_pointer&lt;/code&gt; 跳到下一个版本。&lt;/p&gt;
&lt;p&gt;步骤3：下一个版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;李四&apos;&lt;/code&gt;，该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值也为 &lt;code&gt;10&lt;/code&gt;，也在 &lt;code&gt;trx_ids&lt;/code&gt; 列表内，所以也不符合要求，继续跳到下一个版本。&lt;/p&gt;
&lt;p&gt;步骤4：下一个版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;张三&apos;&lt;/code&gt;，该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;8&lt;/code&gt;，小于 &lt;code&gt;ReadView&lt;/code&gt; 中的 &lt;code&gt;up_limit_id&lt;/code&gt; 值 &lt;code&gt;10&lt;/code&gt;，所以这个版本是符合要求的，最后返回给用户的版本就是这条列 &lt;code&gt;name&lt;/code&gt; 为 &lt;code&gt;&apos;张三&apos;&lt;/code&gt; 的记录。&lt;/p&gt;
&lt;p&gt;之后，我们把 &lt;code&gt;事务id&lt;/code&gt; 为 &lt;code&gt;10&lt;/code&gt; 的事务提交一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Transaction 10
BEGIN;
UPDATE student SET name=&quot;李四&quot; WHERE id=1;
UPDATE student SET name=&quot;王五&quot; WHERE id=1;
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再到 &lt;code&gt;事务id&lt;/code&gt; 为 &lt;code&gt;20&lt;/code&gt; 的事务中更新一下表 &lt;code&gt;student&lt;/code&gt; 中 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt; 的记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
UPDATE student SET name=&quot;钱七&quot; WHERE id=1;
UPDATE student SET name=&quot;宋八&quot; WHERE id=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此刻，表student中 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt; 的记录的版本链就长这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_22-54-24.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后再到刚才使用 &lt;code&gt;READ COMMITTED&lt;/code&gt; 隔离级别的事务中继续查找这个 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt; 的记录，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1：Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为&apos;张三&apos;

# SELECT2：Transaction 10提交，Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为&apos;王五&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;SELECT2&lt;/code&gt; 的执行过程如下:&lt;/p&gt;
&lt;p&gt;步骤1: 在执行 &lt;code&gt;SELECT&lt;/code&gt; 语句时会又会单独生成一个 &lt;code&gt;ReadView&lt;/code&gt;, 该 &lt;code&gt;ReadView&lt;/code&gt; 的 &lt;code&gt;trx_ids&lt;/code&gt; 列表的内容就是&lt;code&gt;[20]&lt;/code&gt;, &lt;code&gt;up_limit_id&lt;/code&gt; 为 &lt;code&gt;20&lt;/code&gt;, &lt;code&gt;low_limit_id&lt;/code&gt; 为 &lt;code&gt;21&lt;/code&gt;, &lt;code&gt;creator_trx_id&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;步骤2: 从版本链中挑选可见的记录, 从图中看出, 最新版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;宋八&apos;&lt;/code&gt;, 该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为&lt;code&gt;20&lt;/code&gt;, 在 &lt;code&gt;trx_ids&lt;/code&gt; 列表内, 所以不符合可见性要求, 根据 &lt;code&gt;roll_pointer&lt;/code&gt; 跳到下一个版本。&lt;/p&gt;
&lt;p&gt;步骤3: 下一个版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;钱七&apos;&lt;/code&gt;, 该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;20&lt;/code&gt;, 也在 &lt;code&gt;trx_ids&lt;/code&gt; 列表内, 所以也不符合要求, 继续跳到下一个版本。&lt;/p&gt;
&lt;p&gt;步骤4: 下一个版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;王五&apos;&lt;/code&gt;, 该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;10&lt;/code&gt;, 小于 &lt;code&gt;ReadView&lt;/code&gt; 中的 &lt;code&gt;up_limit_id&lt;/code&gt; 值&lt;code&gt;20&lt;/code&gt;, 所以这个版本是符合要求的, 最后返回给用户的版本就是这条列 &lt;code&gt;name&lt;/code&gt; 为 &lt;code&gt;&apos;王五&apos;&lt;/code&gt; 的记录。&lt;/p&gt;
&lt;p&gt;以此类推, 如果之后事务 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;20&lt;/code&gt; 的记录也提交了, 再次在使用 &lt;code&gt;READ COMMITTED&lt;/code&gt; 隔离级别的事务中查询表 &lt;code&gt;student&lt;/code&gt; 中 &lt;code&gt;id&lt;/code&gt; 值为 &lt;code&gt;1&lt;/code&gt; 的记录时, 得到的结果就是 &lt;code&gt;&apos;宋八&apos;&lt;/code&gt; 了, 具体流程我们就不分析了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;强调：使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;REPEATABLE READ隔离级别下&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 隔离级别的事务来说，只会在第一次执行查询语句时生成一个 &lt;code&gt;ReadView&lt;/code&gt; ，之后的查询就不会重复生成了。&lt;/p&gt;
&lt;p&gt;比如，系统里有两个 &lt;code&gt;事务id&lt;/code&gt; 分别为 &lt;code&gt;10&lt;/code&gt; 、 &lt;code&gt;20&lt;/code&gt; 的事务在执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Transaction 10
BEGIN;
UPDATE student SET name=&quot;李四&quot; WHERE id=1;
UPDATE student SET name=&quot;王五&quot; WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此刻，表student 中 id 为 1 的记录得到的版本链表如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_22-58-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用REPEATABLE READ隔离级别的事务
BEGIN;

# SELECT1：Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为&apos;张三&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;SELECT1&lt;/code&gt; 的执行过程如下：&lt;/p&gt;
&lt;p&gt;步骤1：在执行 &lt;code&gt;SELECT&lt;/code&gt; 语句时会先生成一个 &lt;code&gt;ReadView&lt;/code&gt;，&lt;code&gt;ReadView&lt;/code&gt; 的 &lt;code&gt;trx_ids&lt;/code&gt; 列表的内容就是 &lt;code&gt;[10, 20]&lt;/code&gt;，&lt;code&gt;up_limit_id&lt;/code&gt; 为 &lt;code&gt;10&lt;/code&gt;，&lt;code&gt;low_limit_id&lt;/code&gt; 为 &lt;code&gt;21``，creator_trx_id&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;步骤2：然后从版本链中挑选可见的记录，从图中看出，最新版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;王五&apos;&lt;/code&gt;，该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;10&lt;/code&gt;，在 &lt;code&gt;trx_ids&lt;/code&gt; 列表内，所以不符合可见性要求，根据 &lt;code&gt;roll_pointer&lt;/code&gt; 跳到下一个版本。&lt;/p&gt;
&lt;p&gt;步骤3：下一个版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;李四&apos;&lt;/code&gt;，该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值也为 &lt;code&gt;10&lt;/code&gt;，也在 &lt;code&gt;trx_ids&lt;/code&gt; 列表内，所以也不符合要求，继续跳到下一个版本。&lt;/p&gt;
&lt;p&gt;步骤4：下一个版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;张三&apos;&lt;/code&gt;，该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;8&lt;/code&gt;，小于 &lt;code&gt;ReadView&lt;/code&gt; 中的 &lt;code&gt;up_limit_id&lt;/code&gt; 值 &lt;code&gt;10&lt;/code&gt;，所以这个版本是符合要求的，最后返回给用户的版本就是这条列 &lt;code&gt;name&lt;/code&gt; 为 &lt;code&gt;&apos;张三&apos;&lt;/code&gt; 的记录。&lt;/p&gt;
&lt;p&gt;之后，我们把 &lt;code&gt;事务id&lt;/code&gt; 为 &lt;code&gt;10&lt;/code&gt; 的事务提交一下，就像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Transaction 10
BEGIN;

UPDATE student SET name=&quot;李四&quot; WHERE id=1;
UPDATE student SET name=&quot;王五&quot; WHERE id=1;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再到 &lt;code&gt;事务id&lt;/code&gt; 为 &lt;code&gt;20&lt;/code&gt; 的事务中更新一下表 &lt;code&gt;student&lt;/code&gt; 中 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt; 的记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
UPDATE student SET name=&quot;钱七&quot; WHERE id=1;
UPDATE student SET name=&quot;宋八&quot; WHERE id=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此刻，表student中 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt; 的记录的版本链就长这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-05_23-01-09.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后再到刚才使用 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 隔离级别的事务中继续查找这个 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt; 的记录，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1：Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为&apos;张三&apos;
# SELECT2：Transaction 10提交，Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值仍为&apos;张三&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SELECT2&lt;/code&gt; 的执行过程如下:&lt;/p&gt;
&lt;p&gt;步骤1: 因为当前事务的隔离级别为 &lt;code&gt;REPEATABLE READ&lt;/code&gt;, 而之前在执行 &lt;code&gt;SELECT1&lt;/code&gt; 时已经生成过 &lt;code&gt;ReadView&lt;/code&gt; 了, 所以此时直接复用之前的 &lt;code&gt;ReadView&lt;/code&gt;, 之前的 &lt;code&gt;ReadView&lt;/code&gt; 的 &lt;code&gt;trx_ids&lt;/code&gt; 列表的内容就是 &lt;code&gt;[10, 20]&lt;/code&gt;, &lt;code&gt;up_limit_id&lt;/code&gt; 为 &lt;code&gt;10&lt;/code&gt;, &lt;code&gt;low_limit_id&lt;/code&gt; 为 &lt;code&gt;21&lt;/code&gt;, &lt;code&gt;creator_trx_id&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;步骤2: 然后从版本链中挑选可见的记录, 从图中可以看出, 最新版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;宋八&apos;&lt;/code&gt;, 该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;20&lt;/code&gt;, 在 &lt;code&gt;trx_ids&lt;/code&gt; 列表内, 所以不符合可见性要求, 根据 &lt;code&gt;roll_pointer&lt;/code&gt; 跳到下一个版本。&lt;/p&gt;
&lt;p&gt;步骤3: 下一个版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;钱七&apos;&lt;/code&gt;, 该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;20&lt;/code&gt;, 也在 &lt;code&gt;trx_ids&lt;/code&gt; 列表内, 所以也不符合要求, 继续跳到下一个版本。&lt;/p&gt;
&lt;p&gt;步骤4: 下一个版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;王五&apos;&lt;/code&gt;, 该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;10&lt;/code&gt;, 而 &lt;code&gt;trx_ids&lt;/code&gt; 列表中是包含值为 &lt;code&gt;10&lt;/code&gt; 的事务 &lt;code&gt;id&lt;/code&gt; 的, 所以该版本也不符合要求, 同理下一个列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;李四&apos;&lt;/code&gt; 的版本也不符合要求。继续跳到下一个版本。&lt;/p&gt;
&lt;p&gt;步骤5: 下一个版本的列 &lt;code&gt;name&lt;/code&gt; 的内容是 &lt;code&gt;&apos;张三&apos;&lt;/code&gt;, 该版本的 &lt;code&gt;trx_id&lt;/code&gt; 值为 &lt;code&gt;8&lt;/code&gt;, 小于 &lt;code&gt;ReadView&lt;/code&gt; 中的 &lt;code&gt;up_limit_id&lt;/code&gt; 值 &lt;code&gt;10&lt;/code&gt;, 所以这个版本是符合要求的, 最后返回给用户的版本就是这条列 &lt;code&gt;c&lt;/code&gt; 为 &lt;code&gt;&apos;张三&apos;&lt;/code&gt; 的记录。&lt;/p&gt;
&lt;p&gt;这次SELECT查询得到的结果是重复的，记录的列c值都是张三，这就是可重复读的含义。如果我们之后再把事务id为20的记录提交了，然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个id为1的记录，得到的结果还是张三，具体执行过程大家可以自己分析一下。&lt;/p&gt;
&lt;h4&gt;如何解决幻读&lt;/h4&gt;
&lt;p&gt;接下来说明InnoDB 是如何解决幻读的 (以下都是基于RR隔离级别)。&lt;/p&gt;
&lt;p&gt;假设现在表 student 中只有一条数据，数据内容中，主键 id=1，隐藏的 trx_id=10，它的 undo log 如下图所示。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-06_21-03-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;假设现在有事务 A 和事务 B 并发执行，&lt;code&gt;事务 A&lt;/code&gt; 的事务 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;20&lt;/code&gt; ， &lt;code&gt;事务 B&lt;/code&gt; 的事务 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;30&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;步骤1：事务 A 开始第一次查询数据，查询的 SQL 语句如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from student where id &amp;gt;= 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在开始查询之前，MySQL 会为事务 A 产生一个 ReadView，此时 &lt;code&gt;ReadView&lt;/code&gt; 的内容如下： &lt;code&gt;trx_ids= [20,30]&lt;/code&gt;， &lt;code&gt;up_limit_id=20&lt;/code&gt; ， &lt;code&gt;low_limit_id=31&lt;/code&gt; ， &lt;code&gt;creator_trx_id=20&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;由于此时表 student 中只有一条数据，且符合 where id&amp;gt;=1 条件，因此会查询出来。然后根据 ReadView 机制，发现该行数据的trx_id=10，小于事务 A 的 ReadView 里 up_limit_id，这表示这条数据是事务 A 开启之前，其他事务就已经提交了的数据，因此事务 A 可以读取到。&lt;/p&gt;
&lt;p&gt;结论：事务 A 的第一次查询，能读取到一条数据，id=1。&lt;/p&gt;
&lt;p&gt;步骤2：接着事务 B(trx_id=30)，往表 student 中新插入两条数据，并提交事务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;insert into student(id,name) values(2,&apos;李四&apos;);
insert into student(id,name) values(3,&apos;王五&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时表student 中就有三条数据了，对应的 undo 如下图所示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-06_21-06-03.png&quot; alt=&quot;&quot; /&gt;
步骤3：接着事务 A 开启第二次查询，根据可重复读隔离级别的规则，此时事务 A 并不会再重新生成 ReadView。此时表 &lt;code&gt;student&lt;/code&gt; 中的 3 条数据都满足 &lt;code&gt;where id&amp;gt;=1&lt;/code&gt; 的条件，因此会先查出来。然后根据 ReadView 机制，判断每条数据是不是都可以被事务 A 看到。&lt;/p&gt;
&lt;p&gt;1）首先 &lt;code&gt;id=1&lt;/code&gt; 的这条数据，前面已经说过了，可以被&lt;code&gt;事务 A&lt;/code&gt; 看到。&lt;/p&gt;
&lt;p&gt;2）然后是 &lt;code&gt;id=2&lt;/code&gt; 的数据，它的 &lt;code&gt;trx_id=30&lt;/code&gt;，此时&lt;code&gt;事务 A&lt;/code&gt; 发现，这个值处于 &lt;code&gt;up_limit_id&lt;/code&gt; 和 &lt;code&gt;low_limit_id&lt;/code&gt; 之间，因此还需要再判断 30 是否处于 &lt;code&gt;trx_ids&lt;/code&gt; 数组内。由于&lt;code&gt;事务 A&lt;/code&gt;的 &lt;code&gt;trx_ids=[20,30]&lt;/code&gt;，因此在数组内，这表示 &lt;code&gt;id=2&lt;/code&gt; 的这条数据是与&lt;code&gt;事务 A&lt;/code&gt; 在同一时刻启动的其他事务提交的，所以这条数据不能让&lt;code&gt;事务 A&lt;/code&gt; 看到。&lt;/p&gt;
&lt;p&gt;3）同理，&lt;code&gt;id=3&lt;/code&gt; 的这条数据，&lt;code&gt;trx_id&lt;/code&gt; 也为 30，因此也不能被事务 A 看见。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-06_21-08-12.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;结论：最终事务 A 的第二次查询，只能查询出 id=1 的这条数据。这和事务 A 的第一次查询的结果是一样 的，因此没有出现幻读现象，所以说在 MySQL 的可重复读隔离级别下，不存在幻读问题。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;这里介绍了 &lt;code&gt;MVCC&lt;/code&gt; 在 &lt;code&gt;READ COMMITTD&lt;/code&gt; 、 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 这两种隔离级别的事务在执行快照读操作时 访问记录的版本链的过程。这样使不同事务的 &lt;code&gt;读-写&lt;/code&gt; 、 &lt;code&gt;写-读&lt;/code&gt; 操作并发执行，从而提升系统性能。&lt;/p&gt;
&lt;p&gt;核心点在于 &lt;code&gt;ReadView&lt;/code&gt; 的原理， &lt;code&gt;READ COMMITTD&lt;/code&gt; 、 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 这两个隔离级别的一个很大不同 就是生成ReadView的时机不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;READ COMMITTD&lt;/code&gt; 在每一次进行普通SELECT操作前都会生成一个ReadView&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REPEATABLE READ&lt;/code&gt; 只在第一次进行普通SELECT操作前生成一个ReadView，之后的查询操作都重复 使用这个ReadView就好了。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;说明:我们之前说执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过MVCC我们可以解决：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;读写之间阻塞的问题&lt;/strong&gt;。通过 MVCC 可以让读写互相不阻塞，即读不阻塞写，写不阻塞读，这样就可以提升事务并发处理能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;降低了死锁的概率&lt;/strong&gt;。这是因为 MVCC 采用了乐观锁的方式，读取数据时并不需要加锁，对于写操作，也只锁定必要的行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决快照读的问题&lt;/strong&gt;。当我们查询数据库在某个时间点的快照时，只能看到这个时间点之前事务提交更新的结果，而不能看到这个时间点之后事务提交的更新结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;重点：ReadView规则，MVCC操作流程&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;简单来讲：
MVCC = 两个隐藏列 + undo log版本链 + ReadView&lt;/p&gt;
&lt;p&gt;undo log控制版本、ReadView并发控制及管理&lt;/p&gt;
</content:encoded></item><item><title>MySQL - 锁</title><link>https://zzyang.top/posts/mysql-lock/</link><guid isPermaLink="true">https://zzyang.top/posts/mysql-lock/</guid><pubDate>Tue, 05 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MySQL - 锁&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;事务的&lt;code&gt;隔离性&lt;/code&gt;由这章讲述的&lt;code&gt;锁&lt;/code&gt;来实现。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;锁&lt;/code&gt;是计算机协调多个进程或线程&lt;code&gt;并发访问某一共享资源&lt;/code&gt;的机制。在程序开发中会存在多线程同步的问题，当多个线程并发访问某个数据的时候，尤其是针对一些敏感的数据（比如订单、金额等），我们就需要保证这个数据在任何时刻&lt;code&gt;最多只有一个线程&lt;/code&gt;在访问，保证数据的&lt;code&gt;完整性&lt;/code&gt;和&lt;code&gt;一致性&lt;/code&gt;。在开发过程中加锁是为了保证数据的一致性，这个思想在数据库领域中同样很重要。&lt;/p&gt;
&lt;p&gt;在数据库中，除传统的计算资源（如CPU、RAM、I/O等）的争用以外，数据也是一种供许多用户共享的 资源。为保证数据的一致性，需要对 &lt;code&gt;并发操作进行控制&lt;/code&gt; ，因此产生了 &lt;code&gt;锁&lt;/code&gt; 。同时 &lt;code&gt;锁机制&lt;/code&gt; 也为实现MySQL 的各个隔离级别提供了保证。 &lt;code&gt;锁冲突&lt;/code&gt; 也是影响数据库 &lt;code&gt;并发访问性能&lt;/code&gt; 的一个重要因素。所以锁对数据库而言显得尤其重要，也更加复杂。&lt;/p&gt;
&lt;h2&gt;MySQL并发事务访问相同记录&lt;/h2&gt;
&lt;p&gt;并发事务访问相同记录的情况大致可以划分为3种：&lt;/p&gt;
&lt;h3&gt;读-读情况&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;读-读&lt;/code&gt;情况，即并发事务相继读&lt;code&gt;取相同的记录&lt;/code&gt;。读取操作本身不会对记录有任何影响，并不会引起什么问题，所以允许这种情况的发生。&lt;/p&gt;
&lt;h3&gt;写-写情况&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;写-写&lt;/code&gt; 情况，即并发事务相继对相同的记录做出改动。&lt;/p&gt;
&lt;p&gt;在这种情况下会发生 &lt;code&gt;脏写&lt;/code&gt; 的问题，任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时，需要让它们 排队执行 ，这个排队的过程其实是通过 锁 来实现的。这个所谓的锁其实是一个内存中的结构 ，在事务执行前本来是没有锁的，也就是说一开始是没有 锁结构 和记录进 行关联的，如图所示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-23_21-45-41.png&quot; alt=&quot;&quot; /&gt;
当一个事务想对这条记录做改动时，首先会看看内存中有没有与这条记录关联的 &lt;code&gt;锁结构&lt;/code&gt; ，当没有的时候 就会在内存中生成一个 &lt;code&gt;锁结构&lt;/code&gt; 与之关联。比如，事务 &lt;code&gt;T1&lt;/code&gt; 要对这条记录做改动，就需要生成一个 &lt;code&gt;锁结构&lt;/code&gt; 与之关联：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-23_21-47-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;锁结构&lt;/code&gt;里有很多信息，为了简化理解，只把两个比较重要的属性拿了出来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trx信息&lt;/code&gt;：代表这个锁结构是哪个事务生成的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_waiting&lt;/code&gt;：代表当前事务是否在等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在事务&lt;code&gt;T1&lt;/code&gt;改动了这条记录后，就生成了一个&lt;code&gt;锁结构&lt;/code&gt;与该记录关联，因为之前没有别的事务为这条记录加锁，所以&lt;code&gt;is_waiting&lt;/code&gt;属性就是&lt;code&gt;false&lt;/code&gt;，我们把这个场景就称值为&lt;strong&gt;获取锁成功&lt;/strong&gt;，或者&lt;strong&gt;加锁成功&lt;/strong&gt;，然后就可以继续执行操作了。&lt;/p&gt;
&lt;p&gt;在事务&lt;code&gt;T1&lt;/code&gt;提交之前，另一个事务&lt;code&gt;T2&lt;/code&gt;也想对该记录做改动，那么先看看有没有&lt;code&gt;锁结构&lt;/code&gt;与这条记录关联，发现有一个&lt;code&gt;锁结构&lt;/code&gt;与之关联后，然后也生成了一个锁结构与这条记录关联，不过锁结构的&lt;code&gt;is_waiting&lt;/code&gt;属性值为&lt;code&gt;true&lt;/code&gt;，表示当前事务需要等待，我们把这个场景就称之为&lt;strong&gt;获取锁失败&lt;/strong&gt;，或者加锁失败，图示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-23_21-50-33.png&quot; alt=&quot;&quot; /&gt;
在事务&lt;code&gt;T1&lt;/code&gt;提交之后，就会把该事务生成的&lt;code&gt;锁结构释放掉&lt;/code&gt;，然后看看还有没有别的事务在等待获取锁，发现了事务T2还在等待获取锁，所以把事务T2对应的锁结构的&lt;code&gt;is_waiting&lt;/code&gt;属性设置为&lt;code&gt;false&lt;/code&gt;，然后把该事务对应的线程唤醒，让它继续执行，此时事务T2就算获取到锁了。效果就是这样。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-23_21-51-44.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::info
&lt;strong&gt;脏写&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;关于脏写的问题，比如说，这里有一条记录，我们用事务A对它进行写入，同时事务B也对它进行写入。假设这条记录的初始值是1，A和B在读取时看到的都是1。接下来，A把这个值改成了2，而B又把它改成了3。然后，A执行了commit操作，提交了修改，这时A认为2这个值已经被固定下来了。但B随后执行了rollback操作，把3回滚回去，结果这条记录又变成了1。这样一来，A再去查，发现结果又变成了1。这就是我们说的“脏写”问题。你会发现，当一个事务还没完成修改时，另一个事务又参与进来，就会导致数据混乱。那该怎么办呢？要解决这个问题，必须让两个事务排队执行，一个一个来。比如A改完了，提交或回滚后，B再进来操作。通过排队执行，才能解决这个安全性问题。
:::&lt;/p&gt;
&lt;p&gt;小结几种说法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不加锁&lt;/p&gt;
&lt;p&gt;意思就是不需要在内存中生成对应的 &lt;code&gt;锁结构&lt;/code&gt; ，可以直接执行操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取锁成功，或者加锁成功&lt;/p&gt;
&lt;p&gt;意思就是在内存中生成了对应的 &lt;code&gt;锁结构&lt;/code&gt; ，而且锁结构的 &lt;code&gt;is_waiting&lt;/code&gt; 属性为 &lt;code&gt;false&lt;/code&gt; ，也就是事务 可以继续执行操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取锁失败，或者加锁失败，或者没有获取到锁&lt;/p&gt;
&lt;p&gt;意思就是在内存中生成了对应的 &lt;code&gt;锁结构&lt;/code&gt; ，不过锁结构的 &lt;code&gt;is_waiting&lt;/code&gt; 属性为 &lt;code&gt;true&lt;/code&gt; ，也就是事务 需要等待，不可以继续执行操作。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;读-写或写-读情况&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;读-写&lt;/code&gt; 或 &lt;code&gt;写-读&lt;/code&gt; ，即一个事务进行读取操作，另一个进行改动操作。这种情况下可能发生 &lt;code&gt;脏读&lt;/code&gt; 、 &lt;code&gt;不可重复读&lt;/code&gt; 、 &lt;code&gt;幻读&lt;/code&gt; 的问题。&lt;/p&gt;
&lt;p&gt;各个数据库厂商对 &lt;code&gt;SQL标准&lt;/code&gt; 的支持都可能不一样。比如MySQL在 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 隔离级别上就已经解决了 &lt;code&gt;幻读&lt;/code&gt; 问题。&lt;/p&gt;
&lt;h3&gt;并发问题的解决方案&lt;/h3&gt;
&lt;p&gt;怎么解决 &lt;code&gt;脏读&lt;/code&gt; 、 &lt;code&gt;不可重复读&lt;/code&gt; 、 &lt;code&gt;幻读&lt;/code&gt; 这些问题呢？其实有两种可选的解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方案一：读操作利用多版本并发控制（ &lt;code&gt;MVCC&lt;/code&gt; ，下章讲解），写操作进行 &lt;code&gt;加锁&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所谓的 &lt;code&gt;MVCC&lt;/code&gt;，就是生成一个 &lt;code&gt;ReadView&lt;/code&gt;，通过 ReadView 找到符合条件的记录版本（历史版本由 &lt;code&gt;undo日志&lt;/code&gt; 构建）。查询语句只能&lt;code&gt;读&lt;/code&gt;到在生成 &lt;code&gt;ReadView&lt;/code&gt; 之前&lt;code&gt;已提交事务所做的更改&lt;/code&gt;，在生成 ReadView 之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而&lt;code&gt;写操作&lt;/code&gt;肯定针对的是&lt;code&gt;最新版本的记录&lt;/code&gt;，读记录的历史版本和改动记录的最新版本本身并不冲突，也就是采用 MVCC 时，&lt;code&gt;读 - 写&lt;/code&gt;操作并不冲突。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;普通的&lt;code&gt;SELECT&lt;/code&gt;语句在&lt;code&gt;READ COMMITTED&lt;/code&gt;和&lt;code&gt;REPEATABLE READ&lt;/code&gt;隔离级别下会使用到&lt;code&gt;MVCC&lt;/code&gt;读取记录。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;READ COMMITTED&lt;/code&gt; 隔离级别下，一个事务在执行过程中每次执行&lt;code&gt;SELECT&lt;/code&gt;操作时都会生成一 个&lt;code&gt;ReadView&lt;/code&gt;，ReadView的存在本身就保证了&lt;code&gt;事务不可以读取到未提交的事务所做的更改&lt;/code&gt; ，也就是避免了脏读现象；&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 隔离级别下，一个事务在执行过程中只有 &lt;code&gt;第一次执行SELECT操作&lt;/code&gt; 才会生成一个ReadView，之后的SELECT操作都 &lt;code&gt;复用&lt;/code&gt; 这个ReadView，这样也就避免了不可重复读和幻读的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;方案二：读、写操作都采用 加锁 的方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们的一些业务场景不允许读取记录的旧版本，而是每次都必须去&lt;code&gt;读取记录的最新版本&lt;/code&gt;。比如，在银行存款的事务中，你需要先把账户的余额读出来，然后将其加上本次存款的数额，最后再写到数据库中。在将账户余额读取出来后，就不想让别的事务再访问该余额，直到本次存款事务执行完成，其他事务才可以访问账户的余额。这样在读取记录的时候就需要对其进行&lt;code&gt;加锁&lt;/code&gt;操作，这样也就意味着&lt;code&gt;读&lt;/code&gt;操作和&lt;code&gt;写&lt;/code&gt;操作也像&lt;code&gt;写 - 写&lt;/code&gt;操作那样&lt;code&gt;排队&lt;/code&gt;执行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;脏读&lt;/code&gt;的产生是因为当前事务读取了另一个未提交事务写的一条记录，如果另一个事务在写记录的时候就给这条记录加锁，那么当前事务就无法继续读取该记录了，所以也就不会有脏读问题的产生了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;不可重复读&lt;/code&gt;的产生是因为当前事务先读取一条记录，另外一个事务对该记录做了改动之后并提交之后，当前事务再次读取时会获得不同的值，如果在当前事务读取记录时就给该记录加锁，那么另一个事务就无法修改该记录，自然也不会发生不可重复读了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;幻读&lt;/code&gt;问题的产生是因为当前事务读取了一个范围的记录，然后另外的事务向该范围内插入了新记录，当前事务再次读取该范围的记录时发现了新插入的新记录。采用加锁的方式解决幻读问题就有一些麻烦，因为当前事务在第一次读取记录时幻影记录并不存在，所以读取的时候加锁就有点尴尬（因为你并不知道给谁加锁）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小结对比发现：
&lt;ul&gt;
&lt;li&gt;采用 &lt;code&gt;MVCC&lt;/code&gt; 方式的话， 读-写 操作彼此并不冲突， 性能更高 。&lt;/li&gt;
&lt;li&gt;采用 &lt;code&gt;加锁&lt;/code&gt; 方式的话， 读-写 操作彼此需要 &lt;code&gt;排队执行&lt;/code&gt; ，影响性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般情况下我们当然愿意采用 &lt;code&gt;MVCC&lt;/code&gt; 来解决 &lt;code&gt;读-写&lt;/code&gt; 操作并发执行的问题，但是业务在某些特殊情况下，要求必须采用 &lt;code&gt;加锁&lt;/code&gt; 的方式执行。下面就讲解下MySQL中不同类别的锁。&lt;/p&gt;
&lt;h2&gt;锁的不同角度分类&lt;/h2&gt;
&lt;p&gt;锁的分类图，如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-23_22-49-12.png&quot; alt=&quot;&quot; /&gt;
在表级锁这个层面，比如说我们的 &lt;code&gt;MyISAM&lt;/code&gt; 引擎，它只支持表级锁，而像 &lt;code&gt;InnoDB&lt;/code&gt; 引擎，它既可以支持表级锁，也可以支持行级锁。使用行级锁时，锁的粒度更小，并发性也会更好一些。因此，InnoDB 通常我们都会用行级锁。接下来，我们可以根据对锁的态度，将其分为悲观锁和乐观锁。所谓悲观和乐观，就像面对半瓶水一样，悲观的人会说“只剩半瓶水了”，而乐观的人会说“还剩半瓶水”。我们对待锁的态度也是如此：有的人担心数据会被修改，这就是悲观锁；而乐观锁则认为数据不会轻易被修改。后面我们还会具体提到这些情况。此外，还有加锁的方式，可以分为显式加锁和隐式加锁。其他相关内容还包括全局锁和死锁的问题。&lt;/p&gt;
&lt;h3&gt;从数据操作的类型划分：读锁、写锁&lt;/h3&gt;
&lt;p&gt;对于数据库中并发事务的&lt;code&gt;读-读&lt;/code&gt;情况并不会引起什么问题。对于&lt;code&gt;写-写&lt;/code&gt;、&lt;code&gt;读-写&lt;/code&gt;或&lt;code&gt;写-读&lt;/code&gt;这些情况可能会引起一些问题，需要使用 &lt;code&gt;MVCC&lt;/code&gt; 或者&lt;code&gt;加锁&lt;/code&gt;的方式来解决它们。在使用加锁的方式解决问题时，由于既要允许&lt;code&gt;读-读&lt;/code&gt;情况不受影响，又要使&lt;code&gt;写-写&lt;/code&gt;、&lt;code&gt;读-写&lt;/code&gt;或&lt;code&gt;写-读&lt;/code&gt;情况中的操作相互阻塞，所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为&lt;strong&gt;共享锁（Shared Lock，S Lock）&lt;strong&gt;和&lt;/strong&gt;排他锁（Exclusive Lock，X Lock）&lt;/strong&gt;，也叫&lt;strong&gt;读锁（readlock）&lt;strong&gt;和&lt;/strong&gt;写锁（write lock）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;读锁&lt;/code&gt; ：也称为 &lt;code&gt;共享锁&lt;/code&gt; 、英文用 S 表示。针对同一份数据，多个事务的读操作可以同时进行而不会互相影响，相互不阻塞的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;写锁&lt;/code&gt; ：也称为 &lt;code&gt;排他锁&lt;/code&gt; 、英文用 X 表示。当前写操作没有完成前，它会阻断其他写锁和读锁。这样 就能确保在给定的时间里，只有一个事务能执行写入，并防止其他用户读取正在写入的同一资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意的是对于 InnoDB 引擎来说，读锁和写锁可以加在表上，也可以加在行上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例 (行级读写锁)&lt;/strong&gt;：如果一个事务 T1 已经获得了某个行 r 的读锁，那么此时另外的一个事务 T2 是可以去获得这个行 r 的读锁的，因为读取操作并没有改变行 r 的数据；但是，如果某个事务 T3 想获得行 r 的写锁，则它必须等待事务 T1、T2 释放掉行 r 上的读锁才行。&lt;/p&gt;
&lt;p&gt;总结：这里的兼容是指对同一张表或记录的锁的兼容性情况。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;X锁&lt;/th&gt;
&lt;th&gt;S锁&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;X锁&lt;/td&gt;
&lt;td&gt;不兼容&lt;/td&gt;
&lt;td&gt;不兼容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S锁&lt;/td&gt;
&lt;td&gt;不兼容&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;兼容&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;&lt;strong&gt;1. 锁定读&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在采用 &lt;code&gt;加锁&lt;/code&gt; 方式解决 &lt;code&gt;脏读&lt;/code&gt; 、 &lt;code&gt;不可重复读&lt;/code&gt; 、 &lt;code&gt;幻读&lt;/code&gt; 这些问题时，读取一条记录时需要获取该记录的 &lt;code&gt;S锁&lt;/code&gt; ，其实是不严谨的，有时候需要在读取记录时就获取记录的 &lt;code&gt;X锁&lt;/code&gt; ，来禁止别的事务读写该记录，为此MySQL提出了两种比较特殊的 &lt;code&gt;SELECT&lt;/code&gt; 语句格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对读取的记录加 &lt;code&gt;S锁&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;SELECT... LOCK IN SHARE MODE;
#或
SELECT ... FOR SHARE; #(8.0新增语法)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在普通的SELECT语句后边加&lt;code&gt;LOCK IN SHARE MODE&lt;/code&gt;，如果当前事务执行了该语句，那么它会为读取到的记录加&lt;code&gt;S锁&lt;/code&gt;，这样允许别的事务继续获取这些记录的S锁（比方说别的事务也使用&lt;code&gt;SELECT...LOCK IN SHARE MODE&lt;/code&gt;语句来读取这些记录），但是不能获取这些记录的&lt;code&gt;X锁&lt;/code&gt;（比如使用&lt;code&gt;SELECT...FOR UPDATE&lt;/code&gt;语句来读取这些记录，或者直接修改这些记录）。如果别的事务想要获取这些记录的&lt;code&gt;X锁&lt;/code&gt;，那么它们会阻塞，直到当前事务提交之后将这些记录上的&lt;code&gt;S锁&lt;/code&gt;释放掉。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对读取的记录加 &lt;code&gt;X 锁&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;SELECT... FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在普通的 &lt;code&gt;SELECT&lt;/code&gt; 语句后边加 &lt;code&gt;FOR UPDATE&lt;/code&gt;，如果当前事务执行了该语句，那么它会为读取到的记录加 &lt;code&gt;X锁&lt;/code&gt;，这样既不允许别的事务获取这些记录的 &lt;code&gt;S锁&lt;/code&gt;（比方说别的事务使用 &lt;code&gt;SELECT... LOCK IN SHARE MODE&lt;/code&gt; 语句来读取这些记录），也不允许获取这些记录的 &lt;code&gt;X锁&lt;/code&gt;（比如使用 &lt;code&gt;SELECT... FOR UPDATE&lt;/code&gt; 语句来读取这些记录，或者直接修改这些记录）。如果别的事务想要获取这些记录的 &lt;code&gt;S锁&lt;/code&gt;或者 &lt;code&gt;X锁&lt;/code&gt;，那么它们会阻塞，直到当前事务提交之后将这些记录上的 &lt;code&gt;X锁&lt;/code&gt;释放掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MySQL8.0新特性:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在5.7及之前的版本，SELECT ... FOR UPDATE，如果获取不到锁，会一直等待，直到&lt;code&gt;innodb_lock_wait_timeout&lt;/code&gt;超时。在8.0版本中，&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;，&lt;code&gt;SELECT ... FOR SHARE&lt;/code&gt;添加&lt;code&gt;NOWAIT&lt;/code&gt;、&lt;code&gt;SKIP LOCKED&lt;/code&gt;语法，跳过锁等待，或者跳过锁定。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过添加&lt;code&gt;NOWAIT&lt;/code&gt;、&lt;code&gt;SKIP LOCKED&lt;/code&gt;语法，能够立即返回。如果查询的行已经加锁：
&lt;ul&gt;
&lt;li&gt;那么&lt;code&gt;NOWAIT&lt;/code&gt;会立即报错返回&lt;/li&gt;
&lt;li&gt;而&lt;code&gt;SKIP LOCKED&lt;/code&gt;也会立即返回，只是返回的结果中不包含被锁定的行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# session1:
mysql&amp;gt; begin;
mysql&amp;gt; select * from t1 where c1 = 2 for update;
+------+-------+
| c1   | c2    |
+------+-------+
| 2    | 60530 |
| 2    | 24678 |
+------+-------+
2 rows in set (0.00 sec)

# session2:
mysql&amp;gt; select * from t1 where c1 = 2 for update nowait;
ERROR 3572 (HY000): Statement aborted because lock(s) could not be acquired immediately and
NOWAIT is set.
mysql&amp;gt; select * from t1 where c1 = 2 for update skip locked;
Empty set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;2. 写操作&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;平常所用到的&lt;code&gt;写操作&lt;/code&gt;无非是 &lt;code&gt;DELETE&lt;/code&gt;、&lt;code&gt;UPDATE&lt;/code&gt;、&lt;code&gt;INSERT&lt;/code&gt; 这三种:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DELETE&lt;/code&gt;:
对一条记录做 DELETE 操作的过程其实是先在 &lt;code&gt;B+树&lt;/code&gt;中定位到这条记录的位置，然后获取这条记录的 &lt;code&gt;X 锁&lt;/code&gt;，再执行 &lt;code&gt;delete mark&lt;/code&gt; 操作。我们也可以把这个定位待删除记录在 B+ 树中位置的过程看成是一个获取 &lt;code&gt;X 锁&lt;/code&gt;的&lt;code&gt;锁定读&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UPDATE&lt;/code&gt;:在对一条记录做 UPDATE 操作时分为三种情况:
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;情况1: 未修改该记录的键值，并且被更新的列占用的存储空间在修改前后未发生变化。&lt;/p&gt;
&lt;p&gt;则先在 &lt;code&gt;B+ 树&lt;/code&gt;中定位到这条记录的位置，然后再获取一下记录的 &lt;code&gt;X 锁&lt;/code&gt;，最后在原记录的位置进行修改操作。我们也可以把这个定位待修改记录在 &lt;code&gt;B+ 树&lt;/code&gt;中位置的过程看成是一个获取 &lt;code&gt;X 锁&lt;/code&gt;的&lt;code&gt;锁定读&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况2: 未修改该记录的键值，并且至少有一个被更新的列占用的存储空间在修改前后发生变化。&lt;/p&gt;
&lt;p&gt;则先在 &lt;code&gt;B+ 树&lt;/code&gt;中定位到这条记录的位置，然后获取一下记录的 &lt;code&gt;X 锁&lt;/code&gt;，将该记录彻底删除掉(就是把记录彻底移入垃圾链表)，最后再插入一条新记录。这个定位待修改记录在 &lt;code&gt;B+ 树&lt;/code&gt;中位置的过程看成是一个获取 &lt;code&gt;X锁&lt;/code&gt;的&lt;code&gt;锁定读&lt;/code&gt;，新插入的记录由 &lt;code&gt;INSERT&lt;/code&gt; 操作提供的&lt;code&gt;隐式锁&lt;/code&gt;进行保护。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况3: 修改了该记录的主键值，则相当于在原记录上做 DELETE 操作之后再来一次 INSERT 操作，加锁操作就需要按照 &lt;code&gt;DELETE&lt;/code&gt; 和 &lt;code&gt;INSERT&lt;/code&gt; 的规则进行了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSERT&lt;/code&gt;:
一般情况下，新插入一条记录的操作并不加锁，通过一种称之为&lt;code&gt;隐式锁&lt;/code&gt;的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;从数据操作的粒度划分：表级锁、页级锁、行锁&lt;/h3&gt;
&lt;p&gt;为了尽可能提高数据库的并发度，每次锁定的数据范围越小越好，理论上每次只锁定当前操作的数据的方案会得到最大的并发度，但是管理锁是很&lt;code&gt;耗资源&lt;/code&gt;的事情（涉及获取、检查、释放锁等动作）。因此数据库系统需要在&lt;code&gt;高并发响应&lt;/code&gt;和&lt;code&gt;系统性能&lt;/code&gt;两方面进行平衡，这样就产生了“&lt;code&gt;锁粒度（Lock granularity）&lt;/code&gt;”的概念。&lt;/p&gt;
&lt;p&gt;对一条记录加锁影响的也只是这条记录而已，我们就说这个锁的粒度比较细；其实一个事务也可以在表级别进行加锁，自然就被称之为&lt;code&gt;表级锁&lt;/code&gt;或者&lt;code&gt;表锁&lt;/code&gt;，对一个表加锁影响整个表中的记录，我们就说这个锁的粒度比较粗。锁的粒度主要分为&lt;code&gt;表级锁&lt;/code&gt;、&lt;code&gt;页级锁&lt;/code&gt;和&lt;code&gt;行锁&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;1. 表锁（Table Lock）&lt;/h4&gt;
&lt;p&gt;该锁会锁定整张表，它是 MySQL 中最基本的锁策略，并&lt;strong&gt;不依赖于存储引擎&lt;/strong&gt;（不管你是 MySQL 的什么存储引擎，对于表锁的策略都是一样的），并且表锁是&lt;strong&gt;开销最小&lt;/strong&gt;的策略（因为粒度比较大）。由于表级锁一次会将整个表锁定，所以可以很好的&lt;strong&gt;避免死锁&lt;/strong&gt;问题。当然，锁的粒度大所带来最大的负面影响就是出现锁资源争用的概率也会最高，导致&lt;strong&gt;并发率大打折扣&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;读锁两个事务可读，不可写&lt;/p&gt;
&lt;p&gt;写锁只有一个事务可读可写，其他不行&lt;/p&gt;
&lt;h5&gt;1. 表级别的S锁、X锁&lt;/h5&gt;
&lt;p&gt;在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时，InnoDB存储引擎是不会为这个表添加表级别的 &lt;code&gt;S锁&lt;/code&gt; 或者 &lt;code&gt;X锁&lt;/code&gt; 的。在对某个表执行一些诸如 &lt;code&gt;ALTER TABLE&lt;/code&gt; 、 &lt;code&gt;DROP TABLE&lt;/code&gt; 这类的 &lt;code&gt;DDL&lt;/code&gt; 语句时，其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理，某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时，在其他会话中对这个表执行 &lt;code&gt;DDL&lt;/code&gt; 语句也会发生阻塞。这个过程其实是通过在 server层使用一种称之为 &lt;code&gt;元数据锁&lt;/code&gt; （英文名： &lt;code&gt;Metadata Locks&lt;/code&gt; ， 简称 MDL ）结构来实现的。&lt;/p&gt;
&lt;p&gt;一般情况下，不会使用InnoDB存储引擎提供的表级别的 &lt;code&gt;S锁&lt;/code&gt; 和 &lt;code&gt;X锁&lt;/code&gt; 。只会在一些特殊情况下，比方说 &lt;code&gt;崩溃恢复&lt;/code&gt; 过程中用到。比如，在系统变量 &lt;code&gt;autocommit=0&lt;/code&gt;，&lt;code&gt;innodb_table_locks = 1&lt;/code&gt; 时， 手动 获取 InnoDB存储引擎提供的表t 的 &lt;code&gt;S锁&lt;/code&gt; 或者 &lt;code&gt;X锁&lt;/code&gt; 可以这么写：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LOCK TABLES t READ&lt;/code&gt; ：InnoDB存储引擎会对&lt;code&gt;表 t&lt;/code&gt; 加表级别的 &lt;code&gt;S锁&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOCK TABLES t WRITE&lt;/code&gt; ：InnoDB存储引擎会对&lt;code&gt;表 t&lt;/code&gt; 加表级别的 &lt;code&gt;X锁&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;autocommit = 0&lt;/code&gt;(autocommit=1 是默认值)，设置当前会话的自动提交行为为 关闭。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;innodb_table_locks = 1&lt;/code&gt;(默认值是：1)，允许 InnoDB 对表执行 表级锁（table-level locks）。InnoDB 在执行 LOCK TABLES 时会配合加上 InnoDB 的内部表锁（也称意向锁）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不过尽量避免在使用InnoDB存储引擎的表上使用 &lt;code&gt;LOCK TABLES&lt;/code&gt; 这样的手动锁表语句，它们并不会提供 什么额外的保护，只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的 &lt;code&gt;行锁&lt;/code&gt; ，关于 InnoDB表级别的 &lt;code&gt;S锁&lt;/code&gt; 和 &lt;code&gt;X锁&lt;/code&gt; 大家了解一下就可以了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例：&lt;/strong&gt; 下面我们讲解MyISAM引擎下的表锁。&lt;/p&gt;
&lt;p&gt;步骤1：创建表并添加数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE mylock(
id INT NOT NULL PRIMARY KEY auto_increment,
NAME VARCHAR(20)
)ENGINE myisam;

# 插入一条数据
INSERT INTO mylock(NAME) VALUES(&apos;a&apos;);

# 查询表中所有数据
SELECT * FROM mylock;
+----+------+
| id | Name |
+----+------+
| 1  | a    |
+----+------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;步骤二：查看表上加过的锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW OPEN TABLES; # 主要关注In_use字段的值
或者
SHOW OPEN TABLES where In_use &amp;gt; 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show open tables;
+----------+-----------------------------------+---------+-----------+
| Database | Table                             | In_use  | Name_locked |
+----------+-----------------------------------+---------+-----------+
| atguigudb3 | mylock                            |       0 |         0 |
| sys      | x$waits_by_user_by_latency        |       0 |         0 |
| sys      | x$user_summary_by_stages          |       0 |         0 |
| sys      | x$statements_with_sorting         |       0 |         0 |
| sys      | x$statements_with_runtimes_in_95th_percentile |       0 |         0 |
| sys      | x$statements_with_full_table_scans|       0 |         0 |
| sys      | x$session                         |       0 |         0 |
| sys      | x$schema_table_statistics_with_buffer |       0 |         0 |
| sys      | x$schema_table_lock_waits         |       0 |         0 |
| sys      | x$schema_index_statistics         |       0 |         0 |
| sys      | x$processlist                     |       0 |         0 |
| sys      | x$memory_global_total             |       0 |         0 |
| sys      | x$io_global_by_wait_by_bytes      |       0 |         0 |
| sys      | x$io_by_thread_by_latency         |       0 |         0 |
| sys      | x$statement_analysis              |       0 |         0 |
| sys      | x$host_summary_by_stages          |       0 |         0 |
| sys      | x$host_summary_by_file_io_type    |       0 |         0 |
| sys      | x$host_summary                    |       0 |         0 |
| sys      | waits_by_host_by_latency          |       0 |         0 |
+----------+-----------------------------------+---------+-----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的结果表明，当前数据库中没有被锁定的表&lt;/p&gt;
&lt;p&gt;步骤3：手动增加表锁命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOCK TABLES t READ; # 存储引擎会对表t加表级别的共享锁。共享锁也叫读锁或S锁（Share的缩写）
LOCK TABLES t WRITE; # 存储引擎会对表t加表级别的排他锁。排他锁也叫独占锁、写锁或X锁（exclusive的缩写）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; lock tables mylock read;
Query OK, 0 rows affected (0.00 sec)

mysql&amp;gt; show open tables where in_use &amp;gt; 0;
+----------+--------+--------+-------------+
| Database | Table  | In_use | Name_locked |
+----------+--------+--------+-------------+
| atguigudb3 | mylock |      1 |           0 |
+----------+--------+--------+-------------+
1 row in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;步骤4：释放表锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UNLOCK TABLES; # 使用此命令解锁当前加锁的表
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; unlock tables;
Query OK, 0 rows affected (0.00 sec)

mysql&amp;gt; show open tables where in_use &amp;gt; 0;
Empty set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;步骤5：加读锁&lt;/p&gt;
&lt;p&gt;我们为mylock表加read锁（读阻塞写），观察阻塞的情况，流程如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-24_22-22-39.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;步骤6：加写锁&lt;/p&gt;
&lt;p&gt;为mylock表加write锁，观察阻塞的情况，流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-24_22-25-02.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-24_22-25-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MyISAM在执行查询语句（SELECT）前，会给涉及的所有表加&lt;code&gt;读锁&lt;/code&gt;，在执行增删改操作前，会给涉及的表加&lt;code&gt;写锁&lt;/code&gt;。InnoDB存储引擎是不会为这个表添加表级别的&lt;code&gt;读锁&lt;/code&gt;和写&lt;code&gt;锁的&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;MySQL的表级锁有两种模式：（以MyISAM表进行操作的演示）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表共享读锁（Table Read Lock）&lt;/li&gt;
&lt;li&gt;表独占写锁（Table Write Lock）&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;锁类型&lt;/th&gt;
&lt;th&gt;自己可读&lt;/th&gt;
&lt;th&gt;自己可写&lt;/th&gt;
&lt;th&gt;自己可操作其他表&lt;/th&gt;
&lt;th&gt;他人可读&lt;/th&gt;
&lt;th&gt;他人可写&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;读锁&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否，等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写锁&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否，等&lt;/td&gt;
&lt;td&gt;否，等&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;2. 意向锁（intention lock）&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;InnoDB&lt;/code&gt; 支持 &lt;code&gt;多粒度锁（multiple granularity locking）&lt;/code&gt; ，它允许 &lt;code&gt;行级锁&lt;/code&gt; 与 &lt;code&gt;表级锁&lt;/code&gt; 共存，而&lt;code&gt;意向锁&lt;/code&gt;就是其中的一种 &lt;code&gt;表锁&lt;/code&gt; 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;意向锁&lt;/code&gt;的存在是为了协调&lt;code&gt;行锁&lt;/code&gt;和&lt;code&gt;表锁&lt;/code&gt;的关系，支持多粒度（表锁和行锁）的锁并存。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;意向锁&lt;/code&gt;是一种&lt;code&gt;不与行级锁冲突的表级锁&lt;/code&gt;，这一点非常重要。&lt;/li&gt;
&lt;li&gt;表明“某个事务正在某些行持有了锁或该事务准备去持有锁”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;意向锁分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;意向共享锁（intention shared lock, IS）&lt;/strong&gt;：事务有意向对表中的某些行加共享锁（S锁）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;  -- 事务要获取某些行的 S 锁，必须先获得表的 IS 锁。
  SELECT column FROM table ... LOCK IN SHARE MODE;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;意向排他锁（intention exclusive lock, IX）&lt;/strong&gt;：事务有意向对表中的某些行加排他锁（X锁）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;  -- 事务要获取某些行的 X 锁，必须先获得表的 IX 锁。
  SELECT column FROM table ... FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即：意向锁是由存储引擎 &lt;code&gt;自己维护的&lt;/code&gt; ，用户无法手动操作意向锁，在为数据行加共享 / 排他锁之前， &lt;code&gt;InooDB&lt;/code&gt; 会先获取该数据行 &lt;code&gt;所在数据表的对应意向锁&lt;/code&gt; 。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;意向锁要解决的问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;现在有两个事务，分别是T1和T2，其中T2试图在该表级别上应用共享或排它锁，如果没有意向锁存在，那么T2就需要去检查各个页或行是否存在锁；如果存在意向锁，那么此时就会受到由T1控制的&lt;code&gt;表级别意向锁的阻塞&lt;/code&gt;。T2在锁定该表前不必检查各个页或行锁，而只需检查表上的意向锁。简单来说就是给更大一级别的空间示意里面是否已经上过锁。&lt;/p&gt;
&lt;p&gt;在数据表的场景中，&lt;strong&gt;如果我们给某一行数据加上了排它锁，数据库会自动给更大一级的空间，比如数据页或数据表加上意向锁，告诉其他人这个数据页或数据表已经有人上过排它锁了&lt;/strong&gt;，这样当其他人想要获取数据表排它锁的时候，只需要了解是否有人已经获取了这个数据表的意向排他锁即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果事务想要获得数据表中某些记录的共享锁，就需要在数据表上添加意向共享锁。&lt;/li&gt;
&lt;li&gt;如果事务想要获得数据表中某些记录的排他锁，就需要在数据表上添加意向排他锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;举例：&lt;/strong&gt; 创建表teacher,插入6条数据，事务的隔离级别默认为&lt;code&gt;Repeatable-Read&lt;/code&gt;，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `teacher` (
	`id` int NOT NULL,
    `name` varchar(255) NOT NULL,
    PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `teacher` VALUES
(&apos;1&apos;, &apos;zhangsan&apos;),
(&apos;2&apos;, &apos;lisi&apos;),
(&apos;3&apos;, &apos;wangwu&apos;),
(&apos;4&apos;, &apos;zhaoliu&apos;),
(&apos;5&apos;, &apos;songhongkang&apos;),
(&apos;6&apos;, &apos;leifengyang&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设事务A获取了某一行的排他锁，并未提交，语句如下所示:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BEGIN;

SELECT * FROM teacher WHERE id = 6 FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务B想要获取teacher表的表读锁，语句如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BEGIN;

LOCK TABLES teacher READ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为共享锁与排他锁互斥，所以事务 B 在试图对 teacher 表加共享锁的时候，必须保证两个条件。&lt;/p&gt;
&lt;p&gt;（1）当前没有其他事务持有 teacher 表的排他锁
（2）当前没有其他事务持有 teacher 表中任意一行的排他锁。&lt;/p&gt;
&lt;p&gt;为了检测是否满足第二个条件，事务 B 必须在确保 teacher 表不存在任何排他锁的前提下，去检测表中的每一行是否存在排他锁。很明显这是一个效率很差的做法，但是有了意向锁之后，情况就不一样了。&lt;/p&gt;
&lt;p&gt;意向锁是怎么解决这个问题的呢？首先，我们需要知道意向锁之间的兼容互斥性，如下所示。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;意向共享锁（IS）&lt;/th&gt;
&lt;th&gt;意向排他锁（IX）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;意向共享锁（IS）&lt;/td&gt;
&lt;td&gt;兼容&lt;/td&gt;
&lt;td&gt;兼容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;意向排他锁（IX）&lt;/td&gt;
&lt;td&gt;兼容&lt;/td&gt;
&lt;td&gt;兼容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;即意向锁之间是互相兼容的，虽然意向锁和自家兄弟互相兼容，但是它会与普通的排他/共享锁互斥。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;意向共享锁（IS）&lt;/th&gt;
&lt;th&gt;意向排他锁（IX）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;共享锁（S）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;兼容&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;互斥&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;排他锁（X）&lt;/td&gt;
&lt;td&gt;互斥&lt;/td&gt;
&lt;td&gt;互斥&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意这里的排他/共享锁指的都是表锁，意向锁不会与行级的共享/排他锁互斥。回到刚才 teacher 表的例子。&lt;/p&gt;
&lt;p&gt;事务 A 获取了某一行的排他锁，并未提交：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BEGIN;

SELECT * FROM teacher WHERE id = 6 FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时teacher表存在两把锁：teacher表上的意向排他锁 与 id为6的数据行上的排他锁。事务B想要获取teacher表的共享锁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BEGIN;

LOCK TABLES teacher READ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时事务B检测事务A持有teacher表的意向排他锁，就可以得知事务A必须持有该表中某些数据行的排他锁，那么事务B对teacher表的加锁请求就会被排斥（阻塞），而无需去检测表中的每一行数据是否存在排他锁。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;意向锁的并发性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;意向锁不会与行级的共享 / 排他锁互斥！正因为如此，意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性。（不然我们直接用普通的表锁就行了）&lt;/p&gt;
&lt;p&gt;我们扩展一下上面 teacher表的例子来概括一下意向锁的作用（一条数据从被锁定到被释放的过程中，可能存在多种不同锁，但是这里我们只着重表现意向锁）。&lt;/p&gt;
&lt;p&gt;事务A先获得了某一行的排他锁，并未提交：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BEGIN;

SELECT * FROM teacher WHERE id = 6 FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务A获取了teacher表上的意向排他锁。事务A获取了id为6的数据行上的排他锁。&lt;/p&gt;
&lt;p&gt;之后事务B想要获取teacher表上的共享锁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BEGIN;

LOCK TABLES teacher READ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务B检测到事务A持有teacher表的意向排他锁。事务B对teacher表的加锁请求被阻塞（排斥）。&lt;/p&gt;
&lt;p&gt;最后事务C也想获取teacher表中某一行的排他锁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BEGIN;

SELECT * FROM teacher WHERE id = 5 FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务C申请teacher表的意向排他锁。事务C检测到事务A持有teacher表的意向排他锁。因为意向锁之间并不互斥，所以事务C获取到了teacher表的意向排他锁。因为id为5的数据行上不存在任何排他锁，最终事务C成功获取到了该数据行上的排他锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;从上面的案例可以得到如下结论：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;InnoDB 支持 &lt;code&gt;多粒度锁&lt;/code&gt; ，特定场景下，行级锁可以与表级锁共存。&lt;/li&gt;
&lt;li&gt;意向锁之间互不排斥，但除了 IS 与 S 兼容外， 意向锁会与(表级) 共享锁 / 排他锁 互斥。&lt;/li&gt;
&lt;li&gt;IX，IS是表级锁，不会和行级的X，S锁发生冲突。只会和表级的X，S发生冲突。&lt;/li&gt;
&lt;li&gt;意向锁在保证并发性的前提下，实现了 &lt;code&gt;行锁和表锁共存&lt;/code&gt; 且 &lt;code&gt;满足事务隔离性&lt;/code&gt; 的要求。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;3. 自增锁（AUTO-INC锁）&lt;/h5&gt;
&lt;p&gt;在使用MySQL过程中，我们可以为表的某个列添加 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; 属性。举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `teacher` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于这个表的id字段声明了AUTO_INCREMENT，意味着在书写插入语句时不需要为其赋值，SQL语句修改 如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO `teacher` (name) VALUES (&apos;zhangsan&apos;), (&apos;lisi&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上边的插入语句并没有为id列显式赋值，所以系统会自动为它赋上递增的值，结果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; select * from teacher;
+----+----------+
| id | name     |
+----+----------+
| 1  | zhangsan |
| 2  | lisi     |
+----+----------+
2 rows in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们看到的上面插入数据只是一种简单的插入模式，所有插入数据的方式总共分为三类，分别是 “ &lt;code&gt;Simple inserts&lt;/code&gt; ”，“ &lt;code&gt;Bulk inserts&lt;/code&gt; ”和“ &lt;code&gt;Mixed-mode inserts&lt;/code&gt; ”。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;“Simple inserts”&lt;/code&gt; （简单插入）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以 &lt;code&gt;预先确定要插入的行数&lt;/code&gt; （当语句被初始处理时）的语句。包括没有嵌套子查询的单行和多行&lt;code&gt;INSERT...VALUES()&lt;/code&gt;和 &lt;code&gt;REPLACE&lt;/code&gt; 语句。比如我们上面举的例子就属于该类插入，已经确定要插入的行数。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;“Bulk inserts”&lt;/code&gt; （批量插入）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;事先不知道要插入的行数&lt;/code&gt; （和所需自动递增值的数量）的语句。比如 &lt;code&gt;INSERT ... SELECT&lt;/code&gt; ， &lt;code&gt;REPLACE ... SELECT&lt;/code&gt; 和 &lt;code&gt;LOAD DATA&lt;/code&gt; 语句，但不包括纯INSERT。 InnoDB在每处理一行，为&lt;code&gt;AUTO_INCREMENT&lt;/code&gt;列&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;“Mixed-mode inserts”&lt;/code&gt; （混合模式插入）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些是“Simple inserts”语句但是指定部分新行的自动递增值。例如 &lt;code&gt;INSERT INTO teacher (id,name) VALUES (1,&apos;a&apos;), (NULL,&apos;b&apos;), (5,&apos;c&apos;), (NULL,&apos;d&apos;);&lt;/code&gt; 只是指定了部分&lt;code&gt;id&lt;/code&gt;的值。另一种类型的“混合模式插入”是&lt;code&gt; INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;对于上面数据插入的案例，MySQL中采用了&lt;code&gt;自增锁&lt;/code&gt;的方式来实现，&lt;strong&gt;AUTO-INC锁是 当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁&lt;/strong&gt;，在执行插入语句时就在表级别加一个AUTO-INC锁，然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值，在该语句执行结束后，再把AUTO-INC锁释放掉。&lt;strong&gt;一个事务在持有AUTO-INC锁的过程中，其他事务的插入语句都要被阻塞&lt;/strong&gt;，可以保证一个语句中分配的递增值是连续的。也正因为此，其并发性显然并不高，&lt;strong&gt;当我们向一个有AUTO_INCREMENT关键字的主键插入值的时候，每条语句都要对这个表锁进行竞争&lt;/strong&gt;，这样的并发潜力其实是很低下的，所以innodb通过&lt;code&gt;innodb_autoinc_lock_mode&lt;/code&gt;的不同取值来提供不同的锁定机制，来显著提高SQL语句的可伸缩性和性能。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;innodb_autoinc_lock_mode&lt;/code&gt;有三种取值，分别对应与不同锁定模式：&lt;/p&gt;
&lt;p&gt;（1）&lt;code&gt;innodb_autoinc_lock_mode = 0&lt;/code&gt;(“传统”锁定模式 )&lt;/p&gt;
&lt;p&gt;在此锁定模式下，所有类型的insert语句都会获得一个特殊的表级AUTO-INC锁，用于插入具有 AUTO_INCREMENT列的表。这种模式其实就如我们上面的例子，即每当执行insert的时候，都会得到一个 表级锁(AUTO-INC锁)，使得语句中生成的auto_increment为顺序，且在binlog中重放的时候，可以保证 master与slave中数据的auto_increment是相同的。因为是表级锁，当在同一时间多个事务中执行insert的 时候，对于AUTO-INC锁的争夺会 &lt;code&gt;限制并发&lt;/code&gt; 能力。&lt;/p&gt;
&lt;p&gt;（2）&lt;code&gt;innodb_autoinc_lock_mode = 1&lt;/code&gt;(“连续”锁定模式 )&lt;/p&gt;
&lt;p&gt;在 MySQL 8.0 之前，连续锁定模式是 &lt;code&gt;默认&lt;/code&gt; 的。&lt;/p&gt;
&lt;p&gt;在这个模式下，“bulk inserts”仍然使用AUTO-INC表级锁，并保持到语句结束。这适用于所有INSERT ... SELECT，REPLACE ... SELECT和LOAD DATA语句。同一时刻只有一个语句可以持有AUTO-INC锁。&lt;/p&gt;
&lt;p&gt;对于“Simple inserts”（要插入的行数事先已知），则通过在 &lt;code&gt;mutex（轻量锁）&lt;/code&gt; 的控制下获得所需数量的自动递增值来避免表级AUTO-INC锁， 它只在分配过程的持续时间内保持，而不是直到语句完成。不使用表级AUTO-INC锁，除非AUTO-INC锁由另一个事务保持。如果另一个事务保持AUTO-INC锁，则“Simple inserts”等待AUTO-INC锁，如同它是一个“bulk inserts”。&lt;/p&gt;
&lt;p&gt;（3）&lt;code&gt;innodb_autoinc_lock_mode = 2&lt;/code&gt;(“交错”锁定模式 )&lt;/p&gt;
&lt;p&gt;从 MySQL 8.0 开始，交错锁模式是 &lt;code&gt;默认&lt;/code&gt; 设置。&lt;/p&gt;
&lt;p&gt;在此锁定模式下，自动递增值 &lt;code&gt;保证&lt;/code&gt; 在所有并发执行的所有类型的insert语句中是 &lt;code&gt;唯一&lt;/code&gt; 且 &lt;code&gt;单调递增&lt;/code&gt; 的。但是，由于多个语句可以同时生成数字（即，跨语句交叉编号），&lt;strong&gt;为任何给定语句插入的行生成的值可能不是连续的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果执行的语句是“simple inserts&quot;，其中要插入的行数已提前知道，除了&quot;Mixed-mode inserts&quot;之外，为单个语句生成的数字不会有间隙。然后，当执行&quot;bulk inserts&quot;时，在由任何给定语句分配的自动递增值中可能存在间隙。&lt;/p&gt;
&lt;p&gt;:::tip
&lt;code&gt;INSERT INTO teacher (name) SELECT ...&lt;/code&gt;：把查询出来的数据批量插入到另一张表&lt;/p&gt;
&lt;p&gt;&lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt;：如果已存在就更新，否则就插入，避免主键冲突&lt;/p&gt;
&lt;p&gt;&lt;code&gt;REPLACE ... SELECT&lt;/code&gt;: 从一张表中查询数据,并将其插入到目标表中;如果主键或唯一键冲突，先删除冲突行，再插入新行&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LOAD DATA&lt;/code&gt;： 用于高效地将文本文件（通常是 CSV/TSV、制表符分隔文件等）导入到表中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOAD DATA [LOCAL] INFILE &apos;file_path&apos;
INTO TABLE 目标表
[CHARACTER SET 文件字符集]
FIELDS TERMINATED BY &apos;分隔符&apos;
[OPTIONALLY] ENCLOSED BY &apos;引用符&apos;
LINES TERMINATED BY &apos;行结束符&apos;
IGNORE n LINES
(col1, col2, ..., colN)
SET colX = expr, …;

 -- LOCAL：若文件在客户端机器上，需加 LOCAL；否则省略表示服务器端文件。
 -- FIELDS TERMINATED BY：字段之间的分隔符，默认为制表符 \t。
 -- ENCLOSED BY：字段两侧包裹字符，比如 CSV 常用双引号 &quot;。
 -- LINES TERMINATED BY：行结束符，Unix 下一般 \n。
 -- IGNORE n LINES：忽略前几行（比如跳过表头）。
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对比项&lt;/th&gt;
&lt;th&gt;REPLACE ... SELECT&lt;/th&gt;
&lt;th&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;冲突时行为&lt;/td&gt;
&lt;td&gt;删除 + 插入&lt;/td&gt;
&lt;td&gt;更新指定字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否保留原记录其他字段&lt;/td&gt;
&lt;td&gt;❌ 否（会被清空）&lt;/td&gt;
&lt;td&gt;✅ 是&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h5&gt;4. 元数据锁（MDL锁）&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;MySQL5.5&lt;/code&gt;引入了&lt;code&gt;meta data lock&lt;/code&gt;，简称MDL锁，属于表锁范畴。MDL 的作用是，保证读写的正确性。比如，如果一个查询正在遍历一个表中的数据，而执行期间另一个线程对这个&lt;code&gt;表结构做变更&lt;/code&gt; ，增加了一 列，那么查询线程拿到的结果跟表结构对不上，肯定是不行的。&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;当对一个表做增删改查操作的时候，加 MDL读锁；当要对表做结构变更操作的时候，加 MDL 写锁&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;读锁之间不互斥，因此你可以有多个线程同时对一张表增删查改。读写锁之间、写锁之间都是互斥的，用来保证变更表结构操作的安全性，解决了DML和DDL操作之间的一致性问题。&lt;code&gt;不需要显式使用&lt;/code&gt;，在访问一个表的时候会被自动加上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例：元数据锁的使用场景模拟&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;会话A：&lt;/strong&gt; 从表中查询数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql&amp;gt; SELECT COUNT(1) FROM teacher;
+----------+
| COUNT(1) |
+----------+
| 2        |
+----------+
1 row int set (7.46 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;会话B：&lt;/strong&gt; 修改表结构，增加新列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql&amp;gt; alter table teacher add age int not null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;会话C：&lt;/strong&gt; 查看当前MySQL的进程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show processlist;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show processlist;
+----+------------------+-----------+---------+---------+------+-----------------------------+------------------------------+
| Id | User             | Host      | db      | Command | Time | State                       | Info                         |
+----+------------------+-----------+---------+---------+------+-----------------------------+------------------------------+
| 5  | event_scheduler  | localhost | NULL    | Daemon  | 8205 | Waiting on empty queue      | NULL                         |
| 8  | root             | localhost | atguigudb1 | Sleep   | 46   |                             | NULL                         |
| 9  | root             | localhost | atguigudb1 | Query   | 24   | Waiting for table metadata lock | alter table teacher add age int |
| 13 | root             | localhost | NULL    | Query   | 0    | init                        | show processlist             |
+----+------------------+-----------+---------+---------+------+-----------------------------+------------------------------+
4 rows in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过会话C可以看出会话B被阻塞，这是由于会话A拿到了teacher表的&lt;code&gt;元数据读锁&lt;/code&gt;，会话B想申请teacher表的&lt;code&gt;元数据写锁&lt;/code&gt;，由于读写锁互斥，会话B需要等待会话A释放元数据锁才能执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;元数据锁可能带来的问题&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Session A&lt;/th&gt;
&lt;th&gt;Session B&lt;/th&gt;
&lt;th&gt;Session C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;begin;select * from teacher;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;alter table teacher add age int;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;select * from teacher;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我们可以看到 session A 会对表 teacher 加一个 MDL 读锁，之后 session B 要加 MDL 写锁会被 blocked，因为 session A 的 MDL 读锁还没有释放，而 session C 要在表 teacher 上新申请 MDL 读锁的请求也会被 session B 阻塞。前面我们说了，所有对表的增删改查操作都需要先申请 MDL 读锁，就都被阻塞，等于这个表现在完全不可读写了。&lt;/p&gt;
&lt;h4&gt;2. InnoDB中的行锁&lt;/h4&gt;
&lt;p&gt;行锁（Row Lock）也称为记录锁，顾名思义，就是锁住某一行（某条记录 row）。需要注意的是，MySQL服务器层并没有实现行锁机制，&lt;strong&gt;行级锁只在存储引擎层实现&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt; 锁定力度小，发生&lt;code&gt;锁冲突概率低&lt;/code&gt;，可以实现的&lt;code&gt;并发度高&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt; 对于&lt;code&gt;锁的开销比较大&lt;/code&gt;，加锁会比较慢，容易出现&lt;code&gt;死锁&lt;/code&gt;情况。&lt;/p&gt;
&lt;p&gt;InnoDB与MyISAM的最大不同有两点：一是支持事务（TRANSACTION）；二是采用了行级锁。&lt;/p&gt;
&lt;p&gt;首先我们创建表如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE student (
	id INT,
    name VARCHAR(20),
    class VARCHAR(10),
    PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;向这个表里插入几条记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO student VALUES
(1, &apos;张三&apos;, &apos;一班&apos;),
(3, &apos;李四&apos;, &apos;一班&apos;),
(8, &apos;王五&apos;, &apos;二班&apos;),
(15, &apos;赵六&apos;, &apos;二班&apos;),
(20, &apos;钱七&apos;, &apos;三班&apos;);

mysql&amp;gt; SELECT * FROM student;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; select * from student;
+----+--------+-------+
| id | name   | class |
+----+--------+-------+
| 1  | 张三   | 一班  |
| 3  | 李四   | 一班  |
| 8  | 王五   | 二班  |
| 15 | 赵六   | 二班  |
| 20 | 钱七   | 三班  |
+----+--------+-------+
5 rows in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;student表中的聚簇索引的简图如下所示。&lt;/p&gt;
&lt;p&gt;聚簇索引示意图
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-25_20-46-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里把B+树的索引结构做了超级简化，只把索引中的记录给拿了出来，下面看看都有哪些常用的行锁类型。&lt;/p&gt;
&lt;h5&gt;记录锁（Record Locks）&lt;/h5&gt;
&lt;p&gt;记录锁也就是仅仅把一条记录锁，官方的类型名称为：&lt;code&gt;LOCK_REC_NOT_GAP&lt;/code&gt;。比如我们把id值为8的那条记录加一个记录锁的示意图如果所示。仅仅是锁住了id值为8的记录，对周围的数据没有影响。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-25_20-49-08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;举例如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-25_20-49-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;记录锁是有S锁和X锁之分的，称之为 &lt;code&gt;S型记录锁&lt;/code&gt; 和 &lt;code&gt;X型记录锁&lt;/code&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当一个事务获取了一条记录的S型记录锁后，其他事务也可以继续获取该记录的S型记录锁，但不可以继续获取X型记录锁；&lt;/li&gt;
&lt;li&gt;当一个事务获取了一条记录的X型记录锁后，其他事务既不可以继续获取该记录的S型记录锁，也不可以继续获取X型记录锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;间隙锁（Gap Locks）&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;MySQL&lt;/code&gt; 在 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 隔离级别下是可以解决幻读问题的，解决方案有两种，可以使用 &lt;code&gt;MVCC&lt;/code&gt; 方案解决，也可以采用 &lt;code&gt;加锁&lt;/code&gt; 方案解决。但是在使用加锁方案解决时有个大问题，就是事务在第一次执行读取操作时，那些幻影记录尚不存在，我们无法给这些 &lt;code&gt;幻影记录&lt;/code&gt; 加上 &lt;code&gt;记录锁&lt;/code&gt; 。InnoDB提出了一种称之为 &lt;code&gt;Gap Locks&lt;/code&gt; 的锁，官方的类型名称为： &lt;code&gt;LOCK_GAP&lt;/code&gt; ，我们可以简称为 &lt;code&gt;gap锁&lt;/code&gt; 。比如，把id值为8的那条 记录加一个gap锁的示意图如下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-25_20-54-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中id值为8的记录加了gap锁，意味着 &lt;code&gt;不允许别的事务在id值为8的记录前边的间隙插入新记录&lt;/code&gt; ，其实就是 id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如，有另外一个事务再想插入一条id值为4的新 记录，它定位到该条新记录的下一条记录的id值为8，而这条记录上又有一个gap锁，所以就会阻塞插入 操作，直到拥有这个gap锁的事务提交了之后，id列的值在区间(3, 8)中的新记录才可以被插入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;gap锁的提出仅仅是为了防止插入幻影记录而提出的。&lt;/strong&gt; 虽然有&lt;code&gt;共享gap锁&lt;/code&gt;和&lt;code&gt;独占gap锁&lt;/code&gt;这样的说法，但是它们起到的作用是相同的。而且如果对一条记录加了gap锁（不论是共享gap锁还是独占gap锁），并不会限制其他事务对这条记录加记录锁或者继续加gap锁（间隙锁与其他间隙锁是兼容的，不会互相阻塞。多个事务可以在同一个间隙上加“间隙锁”，它们之间是兼容的）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Session1&lt;/th&gt;
&lt;th&gt;Session2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;select * from student where id=5 lock in share mode;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;select * from student where id=5 for update;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里session2并不会被堵住。因为表里并没有id=5这条记录，因此session1加的是间隙锁(3,8)。而session2也是在这个间隙加的间隙锁。它们有共同的目标，即：保护这个间隙锁，不允许插入值。但，它们之间是不冲突的。&lt;/p&gt;
&lt;p&gt;注意，给一条记录加了 &lt;code&gt;gap锁&lt;/code&gt; 只是 &lt;code&gt;不允许&lt;/code&gt; 其他事务往这条记录前边的间隙 &lt;code&gt;插入新记录&lt;/code&gt; ，那对于最后一条记录之后的间隙，也就是student表中id值为&lt;code&gt;20&lt;/code&gt;的记录之后的间隙该咋办呢？也就是说给哪条记录加&lt;code&gt;gap锁&lt;/code&gt; 才能阻止其他事务插入&lt;code&gt;id&lt;/code&gt;值在&lt;code&gt;(20，+∞)&lt;/code&gt;这个区间的新记录呢？这时候我们在讲数据页时介绍的两条伪记录派上用场了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Infimum&lt;/code&gt;记录，表示该页面中最小的记录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Supremun&lt;/code&gt;记录，表示该页面中最大的记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了实现阻止其他事务插入id值再&lt;code&gt;(20,正无穷)&lt;/code&gt;这个区间的新纪录，我们可以给索引中的最后一条记录，也就是id值为20的那条记录所在页面的&lt;code&gt;Supremun记录&lt;/code&gt;加上一个&lt;code&gt;gap锁&lt;/code&gt;，如图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/restore_07e7c628731c488aaebe30881e08d74d_1753448365%20%281%29.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; select * from student where id &amp;gt; 20 lock in share mode;
Empty set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140176306603936:1182:140176189910224
ENGINE_TRANSACTION_ID: 110977
THREAD_ID: 54
EVENT_ID: 45
OBJECT_SCHEMA: atguigudb3
OBJECT_NAME: student
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140176189910224
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140176306602224:1182:140176189898032
ENGINE_TRANSACTION_ID: 421651283312880
THREAD_ID: 51
EVENT_ID: 109
OBJECT_SCHEMA: atguigudb3
OBJECT_NAME: student
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140176189898032
LOCK_TYPE: TABLE
LOCK_MODE: IS
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 3. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140176306602224:121:4:1:140176189894928
ENGINE_TRANSACTION_ID: 421651283312880
THREAD_ID: 51
EVENT_ID: 109
OBJECT_SCHEMA: atguigudb3
OBJECT_NAME: student
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140176189894928
LOCK_TYPE: RECORD
LOCK_MODE: S
LOCK_STATUS: GRANTED
LOCK_DATA: supremum pseudo-record
3 rows in set (0.00 sec)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到 &lt;code&gt;supremum pseudo-record&lt;/code&gt;字样
这样就可以阻止其他事务插入id值在&lt;code&gt;(20,+∞)&lt;/code&gt;这个区间的新记录。&lt;/p&gt;
&lt;p&gt;间隙锁的引入，可能会导致同样的语句锁住更大的范围，这其实是影响了并发度的。下面的例子会产生&lt;code&gt;死锁&lt;/code&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Session1&lt;/th&gt;
&lt;th&gt;Session2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;begin; &amp;lt;br/&amp;gt; select * from student where id=5 lock in share mode;&lt;/td&gt;
&lt;td&gt;begin; &amp;lt;br/&amp;gt; select * from student where id=5 for update;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;INTER INTO student VALUES (5,&apos;小明&apos;,&apos;二班&apos;);  阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INTER INTO student VALUES (5,&apos;小明&apos;,&apos;二班&apos;);   &amp;lt;br/&amp;gt;    (ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;（1）session 1 执行 select … for update 语句，由于 id = 5 这一行并不存在，因此会加上间隙锁（3，8）;&lt;/p&gt;
&lt;p&gt;（2）session 2 执行 select … for update 语句，同样会加上间隙锁（3，8），间隙锁之间不会冲突，因此这个语句可以执行成功；&lt;/p&gt;
&lt;p&gt;（3）session 2 试图插入一行(5, &apos;小明&apos;, &apos;二班&apos;)，被 session 1 的间隙锁挡住了，只好进入等待；&lt;/p&gt;
&lt;p&gt;（4）session 1 试图插入一行(5, &apos;小明&apos;, &apos;二班&apos;)，被 session 2 的间隙锁挡住了。至此，两个 session 进入互相等待状态，形成&lt;em&gt;死锁&lt;/em&gt;。当然，InnoDB 的死锁检测马上就发现了这对死锁关系，让 session 1 的 insert 语句报错返回。&lt;/p&gt;
&lt;p&gt;:::tip
&lt;code&gt;Infimum&lt;/code&gt;(in fai men) 和 &lt;code&gt;Supremum&lt;/code&gt;(se pu rui men) 是两个虚拟记录（伪记录），它们并不是你自己插入的数据，而是 每个 InnoDB 数据页（Data Page）中自动存在的特殊记录，用于维持页内记录的有序性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Infimum&lt;/code&gt; 表示页中最小的值，&lt;code&gt;Supremum&lt;/code&gt; 表示页中最大的值，用来做页内记录排序的边界哨兵。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;InnoDB&lt;/code&gt; 把表数据按 &lt;code&gt;B+ 树&lt;/code&gt; 组织，底层是按页（Page）存储的, 每个数据页中，记录按主键顺序排列。&lt;/p&gt;
&lt;p&gt;而为了高效地比较和插入记录的位置，InnoDB 每个数据页会默认包含两个特殊记录：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;伪记录&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Infimum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;极小值&lt;/td&gt;
&lt;td&gt;小于该页中所有数据记录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Supremum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;极大值&lt;/td&gt;
&lt;td&gt;大于该页中所有数据记录&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;什么时候会加间隙锁？&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;只在 REPEATABLE READ（可重复读）隔离级别下自动加间隙锁&lt;/p&gt;
&lt;p&gt;SQL 语句必须涉及 索引 （尤其是范围查询）&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; 或 &lt;code&gt;SELECT ... LOCK IN SHARE MODE&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UPDATE&lt;/code&gt; 或 &lt;code&gt;DELETE&lt;/code&gt; 语句&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;临键锁（Next—Key Locks）&lt;/h5&gt;
&lt;p&gt;有时候我们既想 &lt;code&gt;锁住某条记录&lt;/code&gt; ，又想 &lt;code&gt;阻止&lt;/code&gt; 其他事务在该记录前边的 &lt;code&gt;间隙插入新记录&lt;/code&gt; ，所以InnoDB就提 出了一种称之为 &lt;code&gt;Next-Key Locks&lt;/code&gt; 的锁，官方的类型名称为： &lt;code&gt;LOCK_ORDINARY&lt;/code&gt; ，我们也可以简称为 &lt;code&gt;next-key锁&lt;/code&gt; 。Next-Key Locks是在存储引擎 &lt;code&gt;innodb&lt;/code&gt; 、事务级别在 &lt;code&gt;可重复读&lt;/code&gt; 的情况下使用的数据库锁， &lt;strong&gt;innodb默认的锁就是Next-Key locks&lt;/strong&gt;。比如，我们把id值为8的那条记录加一个next-key锁的示意图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-25_22-40-14.png&quot; alt=&quot;&quot; /&gt;
&lt;code&gt;next-key锁&lt;/code&gt;的本质就是一个&lt;code&gt;记录锁&lt;/code&gt;和一个&lt;code&gt;gap锁&lt;/code&gt;的合体，它既能保护该条记录，又能阻止别的事务将新记录插入被保护记录前边的&lt;code&gt;间隙&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;begin;
select * from student where id &amp;lt;=8 and id &amp;gt; 3 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;插入意向锁（Insert Intention Locks）&lt;/h5&gt;
&lt;p&gt;我们说一个事务在&lt;code&gt;插入&lt;/code&gt;一条记录时需要判断一下插入位置是不是被别的事务加了 &lt;code&gt;gap 锁&lt;/code&gt;（&lt;code&gt;next-key 锁&lt;/code&gt;也包含 &lt;code&gt;gap 锁&lt;/code&gt;），如果有的话，插入操作需要等待，直到拥有 &lt;code&gt;gap 锁&lt;/code&gt;的那个事务提交。但是 &lt;strong&gt;InnoDB 规定事务在等待的时候也需要在内存中生成一个锁结构&lt;/strong&gt;，表明有事务想在某个&lt;code&gt;间隙&lt;/code&gt;中&lt;code&gt;插入&lt;/code&gt;新记录，但是现在在等待。InnoDB 就把这种类型的锁命名为 &lt;code&gt;Insert Intention Locks&lt;/code&gt;，官方的类型名称为：&lt;code&gt;LOCK_INSERT_INTENTION&lt;/code&gt;，我们称为&lt;code&gt;插入意向锁&lt;/code&gt;。插入意向锁是一种 &lt;code&gt;Gap 锁&lt;/code&gt;，不是意向锁，在 insert 操作时产生。&lt;/p&gt;
&lt;p&gt;插入意向锁是在插入一条记录行前，由 &lt;code&gt;INSERT 操作产生的一种间隙锁&lt;/code&gt;。该锁用以表示插入意向，当多个事务在同一区间（gap）插入位置不同的多条数据时，事务之间不需要互相等待。假设存在两条值分别为 4 和 7 的记录，两个不同的事务分别试图插入值为 5 和 6 的两条记录，每个事务在获取插入行上独占的（排他）锁前，都会获取（4，7）之间的间隙锁，但是因为数据行之间并&lt;code&gt;不冲突&lt;/code&gt;，所以两个事务之间并不会产生冲突（阻塞等待）。总结来说，插入意向锁的特性可以分成两部分：&lt;/p&gt;
&lt;p&gt;（1）插入意向锁是一种&lt;code&gt;特殊的间隙锁&lt;/code&gt; —— 间隙锁可以锁定开区间内的部分记录。&lt;/p&gt;
&lt;p&gt;（2）插入意向锁之间&lt;code&gt;互不排斥&lt;/code&gt;，所以即使多个事务在同一区间插入多条记录，只要记录本身（主键、唯一索引）不冲突，那么事务之间就不会出现冲突等待。&lt;/p&gt;
&lt;p&gt;注意，虽然插入意向锁中含有意向锁三个字，但是它并不属于意向锁而属于间隙锁，因为意向锁是表锁 而插入意向锁是&lt;code&gt;行锁&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;比如，把 id 值为 8 的那条记录加一个插入意向锁的示意图如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-25_22-44-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比如，现在 T1 为 id 值为 8 的记录加了一个 gap 锁，然后 T2 和 T3 分别想向 student 表中插入 id 值分别为 4、5 的两条记录，所以现在为 id 值为 8 的记录加的锁的示意图就如下所示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/restore_be604441cd554588b6eab11f7af712ed_1753454787.jpg&quot; alt=&quot;&quot; /&gt;
从图中可以看到，由于T1持有gap锁，所以T2和T3需要生成一个插入意向锁的锁结构并且处于等待状态。当T1提交后会把它获取到的锁都释放掉，这样T2和T3就能获取到对应的插入意向锁了（本质上就是把插入意向锁对应锁结构的is_waiting属性改为false），T2和T3之间也并不会相互阻塞，它们可以同时获取到id值为8的插入意向锁，然后执行插入操作。事实上&lt;strong&gt;插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;3. 页锁&lt;/h4&gt;
&lt;p&gt;页锁就是在 &lt;code&gt;页的粒度&lt;/code&gt; 上进行锁定，锁定的数据资源比行锁要多，因为一个页中可以有多个行记录。当我们使用页锁的时候，会出现数据浪费的现象，但这样的浪费最多也就是一个页上的数据行。&lt;strong&gt;页锁的开销介于表锁和行锁之间，会出现死锁。锁定粒度介于表锁和行锁之间，并发度一般&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;每个层级的锁数量是有限制的，因为锁会占用内存空间， &lt;code&gt;锁空间的大小是有限的&lt;/code&gt; 。当某个层级的锁数量 超过了这个层级的阈值时，就会进行 &lt;code&gt;锁升级&lt;/code&gt; 。锁升级就是用更大粒度的锁替代多个更小粒度的锁，比如 InnoDB 中行锁升级为表锁，这样做的好处是占用的锁空间降低了，但同时数据的并发度也下降了。&lt;/p&gt;
&lt;h3&gt;从对待锁的态度划分:乐观锁、悲观锁&lt;/h3&gt;
&lt;h4&gt;悲观锁&lt;/h4&gt;
&lt;p&gt;从对待锁的态度来看锁的话，可以将锁分成乐观锁和悲观锁，从名字中也可以看出这两种锁是两种看待 &lt;code&gt;数据并发的思维方式&lt;/code&gt; 。需要注意的是，乐观锁和悲观锁并不是锁，而是锁的 &lt;code&gt;设计思想&lt;/code&gt; 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;悲观锁（Pessimistic Locking）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;悲观锁是一种思想，顾名思义，就是很悲观，对数据被其他事务的修改持保守态度，会通过数据库自身的锁机制来实现，从而保证数据操作的排它性。&lt;/p&gt;
&lt;p&gt;悲观锁总是假设最坏的情况，每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这样别人想拿这个数据就会 阻塞 直到它拿到锁（&lt;strong&gt;共享资源每次只给一个线程使用，其它线程阻塞， 用完后再把资源转让给其它线程&lt;/strong&gt;）。比如行锁，表锁等，读锁，写锁等，都是在做操作之前先上锁，当其他线程想要访问数据时，都需要阻塞挂起。Java中 &lt;code&gt;synchronized&lt;/code&gt; 和 &lt;code&gt;ReentrantLock&lt;/code&gt; 等独占锁就是悲观锁思想的实现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;秒杀案例1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;商品秒杀过程中，库存数量的减少，避免出现超卖的情况。比如，商品表中有一个字段为 quantity 表示当前该商品的库存量。假设商品为华为 mate40，id 为 1001，quantity = 100 个。如果不使用锁的情况下，操作方法如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#第1步：查出商品库存
select quantity from items where id = 1001;
#第2步：如果库存大于0，则根据商品信息生产订单
insert into orders (item_id) values(1001);
#第3步：修改商品的库存，num表示购买数量
update items set quantity = quantity - num where id = 1001;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样写的话，在并发量小的公司没有大的问题，但是如果在&lt;code&gt;高并发环境&lt;/code&gt;下可能出现以下问题&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;线程A&lt;/th&gt;
&lt;th&gt;线程B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;step1（查询还有100部手机）&lt;/td&gt;
&lt;td&gt;step1（查询还有100部手机）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;step2（生成订单）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;step2（生成订单）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;step3（减库存1）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;step3（减库存2）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中线程B此时已经下单并且减完库存，这个时候线程A依然去执行step3，就造成了超卖。&lt;/p&gt;
&lt;p&gt;我们使用悲观锁可以解决这个问题，商品信息从查询出来到修改，中间有一个生成订单的过程，使用悲观锁的原理就是，当我们在查询items信息后就把当前的数据锁定，直到我们修改完毕后再解锁。那么整个过程中，因为数据被锁定了，就不会出现有第三者来对其进行修改了。而这样做的前提是&amp;lt;font style=&quot;color:red&quot;&amp;gt;需要将要执行的SQL语句放在同一个事务中，否则达不到锁定数据行的目的&amp;lt;/font&amp;gt;。&lt;/p&gt;
&lt;p&gt;修改如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#第1步：查出商品库存
select quantity from items where id = 1001 for update;
#第2步：如果库存大于0，则根据商品信息生产订单
insert into orders (item_id) values(1001);
#第3步：修改商品的库存，num表示购买数量
update items set quantity = quantity-num where id = 1001;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;select .... for update&lt;/code&gt; 是MySQL中悲观锁。此时在items表中，id为1001的那条数据就被我们锁定了，其他的要执行select quantity from items where id = 1001 for update;语句的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。&lt;/p&gt;
&lt;p&gt;注意，当执行select quantity from items where id = 1001 for update;语句之后，如果在其他事务中执行select quantity from items where id = 1001; 语句，并不会受第一个事务的影响，仍然可以正常查询出数据。&lt;/p&gt;
&lt;p&gt;注意：&amp;lt;font style=&quot;color:red&quot;&amp;gt;select ... for update语句执行过程中所有扫描的行都会被锁上，因此在MySQL中用悲观锁必须确定使用了索引，而不是全表扫描，否则将会把整个表锁住&amp;lt;/font&amp;gt;。&lt;/p&gt;
&lt;p&gt;悲观锁不适用的场景较多，它存在一些不足，因为悲观锁大多数情况下依靠数据库的锁机制来实现，以保证程序的并发访问性，同时这样对数据库性能开销影响也很大，特别是&lt;code&gt;长事务&lt;/code&gt;而言，这样的&lt;code&gt;开销往往无法承受&lt;/code&gt;，这时就需要乐观锁。&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在使用悲观锁（如 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;）时，必须确保查询是基于索引的，这样可以避免全表扫描和性能问题。有索引的话，只会锁住扫描到的符合条件的行。&lt;/li&gt;
&lt;li&gt;如果没有索引，查询会进行全表扫描并锁住所有相关行，这样可能导致锁表，并影响系统的并发性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果没有索引，表锁会加在整个表上，可能导致不必要的性能损耗。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h5&gt;select是否加锁&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;不加锁的情况（快照读）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在&lt;code&gt;READ COMMITTED&lt;/code&gt;和&lt;code&gt;REPEATABLE READ&lt;/code&gt;隔离级别下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 这些SELECT语句都不加锁
SELECT * FROM users WHERE id = 1;
SELECT * FROM orders WHERE user_id = 123;
SELECT COUNT(*) FROM products;
SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id;

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;原理：使用MVCC机制，读取数据的历史版本（快照），不会阻塞其他事务的写操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;实际测试验证&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 会话1：开启事务并修改数据
BEGIN;
UPDATE users SET name = &apos;New Name&apos; WHERE id = 1;
-- 注意：不要COMMIT

-- 会话2：同时查询（不会被阻塞）
SELECT * FROM users WHERE id = 1;  -- 立即返回旧数据
--  不会等待，直接返回修改前的数据

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;加锁的情况&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A. &lt;code&gt;SERIALIZABLE&lt;/code&gt;隔离级别下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 设置最高隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 现在普通SELECT也会加锁
SELECT * FROM users WHERE id = 1;
-- 🔒 自动加共享锁（S锁）

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;B. 显式锁定读语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 排他锁（写锁）
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 🔒 加排他锁，其他事务无法读写此行

-- 共享锁（读锁）
SELECT * FROM users WHERE id = 1 FOR SHARE;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- 旧语法
-- 🔒 加共享锁，其他事务可以读但不能写

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不同存储引擎的表现&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InnoDB&lt;/code&gt;存储引擎（默认），普通SELECT不加锁，使用&lt;code&gt;MVCC&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MyISAM&lt;/code&gt;存储引擎，MyISAM只支持表级锁，读操作不加锁，但写操作会锁整个表&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;乐观锁&lt;/h4&gt;
&lt;p&gt;乐观锁认为对同一数据的并发操作不会总发生，属于小概率事件，不用每次都对数据上锁，但是在更新的时候会判断一下在此期间别人有没有去更新这个数据，也就是不采用数据库自身的锁机制，而是通过程序来实现。在程序上，我们可以采用 &lt;code&gt;版本号机制&lt;/code&gt; 或者 &lt;code&gt;CAS机制&lt;/code&gt; 实现。乐观锁适用于多读的应用类型， 这样可以提高吞吐量。在Java中 &lt;code&gt;java.util.concurrent.atomic&lt;/code&gt; 包下的原子变量类就是使用了乐观锁的一种实现方式：CAS实现的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;乐观锁的版本号机制&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在表中设计一个 &lt;code&gt;版本字段 version&lt;/code&gt; ，第一次读的时候，会获取 &lt;code&gt;version&lt;/code&gt; 字段的取值。然后对数据进行更新或删除操作时，会执行 &lt;code&gt;UPDATE ... SET version=version+1 WHERE version=version&lt;/code&gt; 。此时 如果已经有事务对这条数据进行了更改，修改就不会成功。&lt;/p&gt;
&lt;p&gt;这种方式类似我们熟悉的SVN、CVS版本管理系统，当我们修改了代码进行提交时，首先会检查当前版本号与服务器上的版本号是否一致，如果一致就可以直接提交，如果不一致就需要更新服务器上的最新代码，然后再进行提交。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;乐观锁的时间戳机制&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;时间戳和版本号机制一样，也是在更新提交的时候，将当前数据的时间戳和更新之前取得的时间戳进行 比较，如果两者一致则更新成功，否则就是版本冲突。&lt;/p&gt;
&lt;p&gt;你能看到乐观锁就是程序员自己控制数据并发操作的权限，基本是通过给数据行增加一个戳（版本号或 者时间戳），从而证明当前拿到的数据是否最新。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;时间戳机制&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;SELECT id, status, timestamp FROM orders WHERE id = 1;

UPDATE orders SET status = &apos;completed&apos;, timestamp = NOW() WHERE id = 1 AND timestamp = #{timestamp};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;秒杀案例2&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;依然使用上面秒杀的案例，执行流程如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#第1步：查出商品库存
select quantity from items where id = 1001;
#第2步：如果库存大于0，则根据商品信息生产订单
insert into orders (item_id) values(1001);
#第3步：修改商品的库存，num表示购买数量
update items set quantity = quantity - num, version = version + 1 where id = 1001 and version = #{version};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，如果数据表是读写分离的表，当matser表中写入的数据没有及时同步到slave表中时，会造成更新一直失败的问题。此时需要强制读取master表中的数据（即将select语句放到事务中即可，这时候查询的就是master主库了。）&lt;/p&gt;
&lt;p&gt;如果我们对同一条数据进行频繁的修改的话，那么就会出现这么一种场景，每次修改都只有一个事务能更新成功，在业务感知上面就有大量的失败操作。我们把代码修改如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#第1步：查出商品库存
select quantity from items where id = 1001;
#第2步：如果库存大于0，则根据商品信息生产订单
insert into orders (item_id) values(1001);
#第3步：修改商品的库存，num表示购买数量
update items set quantity = quantity - num where id = 1001 and quantity - num &amp;gt; 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就会使每次修改都能成功，而且不会出现超卖的现象。&lt;/p&gt;
&lt;h4&gt;两种锁的适用场景&lt;/h4&gt;
&lt;p&gt;从这两种锁的设计思想中，我们总结一下乐观锁和悲观锁的适用场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;乐观锁 适合 &lt;code&gt;读操作多&lt;/code&gt; 的场景，相对来说写的操作比较少。它的优点在于 &lt;code&gt;程序实现&lt;/code&gt; ， &lt;code&gt;不存在死锁&lt;/code&gt; 问题，不过适用场景也会相对乐观，因为它阻止不了除了程序以外的数据库操作。&lt;/li&gt;
&lt;li&gt;悲观锁 适合 &lt;code&gt;写操作多&lt;/code&gt; 的场景，因为写的操作具有 &lt;code&gt;排它性&lt;/code&gt; 。采用悲观锁的方式，可以在数据库层 面阻止其他事务对该数据的操作权限，防止 &lt;code&gt;读 - 写&lt;/code&gt; 和 &lt;code&gt;写 - 写&lt;/code&gt; 的冲突。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-26_00-17-09.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;按加锁的方式划分：显式锁、隐式锁&lt;/h3&gt;
&lt;h4&gt;隐式锁&lt;/h4&gt;
&lt;p&gt;一个事务在执行 &lt;code&gt;INSERT&lt;/code&gt; 操作时，如果即将插入的间隙已经被其他事务加了 &lt;code&gt;gap&lt;/code&gt; 锁，那么本次 &lt;code&gt;INSERT&lt;/code&gt; 操作会阻塞，并且当前事务会在该间隙上加一个&lt;code&gt;插入意向锁&lt;/code&gt;，否则一般情况下 &lt;code&gt;INSERT&lt;/code&gt; 操作是不加锁的。那如果一个事务首先插入了一条记录（此时并没有在内存生产与该记录关联的锁结构），然后另一个事务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;立即使用 &lt;code&gt;SELECT... LOCK IN SHARE MODE&lt;/code&gt; 语句读取这条记录，也就是要获取这条记录的 &lt;code&gt;S 锁&lt;/code&gt;，或者使用 &lt;code&gt;SELECT... FOR UPDATE&lt;/code&gt; 语句读取这条记录，也就是要获取这条记录的 &lt;code&gt;X 锁&lt;/code&gt;，怎么办？
如果允许这种情况的发生，那么可能产生&lt;code&gt;脏读&lt;/code&gt;问题。&lt;/li&gt;
&lt;li&gt;立即修改这条记录，也就是要获取这条记录的 &lt;code&gt;X 锁&lt;/code&gt;，怎么办？
如果允许这种情况的发生，那么可能产生&lt;code&gt;脏写&lt;/code&gt;问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候我们前边提过的&lt;code&gt;事务 id&lt;/code&gt; 又要起作用了。我们把聚簇索引和二级索引中的记录分开看一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;情景一：对于聚簇索引记录来说，有一个 &lt;code&gt;trx_id&lt;/code&gt; 隐藏列，该隐藏列记录着最后改动该记录的 &lt;code&gt;事务 id&lt;/code&gt; 。那么如果在当前事务中新插入一条聚簇索引记录后，该记录的 &lt;code&gt;trx_id&lt;/code&gt; 隐藏列代表的的就是 当前事务的 &lt;code&gt;事务id&lt;/code&gt; ，如果其他事务此时想对该记录添加 &lt;code&gt;S锁&lt;/code&gt; 或者 &lt;code&gt;X锁&lt;/code&gt; 时，首先会看一下该记录的 &lt;code&gt;trx_id&lt;/code&gt; 隐藏列代表的事务是否是当前的活跃事务，如果是的话，那么就帮助当前事务创建一个 &lt;code&gt;X 锁&lt;/code&gt; （也就是为当前事务创建一个锁结构， &lt;code&gt;is_waiting&lt;/code&gt; 属性是 &lt;code&gt;false&lt;/code&gt; ），然后自己进入等待状态 （也就是为自己也创建一个锁结构， &lt;code&gt;is_waiting&lt;/code&gt; 属性是 &lt;code&gt;true&lt;/code&gt; ）。&lt;/li&gt;
&lt;li&gt;情景二：对于二级索引记录来说，本身并没有 &lt;code&gt;trx_id&lt;/code&gt; 隐藏列，但是在二级索引页面的 &lt;code&gt;Page Header&lt;/code&gt; 部分有一个 &lt;code&gt;PAGE_MAX_TRX_ID&lt;/code&gt; 属性，该属性代表对该页面做改动的最大的 &lt;code&gt;事务id&lt;/code&gt; ，如果 &lt;code&gt;PAGE_MAX_TRX_ID&lt;/code&gt; 属性值小于当前最小的活跃 &lt;code&gt;事务id&lt;/code&gt; ，那么说明对该页面做修改的事务都已经提交了，否则就需要在页面中定位到对应的二级索引记录，然后回表找到它对应的聚簇索引记录，然后再重复 &lt;code&gt;情景一&lt;/code&gt; 的做法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即：一个事务对新插入的记录可以不显式的加锁（生成一个锁结构），但是由于&lt;code&gt;事务id&lt;/code&gt;的存在，相当于加了一个&lt;code&gt;隐式锁&lt;/code&gt;。别的事务在对这条记录加&lt;code&gt;S锁&lt;/code&gt;或者&lt;code&gt;X锁&lt;/code&gt;时，由于&lt;code&gt;隐式锁&lt;/code&gt;的存在，会先帮助当前事务生成一个锁结构，然后自己再生成一个锁结构后进入等待状态。隐式锁是一种&lt;code&gt;延迟加锁&lt;/code&gt;的机制，从而来减少加锁的数量。&lt;/p&gt;
&lt;p&gt;隐式锁在实际内存对象中并不含有这个锁信息。只有当产生锁等待时，隐式锁转化为显式锁。&lt;/p&gt;
&lt;p&gt;InnoDB的insert操作，对插入的记录不加锁，但是此时如果另一个线程进行当前读，类似以下的用例，session 2会锁等待session 1，那么这是如何实现的呢?&lt;/p&gt;
&lt;p&gt;session 1:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; begin;
Query OK, 0 rows affected (0.00 sec)
mysql&amp;gt; insert INTO student VALUES(34,&quot;周八&quot;,&quot;二班&quot;);
Query OK, 1 row affected (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;session 2:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; begin;
Query OK, 0 rows affected (0.00 sec)
mysql&amp;gt; select * from student lock in share mode; #执行完，当前事务被阻塞
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;session 3:&lt;/p&gt;
&lt;p&gt;执行下述语句，输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SELECT * FROM performance_schema.data_lock_waits\G;
*************************** 1. row ***************************
						ENGINE: INNODB
		REQUESTING_ENGINE_LOCK_ID: 140562531358232:7:4:9:140562535668584
REQUESTING_ENGINE_TRANSACTION_ID: 422037508068888
			REQUESTING_THREAD_ID: 64
			REQUESTING_EVENT_ID: 6
REQUESTING_OBJECT_INSTANCE_BEGIN: 140562535668584
		BLOCKING_ENGINE_LOCK_ID: 140562531351768:7:4:9:140562535619104
BLOCKING_ENGINE_TRANSACTION_ID: 15902
			BLOCKING_THREAD_ID: 64
			BLOCKING_EVENT_ID: 6
BLOCKING_OBJECT_INSTANCE_BEGIN: 140562535619104
1 row in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;隐式锁的逻辑过程如下：&lt;/p&gt;
&lt;p&gt;A. InnoDB的每条记录中都一个隐含的trx_id字段，这个字段存在于聚簇索引的B+Tree中。&lt;/p&gt;
&lt;p&gt;B. 在操作一条记录前，首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活动的事务，首先将 隐式锁 转换为 显式锁 (就是为该事务添加一个锁)。&lt;/p&gt;
&lt;p&gt;C. 检查是否有锁冲突，如果有冲突，创建锁，并设置为waiting状态。如果没有冲突不加锁，跳到E。&lt;/p&gt;
&lt;p&gt;D. 等待加锁成功，被唤醒，或者超时。&lt;/p&gt;
&lt;p&gt;E. 写数据，并将自己的trx_id写入trx_id字段。&lt;/p&gt;
&lt;h4&gt;显式锁&lt;/h4&gt;
&lt;p&gt;通过特定的语句进行加锁，我们一般称之为显示加锁，例如：&lt;/p&gt;
&lt;p&gt;显示加共享锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select .... lock in share mode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显示加排它锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select .... for update
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;其它锁之：全局锁&lt;/h3&gt;
&lt;p&gt;全局锁就是对 &lt;code&gt;整个数据库实例&lt;/code&gt; 加锁。当你需要让整个库处于 &lt;code&gt;只读状态&lt;/code&gt; 的时候，可以使用这个命令，之后 其他线程的以下语句会被阻塞：数据更新语句（数据的增删改）、数据定义语句（包括建表、修改表结构等）和更新类事务的提交语句。全局锁的典型使用&lt;code&gt;场景&lt;/code&gt; 是：做&lt;code&gt;全库逻辑备份&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;全局锁的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Flush tables with read lock
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;其它锁之：死锁&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;概念&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;两个事务都持有对方需要的锁，并且在等待对方释放，并且双方都不会释放自己的锁。&lt;/p&gt;
&lt;p&gt;举例1：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;事务1&lt;/th&gt;
&lt;th&gt;事务2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;start transaction;&amp;lt;br/&amp;gt;update account set money=100 where id=1;&lt;/td&gt;
&lt;td&gt;start transaction;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;update account set money=100 where id=2;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;update account set money=200 where id=2;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;update account set money=200 where id=1;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;举例2：&lt;/p&gt;
&lt;p&gt;用户A给用户B转账100，再次同时，用户B也给用户A转账100。这个过程，可能导致死锁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#事务1
update account set balance = balance - 100 where name = &apos;A&apos;;  #操作1
update account set balance = balance + 100 where name = &apos;B&apos;;  #操作3
#事务2
update account set balance = balance - 100 where name = &apos;B&apos;;  #操作2
update account set balance = balance + 100 where name = &apos;A&apos;;  #操作4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-04_21-41-51.png &quot;
style=&quot;width: 100%; max-width: 300px;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;产生死锁的必要条件&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;两个或者两个以上事务&lt;/li&gt;
&lt;li&gt;每个事务都已经持有锁并且申请新的锁&lt;/li&gt;
&lt;li&gt;锁资源同时只能被同一个事务持有或者不兼容&lt;/li&gt;
&lt;li&gt;事务之间因为持有锁和申请锁导致彼此循环等待&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;死锁的关键在于：两个（或以上）的Session加锁的顺序不一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;如何处理死锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式1：&lt;/strong&gt; 等待，直到超时 (innodb_lock_wait_timeout=50s)
即当两个事务互相等待时，当一个事务等待时间超过设置的阈值时，就将其回滚，另外事务继续进行。这种方法简单有效，在innodb中，参数&lt;code&gt;innodb_lock_wait_timeout&lt;/code&gt;用来设置超时时间。&lt;/p&gt;
&lt;p&gt;缺点：对于在线服务来说，这个等待时间往往是无法接受的。&lt;/p&gt;
&lt;p&gt;那将此值修改短一些，比如1s，0.1s是否合适？不合适，容易误伤到普通的锁等待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式2：&lt;/strong&gt; 使用死锁检测处理死锁程序
方式1检测死锁太过被动，innodb还提供了&lt;code&gt;wait-for graph&lt;/code&gt;算法来主动进行死锁检测，每当加锁请求无法立即满足需要并进入等待时，&lt;code&gt;wait-for graph&lt;/code&gt;算法都会被触发。&lt;/p&gt;
&lt;p&gt;这是一种较为&lt;code&gt;主动的死锁检测机制&lt;/code&gt;，要求数据库保存&lt;code&gt;锁的信息链表&lt;/code&gt;和&lt;code&gt;事务等待链表&lt;/code&gt;两部分信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-04_21-52-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;基于这两个信息，可以绘制wait-for graph（等待图）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-04_21-52-23.png&quot; style=&quot;max-width:300px;&quot;  /&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;死锁检测的原理是构建一个以事务为顶点，锁为边的有向图，判断有向图是否存在环，存在既有死锁。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一旦检测到回路、有死锁，这时候InnoDB存储引擎会选择&lt;code&gt;回滚undo量最小的事务&lt;/code&gt;，让其他事务继续执行（&lt;code&gt;innodb_deadlock_detect=on&lt;/code&gt;表示开启这个逻辑）。&lt;/p&gt;
&lt;p&gt;缺点：每个新的被阻塞的线程，都要判断是不是由于自己的加入导致了死锁，这个操作时间复杂度是O(n)。如果100个并发线程同时更新同一行，意味着要检测100*100=1万次，1万个线程就会有1千万次检测。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何解决？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;方式1：关闭死锁检测，但意味着可能会出现大量的超时，会导致业务有损。&lt;/p&gt;
&lt;p&gt;方式2：控制并发访问的数量。比如在中间件中实现对于相同行的更新，在进入引擎之前排队，这样在InnoDB内部就不会有大量的死锁检测工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;进一步的思路：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以考虑通过将一行改成逻辑上的多行来减少锁冲突。比如，连锁超市账户总额的记录，可以考虑放到多条记录上。账户总额等于这多个记录的值的总和。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何避免死锁&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;合理设计索引，使业务 &lt;code&gt;SQL&lt;/code&gt; 尽可能通过索引定位更少的行，减少锁竞争。&lt;/li&gt;
&lt;li&gt;调整业务逻辑 &lt;code&gt;SQL&lt;/code&gt; 执行顺序，避免 &lt;code&gt;update/delete&lt;/code&gt; 长时间持有锁的 &lt;code&gt;SQL&lt;/code&gt; 在事务前面。&lt;/li&gt;
&lt;li&gt;避免大事务，尽量将大事务拆成多个小事务来处理，小事务缩短锁定资源的时间，发生锁冲突的几率也更小。&lt;/li&gt;
&lt;li&gt;在并发比较高的系统中，不要显式加锁，特别是在事务里显式加锁。如 &lt;code&gt;select … for update&lt;/code&gt; 语句，如果是在事务里运行了 &lt;code&gt;start transaction&lt;/code&gt; 或设置了 &lt;code&gt;autocommit 等于 0&lt;/code&gt;，那么就会锁定所查找到的记录。&lt;/li&gt;
&lt;li&gt;降低隔离级别。如果业务允许，将隔离级别调低也是较好的选择，比如将隔离级别从 &lt;code&gt;RR&lt;/code&gt; 调整为 &lt;code&gt;RC&lt;/code&gt;，可以避免掉很多因为 &lt;code&gt;gap 锁&lt;/code&gt;造成的死锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;锁的内部结构&lt;/h3&gt;
&lt;p&gt;我们前边说对一条记录加锁的本质就是在内存中创建一个锁结构与之关联，那么是不是一个事务对多条记录加锁，就要创建多个锁结构呢？比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 事务T1
SELECT * FROM user LOCK IN SHARE MODE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理论上创建多个&lt;code&gt;锁结构&lt;/code&gt;没问题，但是如果一个事务要获取10000条记录的锁，生成10000个锁结构也太崩溃了！所以决定在对不同记录加锁时，如果符合下边这些条件的记录会放在一个&lt;code&gt;锁结构&lt;/code&gt;中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在同一个事务中进行加锁操作&lt;/li&gt;
&lt;li&gt;被加锁的记录在同一个页面中&lt;/li&gt;
&lt;li&gt;加锁的类型是一样的&lt;/li&gt;
&lt;li&gt;等待状态是一样的
&lt;code&gt;InnoDB&lt;/code&gt; 存储引擎中的 &lt;code&gt;锁结构&lt;/code&gt; 如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-04_22-51-24.png&quot; alt=&quot;&quot; /&gt;
结构解析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;锁所在的事务信息&lt;/strong&gt; ：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不论是 &lt;code&gt;表锁&lt;/code&gt; 还是 &lt;code&gt;行锁&lt;/code&gt; ，都是在事务执行过程中生成的，哪个事务生成了这个锁结构 ，这里就记录这个 事务的信息。&lt;/p&gt;
&lt;p&gt;此 &lt;code&gt;锁所在的事务信息&lt;/code&gt; 在内存结构中只是一个指针，通过指针可以找到内存中关于该事务的更多信息，比方说事务id等。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;索引信息&lt;/strong&gt; ：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于 &lt;code&gt;行锁&lt;/code&gt; 来说，需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;表锁／行锁信息&lt;/strong&gt; ：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;表锁结构&lt;/code&gt; 和 &lt;code&gt;行锁结构&lt;/code&gt; 在这个位置的内容是不同的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;表锁：
记载着是对哪个表加的锁，还有其他的一些信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;行锁：
记载了三个重要的信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Space ID&lt;/code&gt; ：记录所在表空间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Page Number&lt;/code&gt; ：记录所在页号。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n_bits&lt;/code&gt; ：对于行锁来说，一条记录就对应着一个比特位，一个页面中包含很多记录，用不同 的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位，这个&lt;code&gt;n_bis&lt;/code&gt;属性代表使用了多少比特位。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;n_bits&lt;/code&gt;的值一般都比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后 也不至于重新分配锁结构&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;type_mode&lt;/strong&gt; ：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这是一个32位的数，被分成了 &lt;code&gt;lock_mode&lt;/code&gt; 、 &lt;code&gt;lock_type&lt;/code&gt; 和 &lt;code&gt;rec_lock_type&lt;/code&gt; 三个部分，如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-08-04_22-53-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁的模式（ lock_mode ），占用低4位，可选的值如下：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LOCK_IS&lt;/code&gt; （十进制的 0 ）：表示共享意向锁，也就是 &lt;code&gt;IS锁&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOCK_IX&lt;/code&gt; （十进制的 1 ）：表示独占意向锁，也就是 &lt;code&gt;IX锁&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOCK_S&lt;/code&gt; （十进制的 2 ）：表示共享锁，也就是 &lt;code&gt;S锁&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOCK_X&lt;/code&gt; （十进制的 3 ）：表示独占锁，也就是 &lt;code&gt;X锁&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOCK_AUTO_INC&lt;/code&gt; （十进制的 4 ）：表示 &lt;code&gt;AUTO-INC锁&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在InnoDB存储引擎中，&lt;code&gt;LOCK_IS&lt;/code&gt;，&lt;code&gt;LOCK_IX&lt;/code&gt;，&lt;code&gt;LOCK_AUTO_INC&lt;/code&gt;都算是表级锁的模式，&lt;code&gt;LOCK_S&lt;/code&gt;和 &lt;code&gt;LOCK_X&lt;/code&gt;既可以算是表级锁的模式，也可以是行级锁的模式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;锁的类型（ lock_type ），占用第5～8位，不过现阶段只有第5位和第6位被使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LOCK_TABLE&lt;/code&gt; （十进制的 16 ），也就是当第5个比特位置为1时，表示表级锁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOCK_REC&lt;/code&gt; （十进制的 32 ），也就是当第6个比特位置为1时，表示行级锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;行锁的具体类型（ rec_lock_type ），使用其余的位来表示。只有在 &lt;code&gt;lock_type&lt;/code&gt; 的值为 &lt;code&gt;LOCK_REC&lt;/code&gt; 时，也就是只有在该锁为行级锁时，才会被细分为更多的类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LOCK_ORDINARY&lt;/code&gt; （十进制的 0 ）：表示 next-key锁 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOCK_GAP&lt;/code&gt; （十进制的 512 ）：也就是当第10个比特位置为1时，表示 &lt;code&gt;gap锁&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOCK_REC_NOT_GAP&lt;/code&gt; （十进制的 1024 ）：也就是当第11个比特位置为1时，表示正经 &lt;code&gt;记录锁&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOCK_INSERT_INTENTION&lt;/code&gt; （十进制的 2048 ）：也就是当第12个比特位置为1时，表示插入意向锁。其他的类型：还有一些不常用的类型我们就不多说了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;is_waiting&lt;/code&gt; 属性呢？基于内存空间的节省，所以把 &lt;code&gt;is_waiting&lt;/code&gt; 属性放到了 &lt;code&gt;type_mode&lt;/code&gt; 这个32 位的数字中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LOCK_WAIT&lt;/code&gt; （十进制的 256 ） ：当第9个比特位置为 1 时，表示 &lt;code&gt;is_waiting&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; ，也 就是当前事务尚未获取到锁，处在等待状态；当这个比特位为 0 时，表示 &lt;code&gt;is_waiting&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt; ，也就是当前事务获取锁成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;其他信息&lt;/strong&gt; ：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一堆比特位&lt;/strong&gt; ：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果是 &lt;code&gt;行锁结构&lt;/code&gt; 的话，在该结构末尾还放置了一堆比特位，比特位的数量是由上边提到的 &lt;code&gt;n_bits&lt;/code&gt; 属性 表示的。InnoDB数据页中的每条记录在 记录头信息 中都包含一个 &lt;code&gt;heap_no&lt;/code&gt; 属性，伪记录 &lt;code&gt;Infimum&lt;/code&gt; 的 &lt;code&gt;heap_no&lt;/code&gt; 值为 0 ， &lt;code&gt;Supremum&lt;/code&gt; 的 &lt;code&gt;heap_no&lt;/code&gt; 值为 1 ，之后每插入一条记录， &lt;code&gt;heap_no&lt;/code&gt; 值就增1。 锁结 构 最后的一堆比特位就对应着一个页面中的记录，一个比特位映射一个 &lt;code&gt;heap_no&lt;/code&gt; ，即一个比特位映射 到页内的一条记录。&lt;/p&gt;
&lt;h3&gt;锁监控&lt;/h3&gt;
&lt;p&gt;关于MySQL锁的监控，我们一般可以通过检查 InnoDB_row_lock 等状态变量来分析系统上的行锁的争夺情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show status like &apos;innodb_row_lock%&apos;;
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 0     |
| Innodb_row_lock_time_avg      | 0     |
| Innodb_row_lock_time_max      | 0     |
| Innodb_row_lock_waits         | 0     |
+-------------------------------+-------+
5 rows in set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对各个状态量的说明如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Innodb_row_lock_current_waits：当前正在等待锁定的数量；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Innodb_row_lock_time&lt;/code&gt; ：从系统启动到现在锁定总时间长度；（等待总时长）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Innodb_row_lock_time_avg&lt;/code&gt; ：每次等待所花平均时间；（等待平均时长）&lt;/li&gt;
&lt;li&gt;Innodb_row_lock_time_max：从系统启动到现在等待最常的一次所花的时间；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Innodb_row_lock_waits&lt;/code&gt; ：系统启动后到现在总共等待的次数；（等待总次数）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于这5个状态变量，比较重要的3个见上面（灰色）。&lt;/p&gt;
&lt;p&gt;尤其是当等待次数很高，而且每次等待时长也不小的时候，我们就需要分析系统中为什么会有如此多的等待，然后根据分析结果着手指定优化计划。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其他监控方法：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MySQL把事务和锁的信息记录在了 &lt;code&gt;information_schema&lt;/code&gt; 库中，涉及到的三张表分别是 &lt;code&gt;INNODB_TRX&lt;/code&gt; 、 &lt;code&gt;INNODB_LOCKS&lt;/code&gt; 和 &lt;code&gt;INNODB_LOCK_WAITS&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;MySQL5.7及之前 ，可以通过&lt;code&gt;information_schema.INNODB_LOCKS&lt;/code&gt;查看事务的锁情况，但只能看到阻塞事务的锁；如果事务并未被阻塞，则在该表中看不到该事务的锁情况。&lt;/p&gt;
&lt;p&gt;MySQL8.0删除了&lt;code&gt;information_schema.INNODB_LOCKS&lt;/code&gt;，添加了 &lt;code&gt;performance_schema.data_locks&lt;/code&gt; ，可以通过&lt;code&gt;performance_schema.data_locks&lt;/code&gt;查看事务的锁情况，和MySQL5.7及之前不同， &lt;code&gt;performance_schema.data_locks&lt;/code&gt;不但可以看到阻塞该事务的锁，还可以看到该事务所持有的锁。&lt;/p&gt;
&lt;p&gt;同时，information_schema.INNODB_LOCK_WAITS也被 &lt;code&gt;performance_schema.data_lock_waits&lt;/code&gt; 所代替。&lt;/p&gt;
&lt;p&gt;我们模拟一个锁等待的场景，以下是从这三张表收集的信息&lt;/p&gt;
&lt;p&gt;锁等待场景，我们依然使用记录锁中的案例，当事务2进行等待时，查询情况如下：&lt;/p&gt;
&lt;p&gt;（1）查询正在被锁阻塞的sql语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM information_schema.INNODB_TRX\G;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重要属性代表含义已在上述中标注。&lt;/p&gt;
&lt;p&gt;（2）查询锁等待情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM data_lock_waits\G;
*************************** 1. row ***************************
							ENGINE: INNODB
		REQUESTING_ENGINE_LOCK_ID: 139750145405624:7:4:7:139747028690608
REQUESTING_ENGINE_TRANSACTION_ID: 13845 #被阻塞的事务ID
			REQUESTING_THREAD_ID: 72
			REQUESTING_EVENT_ID: 26
REQUESTING_OBJECT_INSTANCE_BEGIN: 139747028690608
		BLOCKING_ENGINE_LOCK_ID: 139750145406432:7:4:7:139747028813248
BLOCKING_ENGINE_TRANSACTION_ID: 13844 #正在执行的事务ID，阻塞了13845
			BLOCKING_THREAD_ID: 71
			BLOCKING_EVENT_ID: 24
BLOCKING_OBJECT_INSTANCE_BEGIN: 139747028813248
1 row in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（3）查询锁的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql &amp;gt; SELECT * from performance_schema.data_locks\G;
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 139750145405624:1068:139747028693520
ENGINE_TRANSACTION_ID: 13847
THREAD_ID: 72
EVENT_ID: 31
OBJECT_SCHEMA: atguigu
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139747028693520
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 139750145405624:7:4:7:139747028690608
ENGINE_TRANSACTION_ID: 13847
THREAD_ID: 72
EVENT_ID: 31
OBJECT_SCHEMA: atguigu
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 139747028690608
LOCK_TYPE: RECORD
LOCK_MODE: X,REC_NOT_GAP
LOCK_STATUS: WAITING
LOCK_DATA: 1
*************************** 3. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 139750145406432:1068:139747028816304
ENGINE_TRANSACTION_ID: 13846
THREAD_ID: 71
EVENT_ID: 28
OBJECT_SCHEMA: atguigu
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139747028816304
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 4. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 139750145406432:7:4:7:139747028813248
ENGINE_TRANSACTION_ID: 13846
THREAD_ID: 71
EVENT_ID: 28
OBJECT_SCHEMA: atguigu
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 139747028813248
LOCK_TYPE: RECORD
LOCK_MODE: X,REC_NOT_GAP
LOCK_STATUS: GRANTED
LOCK_DATA: 1
4 rows in set (0.00 sec)

ERROR:
No query specified
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从锁的情况可以看出来，两个事务分别获取了IX锁，我们从意向锁章节可以知道，IX锁互相时兼容的。所以这里不会等待，但是事务1同样持有X锁，此时事务2也要去同一行记录获取X锁，他们之间不兼容，导致等待的情况发生。&lt;/p&gt;
</content:encoded></item><item><title>MySQL - 事务</title><link>https://zzyang.top/posts/mysql-transaction/</link><guid isPermaLink="true">https://zzyang.top/posts/mysql-transaction/</guid><pubDate>Thu, 10 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;事务&lt;/h2&gt;
&lt;h3&gt;简介&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;事务&lt;/strong&gt; 是一组操作的集合，它是一个不可分割的工作单位，事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求，即这些操作&lt;strong&gt;要么同时成功，要么同时失败&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-16_20-17-45.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;默认MySQL的事务是自动提交的，也就是说，当执行一条DML(增删改)语句，MySQL会立即隐式的提交事务，这意味着如果有一条DML语句执行失败，整个事务会被回滚。&lt;/p&gt;
&lt;p&gt;查看/设置事务提交方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 1自动提交 0手动提交
select @@autocommit;

SET @@autocommit = 0;

-- 提交事务
COMMIT;

-- 回滚事务
ROLLBACK;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启事务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;START TRANSACTION;

BEGIN;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;四大特性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;原子性(Atomicity): 事务是不可分割的最小操作单元，要么全部成功，要么全部失败。&lt;/li&gt;
&lt;li&gt;一致性(Consistency): 事务完成时，数据库从一个一致的状态转变为另一个一致的状态。这意味着事务 在执行前后，不能破坏数据库数据的完整性和一致性，必须使所有的数据都保持一致状态。&lt;/li&gt;
&lt;li&gt;隔离性(solation): 数据库系统提供的隔离机制，多个事务可以同时在数据库中执行，但它们之间应该是相互隔离的，一个事务的执行 不应该影响其他事务的执行，保证事务在不受外部并发操作影响的 独立环境下运行&lt;/li&gt;
&lt;li&gt;持久性(Durability): 事务一旦提交或回滚，它对数据库中的数据的改变就是永久的。即使系统发生故障或重启，也应该能够保持数据的持久性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;并发事务问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;脏读（Dirty Read）： 脏读发生在一个事务读取了另一个事务尚未提交的数据。如果这个事务最终回滚，读取到的数据就是无效的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法： 设置事务隔离级别，使用更高的隔离级别，如 READ COMMITTED， REPEATABLE READ，SERIALIZABLE 可以避免脏读。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不可重复读（Non-Repeatable Read）： 不可重复读发生在一个事务内，先后读取同一条记录，但两个相同查询读取的数据不同，因为在两次查询之间，另一个事务修改了数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法： 设置隔离级别，如：REPEATABLE READ， Serializable，或者使用锁定机制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;幻读（Phantom Read）： 幻读 发生在一个事务内的两个相同条件查询返回了不同的结果，因为在两次查询之间，另一个事务插入或删除了数据，导致结果集发生变化，好像出现了‘幻影’。一个事务按照条件查询数据时，没有对应的数据行，但是在插入数据时，又发现这行数据已经存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法： 设置隔离级别Serializable，通过强制事务串行化执行来避免幻读，或者使用锁定机制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;丢失更新（Lost Update）： 丢失更新发生在两个事务同时尝试更新相同数据，但只有一个更新生效，导致另一个更新的结果丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法： 使用锁定机制，如悲观锁或乐观锁，确保同时只有一个事务可以更新数据。&lt;/p&gt;
&lt;h3&gt;事务隔离级别&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;隔离级别&lt;/th&gt;
&lt;th&gt;脏读&lt;/th&gt;
&lt;th&gt;不可重复读&lt;/th&gt;
&lt;th&gt;幻读&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;READ-UNCOMMITTED&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ-COMMITTED&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE-READ&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;事务隔离级别越严格，数据越安全，但数据库效率越低。 MySQL 默认的事务隔离级别是：&lt;code&gt;REPEATABLE-READ&lt;/code&gt;可重复读级别，简称 &lt;code&gt;RR&lt;/code&gt; 级别，会出现幻读问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 查看事务隔离级别
SELECT @@TRANSACTION_ISOLATION;

-- 设置事务隔离级别
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ-UNCOMMITTED|READ-COMMITTED|REPEATABLE-READ|SERIALIZABLE];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;READ UNCOMMITTED（读未提交）&lt;/code&gt;： 允许一个事务读取另一个事务未提交的数据。这是最低的隔离级别，不提供任何隔离保护。 可能出现脏读、不可重复读和幻读的问题。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;READ COMMITTED（读已提交）&lt;/code&gt;： 允许一个事务只能读取其他事务已经提交的数据，看不到其他未提交事务的修改。 可能出现不可重复读和幻读的问题，但解决了脏读的问题。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;REPEATABLE READ（可重复读）&lt;/code&gt;： 保证一个事务在其生命周期内多次读取相同的数据，将返回相同的结果，即使其他事务已经修改了数据。其他事务的插入操作将被阻止。 可能出现幻读的问题，但解决了脏读和不可重复读的问题。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SERIALIZABLE（串行化）&lt;/code&gt;： 提供最高的隔离级别，确保事务之间完全隔离。事务按顺序执行，所有事务都像是按照顺序串行执行的，没有并发。避免了脏读、不可重复读和幻读的问题，但会降低并发性能。 虽然能保证数据的一致性，但可能会导致大量的事务等待，降低了系统的吞吐量和性能。&lt;/p&gt;
&lt;h2&gt;存储引擎&lt;/h2&gt;
&lt;h3&gt;MySQL体系结构&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-16_23-23-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;连接层
最上层是一些客户端和链接服务，主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务层
第二层架构主要完成大多数的核心服务功能，如SQL接口，并完成缓存的查询，SQL的分析和优化，部分内置函数的执行。所有跨存储引擎的功能也在这一层实现，如 过程、函数等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引擎层
存储引擎真正的负责了MySQL中数据的存储和提取，服务器通过API和存储引擎进行通信。不同的存储引擎具有不同的功能，这样我们可以根据自己的需要，来选取合适的存储引擎。存储引擎层负责管理数据存储、&lt;strong&gt;索引&lt;/strong&gt;实现和管理、并发控制和事务处理。它与底层文件系统交互，以高效地读取和写入数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;存储层
主要是将数据存储在文件系统之上，并完成与存储引擎的交互。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;存储引擎简介&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表的，而不是基于库的，所以存储引擎也可被称为表类型。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mysql5.5&lt;/code&gt;版本之后，默认的存储引擎为 &lt;code&gt;InnoDB&lt;/code&gt;，之前的版本默认的存储引擎为 &lt;code&gt;MyISAM&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;查看当前数据库支持的存储引擎&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW ENGINES;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;InnoDB&lt;/h3&gt;
&lt;p&gt;InnoDB 是 MySQL 5.5 版本之后默认的存储引擎，它支持事务处理、行级锁定和外键完整性。InnoDB 是一个高性能的存储引擎，它在性能和并发方面都有很大的优势。&lt;/p&gt;
&lt;p&gt;特点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DML操作遵循ACID模型，支持&lt;strong&gt;事务&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行级锁&lt;/strong&gt;，提高并发访问性能;&lt;/li&gt;
&lt;li&gt;支持&lt;strong&gt;外键 FOREIGN KEY&lt;/strong&gt;约束，保证数据的完整性和正确性;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xxx.ibd&lt;/code&gt;:xxx代表的是表名，innoDB引擎的每张表都会对应这样一个表空间文件，存储该表的表结构(frm、mysql8之后sdi)、数据和索引;&lt;/p&gt;
&lt;p&gt;参数:innodb_file_per_table&lt;/p&gt;
&lt;p&gt;:::info
mysql8.0默认使用的是innodb_file_per_table=ON，表示每个表对应一个表空间文件，而mysql5.7默认使用的是innodb_file_per_table=OFF，表示所有表共用一个表空间文件。
:::&lt;/p&gt;
&lt;p&gt;查看表空间文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ibd2sdi account

-- 在该ibd文件目录下cmd，命令： idb2sdi 表名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;InnoDB逻辑存储结构
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-16_23-58-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;区(Extent)：一个区大小是固定的，为1M，一个区可以包含64个页。&lt;/li&gt;
&lt;li&gt;页(Page)：InnoDB存储引擎将数据存储在页中，大小也是固定的，页的大小为16KB，即65536字节。页里面即使row数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MyISAM&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MyISAM&lt;/code&gt;是&lt;code&gt;MySQL&lt;/code&gt;早期的默认存储引擎。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不支持事务，不支持外键&lt;/li&gt;
&lt;li&gt;支持表锁，不支持行锁&lt;/li&gt;
&lt;li&gt;访问速度快&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在磁盘中所涉及到的文件有&lt;code&gt;xxxx.MYD&lt;/code&gt;,&lt;code&gt;xxxx.MYI&lt;/code&gt;和&lt;code&gt;xxxx.SDI&lt;/code&gt;三个文件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xxxx.MYD&lt;/code&gt;表中存放的数据，&lt;code&gt;xxxx.MYI&lt;/code&gt;文件存储表的索引信息，&lt;code&gt;xxxx.SDI&lt;/code&gt;表结构信息。&lt;/p&gt;
&lt;h3&gt;Memory&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Memory&lt;/code&gt;引擎的表数据时存储在内存中的，由于受到硬件问题、或断电问题的影响，只能将这些表作为临时表或缓存使用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存存放&lt;/li&gt;
&lt;li&gt;hash索引(默认)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xxx.sdi&lt;/code&gt;:存储表结构信息&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;区别&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-17_21-02-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;存储引擎选择&lt;/h3&gt;
&lt;p&gt;在选择存储引擎时，应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统，还可以根据实际情况选择多种存储引擎进行组合。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InnoDB&lt;/code&gt;:是Mysql的默认存储引擎，支持事务、外键。如果应用对事务的完整性有比较高的要求，在并发条件下要求数据的一致性，数据操作除了插入和查询之外，还包含很多的更新、删除操作，那么InnoDB存储引擎是比较合适的选择。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MyISAM&lt;/code&gt;:如果应用是以读操作和插入操作为主，只有很少的更新和删除操作，并且对事务的完整性、并发性要求不是很高，那么选择这个存储引擎是非常合适的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MEMORY&lt;/code&gt;:将所有数据保存在内存中，访问速度快，通常用于临时表及缓存。MEMORY的缺陷就是对表的大小有限制，太大的表无法缓存在内存中，而且无法保障数据的安全性。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Docker</title><link>https://zzyang.top/posts/docker-s/</link><guid isPermaLink="true">https://zzyang.top/posts/docker-s/</guid><pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Docker&lt;/h1&gt;
&lt;h2&gt;安装docker&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;卸载旧版&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;yum remove docker \
    docker-client \
    docker-client-latest \
    docker-common \
    docker-latest \
    docker-latest-logrotate \
    docker-logrotate \
    docker-engine \
    docker-selinux
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置Docker的yum库&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先要安装一个yum工具&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum install -y yum-utils device-mapper-persistent-data lvm2

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装成功后，执行命令，配置Docker的yum源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

sudo sed -i &apos;s+download.docker.com+mirrors.aliyun.com/docker-ce+&apos; /etc/yum.repos.d/docker-ce.repo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新yum，建立缓存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum makecache fast
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;安装docker&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;启动和校验&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 启动Docker
systemctl start docker

# 停止Docker
systemctl stop docker

# 重启
systemctl restart docker

# 设置开机自启
systemctl enable docker

# 执行docker ps命令，如果不报错，说明安装启动成功
docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置镜像加速
&lt;a href=&quot;https://xuanyuan.me/blog/archives/1154&quot;&gt;Docker/DockerHub 国内镜像源/加速列表&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;镜像地址可能会变更，如果失效可以百度找最新的docker镜像。
配置镜像步骤如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建目录
mkdir -p /etc/docker

# 复制内容
tee /etc/docker/daemon.json &amp;lt;&amp;lt;-&apos;EOF&apos;
{
    &quot;registry-mirrors&quot;: [
        &quot;http://hub-mirror.c.163.com&quot;,
        &quot;https://mirrors.tuna.tsinghua.edu.cn&quot;,
        &quot;http://mirrors.sohu.com&quot;,
        &quot;https://ustc-edu-cn.mirror.aliyuncs.com&quot;,
        &quot;https://ccr.ccs.tencentyun.com&quot;,
        &quot;https://docker.m.daocloud.io&quot;,
        &quot;https://docker.awsl9527.cn&quot;
    ]
}
EOF

# 重新加载配置
systemctl daemon-reload

# 重启Docker
systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;镜像和容器&lt;/h2&gt;
&lt;p&gt;当我们利用Docker安装应用时，Docker:会自动搜索并下载应用镜像(image)。镜像不仅包含应用本身，还包含应用
运行所需要的环境、配置、系统函数库。Docker会在运行镜像时创建一个隔离环境，称为容器(container)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;镜像仓库&lt;/strong&gt;: 存储和管理镜像的平台，Docker官方维护了一个公共仓库:&lt;a href=&quot;https://hub.docker.com/&quot;&gt;DockerHub&lt;/a&gt;
&lt;img src=&quot;../../assets/img/Snipaste_2025-04-21_22-57-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker是做什么的?&lt;/p&gt;
&lt;p&gt;Docker可以帮助我们下载应用镜像，创建并运行镜像的容器，从而快速部署应用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;什么是镜像?&lt;/p&gt;
&lt;p&gt;将应用所需的函数库、依赖、配置等与应用一起打包得到的就是镜像&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;什么是容器?&lt;/p&gt;
&lt;p&gt;为每个镜像的应用进程创建的隔离运行环境就是容器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;什么是镜像仓库?&lt;/p&gt;
&lt;p&gt;存储和管理镜像的服务就是镜像仓库 DockerHub是目前最大的镜像仓库，其中包含各种常见的应用镜像&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;命令解读&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
  --name mysql \
  -p 3306:3306 \
  -e TZ=Asia/Shanghai \
  -e MYSQL_ROOT_PASSWORD=123 \
  mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是因为Docker会自动搜索并下载MySQL。注意：这里下载的不是安装包，而是镜像。镜像中不仅包含了MySQL本身，还包含了其运行所需要的环境、配置、系统级函数库。因此它在运行时就有自己独立的环境，就可以跨系统运行，也不需要手动再次配置环境了。这套独立运行的隔离环境我们称为容器。&lt;/p&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;镜像：英文是image&lt;/li&gt;
&lt;li&gt;容器：英文是container&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;docker run -d ：创建并运行一个容器，-d则是让容器以后台进程运行&lt;/li&gt;
&lt;li&gt;--name mysql  : 给容器起个名字叫mysql，你可以叫别的&lt;/li&gt;
&lt;li&gt;-p 3306:3306 : 设置端口映射。&lt;/li&gt;
&lt;li&gt;容器是隔离环境，外界不可访问。但是可以将宿主机端口映射容器内到端口，当访问宿主机指定端口时，就是在访问容器内的端口了。&lt;/li&gt;
&lt;li&gt;容器内端口往往是由容器内的进程决定，例如MySQL进程默认端口是3306，因此容器内端口一定是3306；而宿主机端口则可以任意指定，一般与容器内保持一致。&lt;/li&gt;
&lt;li&gt;格式： -p 宿主机端口:容器内端口，示例中就是将宿主机的3306映射到容器内的3306端口&lt;/li&gt;
&lt;li&gt;-e TZ=Asia/Shanghai : 配置容器内进程运行时的一些参数&lt;/li&gt;
&lt;li&gt;格式：-e KEY=VALUE，KEY和VALUE都由容器内进程决定&lt;/li&gt;
&lt;li&gt;案例中，TZ=Asia/Shanghai是设置时区；MYSQL_ROOT_PASSWORD=123是设置MySQL默认密码&lt;/li&gt;
&lt;li&gt;mysql : 设置镜像名称，Docker会根据这个名字搜索并下载镜像&lt;/li&gt;
&lt;li&gt;格式：REPOSITORY:TAG，例如mysql:8.0，其中REPOSITORY可以理解为镜像名，TAG是版本号&lt;/li&gt;
&lt;li&gt;在未指定TAG的情况下，默认是最新版本，也就是mysql:latest&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1914680004151607296.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;镜像的名称不是随意的，而是要到DockerRegistry中寻找，镜像运行时的配置也不是随意的，要参考镜像的帮助文档，这些在DockerHub网站或者软件的官方网站中都能找到。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-04-22_21-58-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;常见命令&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;docker pull&lt;/td&gt;
&lt;td&gt;拉取镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker push&lt;/td&gt;
&lt;td&gt;推送镜像到DockerRegistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker images&lt;/td&gt;
&lt;td&gt;查看本地镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker rmi&lt;/td&gt;
&lt;td&gt;删除本地镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker run&lt;/td&gt;
&lt;td&gt;创建并运行容器（不能重复创建）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker stop&lt;/td&gt;
&lt;td&gt;停止指定容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker start&lt;/td&gt;
&lt;td&gt;启动指定容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker restart&lt;/td&gt;
&lt;td&gt;重新启动容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker rm&lt;/td&gt;
&lt;td&gt;删除指定容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker ps&lt;/td&gt;
&lt;td&gt;查看运行的容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker ps -a&lt;/td&gt;
&lt;td&gt;查看所有容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker logs&lt;/td&gt;
&lt;td&gt;查看容器运行日志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker exec&lt;/td&gt;
&lt;td&gt;进入容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker save&lt;/td&gt;
&lt;td&gt;保存镜像到本地压缩文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker load&lt;/td&gt;
&lt;td&gt;加载本地压缩文件到镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker inspect&lt;/td&gt;
&lt;td&gt;查看容器详细信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Docker最常见的命令就是操作镜像、容器的命令，详见官方文档： https://docs.docker.com/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-04-22_22-11-48.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何保存下载好的镜像，并打包？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# docker save --help

Usage:  docker save [OPTIONS] IMAGE [IMAGE...]

Save one or more images to a tar archive (streamed to STDOUT by default)

Aliases:
  docker image save, docker save

Options:
  -o, --output string   Write to a file, instead of STDOUT


[root@localhost ~]# docker save -o nginx.tar nginx:latest


[root@localhost ~]# ll
总用量 192044
-rw-------. 1 root root      1241 6月  22 2024 anaconda-ks.cfg
-rw-------. 1 root root 196647424 4月  22 22:32 nginx.tar

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如何加载回来呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;docker load -i nginx.tar&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# docker load --help

Usage:  docker load [OPTIONS]

Load an image from a tar archive or STDIN

Aliases:
  docker image load, docker load

Options:
  -i, --input string   Read from tar archive file, instead of STDIN
  -q, --quiet          Suppress the load output
[root@localhost ~]# docker load -i nginx.tar
ea680fbff095: Loading layer [==================================================&amp;gt;]   77.9MB/77.9MB
bd903131a05e: Loading layer [==================================================&amp;gt;]  118.7MB/118.7MB
9aad78ecf380: Loading layer [==================================================&amp;gt;]  3.584kB/3.584kB
9e3c6e8c1e25: Loading layer [==================================================&amp;gt;]  4.608kB/4.608kB
8d83f6b79143: Loading layer [==================================================&amp;gt;]   2.56kB/2.56kB
ccc5aac17fc4: Loading layer [==================================================&amp;gt;]   5.12kB/5.12kB
d1e3e4dd1aaa: Loading layer [==================================================&amp;gt;]  7.168kB/7.168kB
Loaded image: nginx:latest
[root@localhost ~]# docker images
REPOSITORY            TAG       IMAGE ID       CREATED        SIZE
nginx                 latest    4e1b6bae1e48   6 days ago     192MB
uums-web              1         d1c9ee2b1a26   8 months ago   946MB
zzy                   1.0.0     5d520aecab93   8 months ago   877MB
tomcat                8         2d2bccf89f53   3 years ago    678MB
redis                 5.0       c5da061a611a   3 years ago    110MB
mysql                 5.6       dd3b2a5dcb48   3 years ago    303MB
centos                7         eeb6ee3f44bd   3 years ago    204MB
mysql                 8.0.25    5c62e459e087   3 years ago    556MB
eclipse/centos_jdk8   latest    5bd02d36ed35   6 years ago    877MB
[root@localhost ~]#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;nginx常规操作&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull nginx

docker images

docker run -d --name nginx -p 80:80 nginx

#查看运行中容器
docker ps
# 也可以加格式化方式访问，格式会更加清爽
docker ps --format &quot;table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}\t{{.Names}}&quot;

#停止容器
docker stop nginx

#再次启动nginx容器
docker start nginx

#查看容器详细信息
docker inspect nginx

# 进入容器,查看容器内目录
docker exec -it nginx bash
# 或者，可以进入MySQL
docker exec -it mysql mysql -uroot -p

#删除容器
docker rm nginx
# 发现无法删除，因为容器运行中，强制删除容器
docker rm -f nginx

#查看日志
docker logs -f nginx

#动态查看日志
docker logs -f bef8969d1e0c


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;补充：&lt;/p&gt;
&lt;p&gt;默认情况下，每次重启虚拟机我们都需要手动启动Docker和Docker中的容器。通过命令可以实现开机自启：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Docker开机自启
systemctl enable docker

# Docker容器开机自启
docker update --restart=always [容器名/容器id]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;命令别名&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 修改/root/.bashrc文件
vi /root/.bashrc
内容如下：
# .bashrc

# User specific aliases and functions

alias rm=&apos;rm -i&apos;
alias cp=&apos;cp -i&apos;
alias mv=&apos;mv -i&apos;
alias dps=&apos;docker ps --format &quot;table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}\t{{.Names}}&quot;&apos;
alias dis=&apos;docker images&apos;

# Source global definitions
if [ -f /etc/bashrc ]; then
        . /etc/bashrc
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，执行命令使别名生效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source /root/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据卷&lt;/h2&gt;
&lt;p&gt;容器是隔离环境，容器内程序的文件、配置、运行时产生的容器都在容器内部，我们要读写容器内的文件非常不方便。大家思考几个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果要升级MySQL版本，需要销毁旧容器，那么数据岂不是跟着被销毁了？&lt;/li&gt;
&lt;li&gt;MySQL、Nginx容器运行后，如果我要修改其中的某些配置该怎么办？&lt;/li&gt;
&lt;li&gt;我想要让Nginx代理我的静态资源怎么办？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，容器提供程序的运行环境，但是程序运行产生的数据、程序运行依赖的配置都应该与容器解耦。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据卷（volume）&lt;strong&gt;是一个虚拟目录，是&lt;/strong&gt;容器内目录&lt;/strong&gt;与&lt;strong&gt;宿主机目录&lt;/strong&gt;之间映射的桥梁。&lt;/p&gt;
&lt;p&gt;以Nginx为例，我们知道Nginx中有两个关键的目录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;html：放置一些静态资源&lt;/li&gt;
&lt;li&gt;conf：放置配置文件
如果我们要让Nginx代理我们的静态资源，最好是放到html目录；如果我们要修改Nginx的配置，最好是找到conf下的nginx.conf文件。
但遗憾的是，容器运行的Nginx所有的文件都在容器内部。所以我们必须利用数据卷将两个目录与宿主机目录关联，方便我们操作。如图：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-04-23_21-22-35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在上图中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们创建了两个数据卷：&lt;code&gt;conf&lt;/code&gt;、&lt;code&gt;html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Nginx容器内部的conf目录和html目录分别与两个数据卷关联。&lt;/li&gt;
&lt;li&gt;而数据卷conf和html分别指向了宿主机的&lt;code&gt;/var/lib/docker/volumes/conf/_data&lt;/code&gt;目录和&lt;code&gt;/var/lib/docker/volumes/html/_data&lt;/code&gt;目录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样以来，容器内的conf和html目录就 与宿主机的conf和html目录关联起来，我们称为&lt;strong&gt;挂载&lt;/strong&gt;。此时，我们操作宿主机的&lt;code&gt;/var/lib/docker/volumes/html/_data&lt;/code&gt;就是在操作容器内的&lt;code&gt;/usr/share/nginx/html/_data&lt;/code&gt;目录。只要我们将静态资源放入宿主机对应目录，就可以被Nginx代理了。&lt;/p&gt;
&lt;p&gt;:::tip
&lt;code&gt;/var/lib/docker/volumes&lt;/code&gt;这个目录就是默认的存放所有容器数据卷的目录，其下再根据数据卷名称创建新目录，格式为&lt;code&gt;/数据卷名/_data&lt;/code&gt;。
:::&lt;/p&gt;
&lt;h3&gt;数据卷命令&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;docker volume create&lt;/td&gt;
&lt;td&gt;创建数据卷&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker volume ls&lt;/td&gt;
&lt;td&gt;查看所有数据卷&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker volume rm&lt;/td&gt;
&lt;td&gt;删除指定数据卷&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker volume inspect&lt;/td&gt;
&lt;td&gt;查看某个数据卷的详情&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker volume prune&lt;/td&gt;
&lt;td&gt;清除数据卷&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意：容器与数据卷的挂载要在创建容器时配置，对于创建好的容器，是不能设置数据卷的。而且&lt;strong&gt;创建容器的过程中，数据卷会自动创建&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name nginx -p 80:80 -v html:/usr/share/nginx/html nginx


docker volume ls


[root@localhost ~]# docker volume ls
DRIVER    VOLUME NAME
local     html
[root@localhost ~]# docker volume inspect html
[
    {
        &quot;CreatedAt&quot;: &quot;2025-04-23T21:37:48+08:00&quot;,
        &quot;Driver&quot;: &quot;local&quot;,
        &quot;Labels&quot;: null,
        &quot;Mountpoint&quot;: &quot;/var/lib/docker/volumes/html/_data&quot;,
        &quot;Name&quot;: &quot;html&quot;,
        &quot;Options&quot;: null,
        &quot;Scope&quot;: &quot;local&quot;
    }
]

[root@localhost ~]# cd /var/lib/docker/volumes/html/_data
[root@localhost _data]# ls
50x.html  index.html

#进入容器内部，查看/usr/share/nginx/html目录内的文件是否变化
docker exec -it nginx bash


&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;什么是数据卷?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据卷是一个虚拟目录，它将宿主机目录映射到容器内目录，方便我们操作容器内文件，或者方便迁移容器产生的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如何挂载数据卷?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在创建容器时，利用-v数据卷名:容器内目录完成挂载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器创建时，如果发现挂载的数据卷不存在时，会自动创建&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;数据卷的常见命令有哪些?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;docker volume ls:查看数据卷&lt;/li&gt;
&lt;li&gt;docker volumerm:删除数据卷&lt;/li&gt;
&lt;li&gt;docker volumeinspect:查看数据卷详情&lt;/li&gt;
&lt;li&gt;docker volume prune:删除未使用的数据卷&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;挂载本地目录&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;mysql挂载&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;数据卷的目录结构较深，如果我们去操作数据卷目录会不太方便。在很多情况下，我们会直接将容器目录与宿主机指定目录挂载。挂载语法与数据卷类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 挂载本地目录
-v 本地目录:容器内目录
# 挂载本地文件
-v 本地文件:容器内文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：本地目录或文件必须以 / 或 ./开头，如果直接以名字开头，会被识别为数据卷名而非本地目录名。&lt;/p&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-v mysql:/var/lib/mysql # 会被识别为一个数据卷叫mysql，运行时会自动创建这个数据卷
-v ./mysql:/var/lib/mysql # 会被识别为当前目录下的mysql目录，运行时如果不存在会创建目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mysql挂载位置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;挂载&lt;code&gt;/root/mysql/data&lt;/code&gt;到容器内的&lt;code&gt;/var/lib/mysql&lt;/code&gt;目录&lt;/li&gt;
&lt;li&gt;挂载&lt;code&gt;/root/mysql/init&lt;/code&gt;到容器内的&lt;code&gt;/docker-entrypoint-initdb.d&lt;/code&gt;目录（初始化的SQL脚本目录）&lt;/li&gt;
&lt;li&gt;挂载&lt;code&gt;/root/mysql/conf&lt;/code&gt;到容器内的&lt;code&gt;/etc/mysql/conf.d&lt;/code&gt;目录（这个是MySQL配置文件目录）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;init放&lt;code&gt;xxx.sql&lt;/code&gt;，初始化时只会执行一次&lt;/p&gt;
&lt;p&gt;conf放&lt;code&gt;xxx.cnf&lt;/code&gt;文件&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;本地目录挂载：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1.删除原来的MySQL容器
docker rm -f mysql

# 2.进入root目录
cd ~

# 3.创建并运行新mysql容器，挂载本地目录
docker run -d \
  --name mysql \
  -p 3306:3306 \
  -e TZ=Asia/Shanghai \
  -e MYSQL_ROOT_PASSWORD=123 \
  -v ./mysql/data:/var/lib/mysql \
  -v ./mysql/conf:/etc/mysql/conf.d \
  -v ./mysql/init:/docker-entrypoint-initdb.d \
  mysql

# 5.1.进入MySQL
docker exec -it mysql mysql -uroot -p123
# 5.2.查看编码表
show variables like &quot;%char%&quot;;

# 查看数据库
show databases;


# 切换到xxx数据库
use xxx;

#查看表
show tables;


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;镜像&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;镜像结构&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;镜像之所以能让我们快速跨操作系统部署应用而忽略其运行环境、配置，就是因为镜像中包含了程序运行需要的系统函数库、环境、配置、依赖。&lt;/p&gt;
&lt;p&gt;因此，自定义镜像本质就是依次准备好程序运行的基础环境、依赖、应用本身、运行配置等文件，并且打包而成。&lt;/p&gt;
&lt;p&gt;那因此，我们打包镜像也是分成这么几步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;准备Linux运行环境（java项目并不需要完整的操作系统，仅仅是基础运行环境即可）&lt;/li&gt;
&lt;li&gt;安装并配置JDK&lt;/li&gt;
&lt;li&gt;拷贝jar包&lt;/li&gt;
&lt;li&gt;配置启动脚本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述步骤中的每一次操作其实都是在生产一些文件（系统运行环境、函数库、配置最终都是磁盘文件），所以&lt;strong&gt;镜像就是一堆文件的集合&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但需要注意的是，镜像文件不是随意堆放的，而是按照操作的步骤分层叠加而成，每一层形成的文件都会单独打包并标记一个唯一id，称为&lt;strong&gt;Layer（层）&lt;/strong&gt;。这样，如果我们构建时用到的某些层其他人已经制作过，就可以直接拷贝使用这些层，而不用重复制作。&lt;/p&gt;
&lt;p&gt;例如，第一步中需要的Linux运行环境，通用性就很强，所以Docker官方就制作了这样的只包含Linux运行环境的镜像。我们在制作java镜像时，就无需重复制作，直接使用Docker官方提供的CentOS或Ubuntu镜像作为基础镜像。然后再搭建其它层即可，这样逐层搭建，最终整个Java项目的镜像结构如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-04-24_20-41-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Dockerfile&lt;/h3&gt;
&lt;p&gt;记录镜像结构的文件就称为Dockerfile，其对应的语法可以参考官方文档：
https://docs.docker.com/engine/reference/builder/&lt;/p&gt;
&lt;p&gt;其中的语法比较多，比较常用的有：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FROM&lt;/td&gt;
&lt;td&gt;指定基础镜像&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FROM centos:6&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENV&lt;/td&gt;
&lt;td&gt;设置环境变量，可在后面指令使用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ENV key value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COPY&lt;/td&gt;
&lt;td&gt;拷贝本地文件到镜像的指定目录&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COPY ./xx.jar /tmp/app.jar&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RUN&lt;/td&gt;
&lt;td&gt;执行Linux的shell命令，一般是安装过程的命令&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RUN yum install gcc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXPOSE&lt;/td&gt;
&lt;td&gt;指定容器运行时监听的端口，是给镜像使用者看的&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EXPOSE 8080&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENTRYPOINT&lt;/td&gt;
&lt;td&gt;镜像中应用的启动命令，容器运行时调用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ENTRYPOINT java -jar xx.jar&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;例如，要基于Ubuntu镜像来构建一个Java应用，其Dockerfile内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量，JDK的安装目录、容器内时区
ENV JAVA_DIR=/usr/local
ENV TZ=Asia/Shanghai
# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 设定时区
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime &amp;amp;&amp;amp; echo $TZ &amp;gt; /etc/timezone
# 安装JDK
RUN cd $JAVA_DIR \
 &amp;amp;&amp;amp; tar -xf ./jdk8.tar.gz \
 &amp;amp;&amp;amp; mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 指定项目监听的端口
EXPOSE 8080
# 入口，java项目的启动命令
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以后我们会有很多很多java项目需要打包为镜像，他们都需要Linux系统环境、JDK环境这两层，只有上面的3层不同（因为jar包不同）。如果每次制作java镜像都重复制作前两层镜像，是不是很麻烦。&lt;/p&gt;
&lt;p&gt;所以，就有人提供了基础的系统加JDK环境，我们在此基础上制作java镜像，就可以省去JDK的配置了：
&lt;img src=&quot;https://s1.imagehub.cc/images/2025/04/27/bf57245388ee66c6cfa15e76bbb23bea.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 基础镜像
FROM openjdk:11.0-jre-buster
# 设定时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime &amp;amp;&amp;amp; echo $TZ &amp;gt; /etc/timezone
# 拷贝jar包
COPY docker-demo.jar /app.jar
# 入口
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;构建镜像&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;命令&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 进入镜像目录
cd /root/demo
# 开始构建
docker build -t docker-demo:1.0 .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker build&lt;/code&gt; : 就是构建一个docker镜像&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-t docker-demo:1.0&lt;/code&gt; ：&lt;code&gt;-t&lt;/code&gt;参数是指定镜像的名称（&lt;code&gt;repository&lt;/code&gt;和&lt;code&gt;tag&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt; .&lt;/code&gt; : 最后的点是指构建时&lt;strong&gt;Dockerfile所在路径&lt;/strong&gt;，由于我们进入了demo目录，所以指定的是&lt;code&gt;.&lt;/code&gt;代表当前目录，也可以直接指定&lt;code&gt;Dockerfile&lt;/code&gt;目录：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 直接指定Dockerfile目录
docker build -t docker-demo:1.0 /root/demo
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Java部署示例&lt;/h4&gt;
&lt;p&gt;上传我们写好的&lt;code&gt;Dockerfile&lt;/code&gt;和&lt;code&gt;jar包&lt;/code&gt;
&lt;img src=&quot;https://s1.imagehub.cc/images/2025/04/27/856e1e28b4f28f5ef1187a6a3c07372b.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 基础镜像
FROM openjdk:11.0-jre-buster
# 设定时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime &amp;amp;&amp;amp; echo $TZ &amp;gt; /etc/timezone
# 拷贝jar包
COPY docker-demo.jar /app.jar
# 入口
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost demo]# docker build -t docker-demo:1.0 .

[+] Building 35.5s (8/8) FINISHED                                                             docker:default
 =&amp;gt; [internal] load build definition from Dockerfile                                                    0.0s
 =&amp;gt; =&amp;gt; transferring dockerfile: 359B                                                                    0.0s
 =&amp;gt; [internal] load metadata for docker.io/library/openjdk:11.0-jre-buster                              9.7s
 =&amp;gt; [internal] load .dockerignore                                                                       0.0s
 =&amp;gt; =&amp;gt; transferring context: 2B                                                                         0.0s
 =&amp;gt; [1/3] FROM docker.io/library/openjdk:11.0-jre-buster@sha256:569ba9252ddd693a29d39e81b3123481f308e  23.4s
 =&amp;gt; =&amp;gt; resolve docker.io/library/openjdk:11.0-jre-buster@sha256:569ba9252ddd693a29d39e81b3123481f308eb  0.0s
 =&amp;gt; =&amp;gt; sha256:4fe4e1c58b4af82939a918665dd1e7b5b636dd73c710b4bccb530edbb15470d2 7.86MB / 7.86MB         16.1s
.........


root@localhost demo]# docker images

REPOSITORY            TAG       IMAGE ID       CREATED          SIZE
docker-demo           1.0       678e0af7095e   32 seconds ago   315MB

[root@localhost demo]# docker run -d --name dockerDemo -p 8080:8080 docker-demo:1.0

364781fb488c583e3d2e59daaf7546a1975338851b29207d952d447f07634982
[root@localhost demo]# dps
CONTAINER ID   IMAGE             PORTS                                       STATUS         NAMES
7e90d418321d   docker-demo:1.0   0.0.0.0:8080-&amp;gt;8080/tcp, :::8080-&amp;gt;8080/tcp   Up 3 seconds   dockerDemo
[root@localhost demo]# curl localhost:8080/hello/count
&amp;lt;h5&amp;gt;欢迎访问商城, 这是您第1次访问&amp;lt;h5&amp;gt;[root@localhost demo]#

[root@localhost demo]# docker logs dockerDemo

  .   ____          _            __ _ _
 /\\ / ___&apos;_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | &apos;_ | &apos;_| | &apos;_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  &apos;  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::               (v2.7.12)

21:28:56  INFO 1 --- [           main] com.itheima.mp.MpDemoApplication         : Starting MpDemoApplication v0.0.1-SNAPSHOT using Java 11.0.16 on 7e90d418321d with PID 1 (/app.jar started by root in /)
21:28:56 DEBUG 1 --- [           main] com.itheima.mp.MpDemoApplication         : Running with Spring Boot v2.7.12, Spring v5.3.27
21:28:56  INFO 1 --- [           main] com.itheima.mp.MpDemoApplication         : No active profile set, falling back to 1 default profile: &quot;default&quot;
21:28:58  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
21:28:58  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
21:28:58  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.75]
21:28:58  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
21:28:58  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2075 ms
21:28:59  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path &apos;&apos;
21:28:59  INFO 1 --- [           main] com.itheima.mp.MpDemoApplication         : Started MpDemoApplication in 4.308 seconds (JVM running for 4.953)
21:29:03  INFO 1 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet &apos;dispatcherServlet&apos;
21:29:03  INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet &apos;dispatcherServlet&apos;
21:29:03  INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
[root@localhost demo]#
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;镜像的结构是怎样的?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;镜像中包含了应用程序所需要的运行环境、函数库、配置、以及应用本身等各种文件，这些文件分层打包而成，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dockerfile是做什么的?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dockerfile就是利用固定的指令来描述镜像的结构和构建过程，这样Docker才可以依次来构建镜像&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构建镜像的命令是什么?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;docker build -t 镜像名 [Dockerfile目录]&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容器网络互联&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://s1.imagehub.cc/images/2025/04/27/f3aa579e539004c2e940eb4f34bd6002.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;刚刚我们创建了一个Java项目的容器，而Java项目往往需要访问其它各种中间件，例如MySQL、Redis等。现在，我们的容器之间能否互相访问呢？我们来测试一下&lt;/p&gt;
&lt;p&gt;首先，我们查看下Nginx容器的详细信息，重点关注其中的网络IP地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker inspect nginx

  &quot;Networks&quot;: {
                &quot;bridge&quot;: {
                    &quot;IPAMConfig&quot;: null,
                    &quot;Links&quot;: null,
                    &quot;Aliases&quot;: null,
                    &quot;MacAddress&quot;: &quot;02:42:ac:11:00:04&quot;,
                    &quot;NetworkID&quot;: &quot;8bbc5fd1fe07fc2539250796feede9bb5c617c28b0f64521e9744b1ffb5cd8ea&quot;,
                    &quot;EndpointID&quot;: &quot;4e20a441965ddedc5264bf9296546342448c4aeb965324eedff362293fa6c809&quot;,
                    &quot;Gateway&quot;: &quot;172.17.0.1&quot;,
                    &quot;IPAddress&quot;: &quot;172.17.0.4&quot;,
                    &quot;IPPrefixLen&quot;: 16,
                    &quot;IPv6Gateway&quot;: &quot;&quot;,
                    &quot;GlobalIPv6Address&quot;: &quot;&quot;,
                    &quot;GlobalIPv6PrefixLen&quot;: 0,
                    &quot;DriverOpts&quot;: null,
                    &quot;DNSNames&quot;: null
                }
            }

[root@localhost ~]# docker exec -it dockerDemo bash

root@7e90d418321d:/# ping 172.17.0.4
PING 172.17.0.4 (172.17.0.4) 56(84) bytes of data.
64 bytes from 172.17.0.4: icmp_seq=1 ttl=64 time=0.286 ms
64 bytes from 172.17.0.4: icmp_seq=2 ttl=64 time=0.085 ms
64 bytes from 172.17.0.4: icmp_seq=3 ttl=64 time=0.092 ms
64 bytes from 172.17.0.4: icmp_seq=4 ttl=64 time=0.087 ms


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现可以互联，没有问题。&lt;/p&gt;
&lt;p&gt;但是，容器的网络IP其实是一个虚拟的IP，其值并不固定与某一个容器绑定，如果我们在开发时写死某个IP，而在部署时很可能MySQL容器的IP会发生变化，连接会失败。&lt;/p&gt;
&lt;p&gt;所以，我们必须借助于docker的网络功能来解决这个问题，官方文档：https://docs.docker.com/engine/reference/commandline/network/&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常见命令有：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker network create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;创建一个网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker network ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看所有网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker network rm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除指定网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker network prune&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;清除未使用的网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker network connect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;使指定容器连接加入某网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker network disconnect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;使指定容器连接离开某网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker network inspect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看网络详细信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;自定义网络&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1.首先通过命令创建一个网络
docker network create customizeNetwork

# 2.然后查看网络
docker network ls
# 结果：
NETWORK ID     NAME      DRIVER    SCOPE
639bc44d0a87   bridge    bridge    local
403f16ec62a2   customizeNetwork     bridge    local
0dc0f72a0fbb   host      host      local
cd8d3e8df47b   none      null      local
# 其中，除了customizeNetwork以外，其它都是默认的网络

# 3.让dockerDemo和mysql都加入该网络，注意，在加入网络时可以通过--alias给容器起别名
# 这样该网络内的其它容器可以用别名互相访问！
# 3.1.mysql容器，指定别名为db，另外每一个容器都有一个别名是容器名
docker network connect customizeNetwork mysql --alias db
# 3.2.dockerDemo容器，也就是我们的java项目
docker network connect customizeNetwork dockerDemo

# 4.进入dockerDemo容器，尝试利用别名访问db
# 4.1.进入容器
docker exec -it dockerDemo bash
# 4.2.用db别名访问
ping db
# 结果
PING db (172.18.0.2) 56(84) bytes of data.
64 bytes from mysql.hmall (172.18.0.2): icmp_seq=1 ttl=64 time=0.070 ms
64 bytes from mysql.hmall (172.18.0.2): icmp_seq=2 ttl=64 time=0.056 ms
# 4.3.用容器名访问
ping mysql
# 结果：
PING mysql (172.18.0.2) 56(84) bytes of data.
64 bytes from mysql.hmall (172.18.0.2): icmp_seq=1 ttl=64 time=0.044 ms
64 bytes from mysql.hmall (172.18.0.2): icmp_seq=2 ttl=64 time=0.054 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在无需记住IP地址也可以实现容器互联了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在自定义网络中，可以给容器起多个别名，默认的别名是容器名本身&lt;/li&gt;
&lt;li&gt;在同一个自定义网络中的容器，可以通过别名互相访问&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;部署项目&lt;/h2&gt;
&lt;p&gt;上传我们的&lt;code&gt;jar包&lt;/code&gt;和&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t 项目名:版本 .
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;网络&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker network create xxx-network

docker network connect xxx-network mysql --alias db

docker network connect xxxx-network nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mysql&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 拉取最新版本
docker pull mysql

# 拉取指定版本（推荐）
docker pull mysql:8.0.35
docker pull mysql:5.7.44

# 查看已下载的镜像
docker images | grep mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建项目目录结构&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建MySQL项目目录
mkdir -p /opt/mysql-docker/{data,conf,logs,init}
cd /opt/mysql-docker

# 目录结构说明
tree
/opt/mysql-docker/
├── data/           # MySQL数据文件
├── conf/           # MySQL配置文件
├── logs/           # MySQL日志文件
└── init/           # 初始化SQL脚本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建自定义配置文件&lt;/strong&gt;&lt;code&gt;my.cnf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;cnf文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[client]
default_character_set=utf8mb4
[mysql]
default_character_set=utf8mb4
[mysqld]
character_set_server=utf8mb4
collation_server=utf8mb4_unicode_ci
init_connect=&apos;SET NAMES utf8mb4&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建初始化脚本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你的&lt;code&gt;xxx.sql&lt;/code&gt;，sql文件&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;运行完整配置的MySQL容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
  --name mysql \
  -p 3306:3306 \
  -e TZ=Asia/Shanghai \
  -e MYSQL_ROOT_PASSWORD=123 \
  -v ./mysql/data:/var/lib/mysql \
  -v ./mysql/conf:/etc/mysql/conf.d \
  -v ./mysql/init:/docker-entrypoint-initdb.d \
  -v ./mysql/logs:/var/log/mysql \
  --network hmall-network \
  --restart always \
  mysql:latest


# 参数详解：
# -e MYSQL_ROOT_PASSWORD: root密码
# -v: 数据卷挂载
# 你的xxx.sql文件放在 mysql/init下
# my.cnf文件放在 mysql/conf下
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;验证MySQL运行&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看容器状态
docker ps | grep mysql

# 查看容器日志
docker logs my-mysql

# 进入MySQL容器
docker exec -it my-mysql bash

# 在容器内连接MySQL
mysql -u root -p
# 输入密码：123456

# 测试SQL命令
SHOW DATABASES;
SELECT VERSION();
EXIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;备份及迁移&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;查看当前环境&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看当前MySQL容器信息
docker ps | grep mysql
docker inspect mysql | grep Image

# 查看当前MySQL版本
docker exec -it mysql mysql -u root -p -e &quot;SELECT VERSION();&quot;

# 查看数据挂载情况
docker inspect mysql | grep -A 10 &quot;Mounts&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;检查新镜像版本&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看可用的MySQL版本
docker search mysql
docker hub search mysql

# 拉取目标版本镜像（先不要删除旧的）
docker pull mysql:8.0.36  # 举例：升级到新版本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🛡️ &lt;strong&gt;数据备份&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用mysqldump备份&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建备份目录
mkdir -p /backup/mysql/$(date +%Y%m%d)
cd /backup/mysql/$(date +%Y%m%d)

# 备份所有数据库
docker exec mysql mysqldump -u root -p123 --all-databases --routines --triggers &amp;gt; all_databases_backup.sql

# 备份指定数据库（推荐分别备份）
docker exec mysql mysqldump -u root -p123 --databases your_db1 your_db2 &amp;gt; databases_backup.sql

# 验证备份文件
ls -la *.sql
head -n 20 all_databases_backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;删除旧容器（保留数据）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 只删除容器，不删除挂载的数据
docker rm mysql

# 确认数据目录依然存在
ls -la /opt/mysql-docker/data/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用新镜像创建容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用新镜像运行容器（使用相同的数据挂载）
docker run -d \
  --name mysql \
  -p 3306:3306 \
  -e TZ=Asia/Shanghai \
  -e MYSQL_ROOT_PASSWORD=123 \
  -v /opt/mysql-docker/data:/var/lib/mysql \
  -v /opt/mysql-docker/conf:/etc/mysql/conf.d \
  -v /opt/mysql-docker/init:/docker-entrypoint-initdb.d \
  -v /opt/mysql-docker/logs:/var/log/mysql \
  --restart always \
  mysql:8.0.36  # 新的镜像版本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;验证和测试&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看容器状态
docker ps | grep mysql

# 查看启动日志
docker logs mysql

# 连接MySQL验证
docker exec -it mysql mysql -u root -p123 -e &quot;SELECT VERSION();&quot;

# 验证数据完整性
docker exec -it mysql mysql -u root -p123 -e &quot;SHOW DATABASES;&quot;
docker exec -it mysql mysql -u root -p123 -e &quot;USE your_database; SHOW TABLES;&quot;

# 测试应用连接
# 启动你的应用，测试数据库连接和功能
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nginx&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;创建项目目录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /root/nginx/{conf,conf.d,html,logs,ssl}
cd /root/nginx

# 查看目录结构
tree
/root/nginx/
├── conf/           # 主配置目录
├── conf.d/         # 站点配置目录
├── html/           # 网站文件目录
├── logs/           # 日志目录
└── ssl/            # SSL证书目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建主配置文件
cat &amp;gt; /root/nginx/conf/nginx.conf &amp;lt;&amp;lt; &apos;EOF&apos;
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main &apos;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &apos;
                   &apos;$status $body_bytes_sent &quot;$http_referer&quot; &apos;
                   &apos;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&apos;;

    access_log /var/log/nginx/access.log main;

    sendfile on;
    keepalive_timeout 65;

    # 重要：包含conf.d目录下的所有配置文件
    include /etc/nginx/conf.d/*.conf;
}
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;站点配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建默认站点配置
cat &amp;gt; /root/nginx/conf.d/default.conf &amp;lt;&amp;lt; &apos;EOF&apos;
server {
    listen 80;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html index.htm;

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # API代理
    location /api/ {
        proxy_pass http://backend:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
      --name nginx \
      -p 80:80 \
      -p 443:443 \
      -v /root/nginx/conf.d/:/etc/nginx/conf.d \
      -v /root/nginx/conf/nginx.conf:/etc/nginx/conf/nginx.conf \
      -v /root/nginx/ssl:/etc/nginx/ssl \
      -v /root/nginx/html:/usr/share/nginx/html \
      -v /root/nginx/logs:/var/log/nginx \
      --network xxx-network
      --restart always \
      nginx


# 参数详解：
# --name nginx: 容器名称
# -p 80:80: HTTP端口映射
# -p 443:443: HTTPS端口映射
# -v: 挂载配置、网站文件、日志等
# --restart always: 自动重启
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;验证部署&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看容器状态
docker ps | grep nginx

# 查看Nginx日志
docker logs nginx

# 测试HTTP访问
curl http://localhost
curl -I http://localhost

# 测试配置文件语法
docker exec nginx nginx -t

# 重新加载配置（无需重启）
docker exec nginx nginx -s reload
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;前端两个项目，admin端，客户端部署示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
  --name nginx \
  -p 18080:18080 \
  -p 18081:18081 \
  -v /root/nginx/html:/usr/share/nginx/html \
  -v /root/nginx/nginx.conf:/etc/nginx/nginx.conf \
  --network hmall \
  nginxs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ 配置文件中，这里不要写死，用于容器之间互相通信；&lt;/p&gt;
&lt;p&gt;该容器名称是指后端部署的项目，使用容器名称代替ip地址 (因为项目重启后网桥ip是会变的)
&lt;img src=&quot;https://s1.imagehub.cc/images/2025/04/27/cc0397c00a235d59a1c9e787de4335b1.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/json;

    sendfile        on;

    keepalive_timeout  65;

    server {
        listen       18080;
        # 指定前端项目所在的位置
        location / {
            root /usr/share/nginx/html/hmall-portal;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
        location /api {
            rewrite /api/(.*)  /$1 break;
            proxy_pass http://hmall:8080;
        }
    }
    server {
        listen       18081;
        # 指定前端项目所在的位置
        location / {
            root /usr/share/nginx/html/hmall-admin;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
        location /api {
            rewrite /api/(.*)  /$1 break;
            proxy_pass http://hmall:8080;
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Redis&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;创建项目目录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建Redis项目目录
mkdir -p /opt/redis-docker/{conf,data,logs}
cd /opt/redis-docker

# 目录结构说明
tree
/opt/redis-docker/
├── conf/           # 配置文件目录
├── data/           # 数据持久化目录
└── logs/           # 日志文件目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建Redis配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建Redis配置文件
cat &amp;gt; /opt/redis-docker/conf/redis.conf &amp;lt;&amp;lt; &apos;EOF&apos;
# ==================== 基础配置 ====================
# 绑定地址（0.0.0.0允许所有IP访问）
bind 0.0.0.0

# 端口号
port 6379

# 超时设置（0表示不超时）
timeout 0

# TCP keepalive
tcp-keepalive 300

# ==================== 安全配置 ====================
# 设置密码（生产环境必须设置）
requirepass your_redis_password_123

# 禁用危险命令
rename-command FLUSHDB &quot;&quot;
rename-command FLUSHALL &quot;&quot;
rename-command DEBUG &quot;&quot;
rename-command CONFIG &quot;CONFIG_d83jf93jf&quot;

# ==================== 持久化配置 ====================
# RDB持久化配置
save 900 1        # 900秒内至少1个key变化时保存
save 300 10       # 300秒内至少10个key变化时保存
save 60 10000     # 60秒内至少10000个key变化时保存

# RDB文件名和位置
dbfilename dump.rdb
dir /data

# 压缩RDB文件
rdbcompression yes

# 校验RDB文件
rdbchecksum yes

# AOF持久化配置
appendonly yes
appendfilename &quot;appendonly.aof&quot;
appendfsync everysec     # 每秒同步一次

# AOF重写配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# ==================== 内存配置 ====================
# 最大内存限制（根据服务器内存调整）
maxmemory 1gb

# 内存溢出策略
maxmemory-policy allkeys-lru

# ==================== 日志配置 ====================
# 日志级别：debug, verbose, notice, warning
loglevel notice

# 日志文件（空表示输出到stdout）
logfile /var/log/redis/redis-server.log

# ==================== 性能优化 ====================
# 数据库数量
databases 16

# 客户端连接数
maxclients 10000

# TCP缓冲区
tcp-backlog 511

# 惰性删除
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes

# ==================== 慢查询日志 ====================
# 慢查询阈值（微秒）
slowlog-log-slower-than 10000

# 慢查询日志长度
slowlog-max-len 128
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行Redis容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
  --name redis-server \
  -p 6379:6379 \
  -v /opt/redis-docker/conf/redis.conf:/etc/redis/redis.conf \  # 挂载配置文件
  -v /opt/redis-docker/data:/data \
  -v /opt/redis-docker/logs:/var/log/redis \
  --restart always \
  redis:7.2-alpine \                    # ← 使用这个镜像
  redis-server /etc/redis/redis.conf   # ← 执行这个命令

# 流程说明：
# 1. Docker拉取 redis:7.2-alpine 镜像
# 2. 创建容器，挂载本地配置文件到容器的 /etc/redis/redis.conf
# 3. 容器启动时执行命令：redis-server /etc/redis/redis.conf
# 4. Redis读取配置文件，发现 requirepass 设置，启用密码认证
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;验证Redis部署&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看容器状态
docker ps | grep redis

# 查看Redis日志
docker logs redis-server

# 连接Redis（带密码）
docker exec -it redis-server redis-cli -a your_redis_password_123

# 或者不进入容器直接执行命令
docker exec redis-server redis-cli -a your_redis_password_123 ping
docker exec redis-server redis-cli -a your_redis_password_123 info
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🧪 &lt;strong&gt;Redis功能测试&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 进入Redis CLI
docker exec -it redis-server redis-cli -a your_redis_password_123

# 测试基础操作
127.0.0.1:6379&amp;gt; ping
PONG

# 字符串操作
127.0.0.1:6379&amp;gt; set name &quot;张三&quot;
OK
127.0.0.1:6379&amp;gt; get name
&quot;张三&quot;


# 哈希操作
127.0.0.1:6379&amp;gt; hset user:1 name &quot;李四&quot; age 25
(integer) 2
127.0.0.1:6379&amp;gt; hgetall user:1
1) &quot;name&quot;
2) &quot;李四&quot;
3) &quot;age&quot;
4) &quot;25&quot;

# 查看数据库信息
127.0.0.1:6379&amp;gt; info
127.0.0.1:6379&amp;gt; dbsize

# 退出
127.0.0.1:6379&amp;gt; exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🔧 &lt;strong&gt;常用管理命令&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看Redis容器状态
docker ps | grep redis
docker stats redis-server

# 查看Redis日志
docker logs redis-server
docker logs -f redis-server --tail 100

# 重启Redis容器
docker restart redis-server

# 停止/启动Redis容器
docker stop redis-server
docker start redis-server

# 进入Redis容器
docker exec -it redis-server redis-cli -a your_redis_password_123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Redis命令&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看Redis信息
docker exec redis-server redis-cli -a your_redis_password_123 info
docker exec redis-server redis-cli -a your_redis_password_123 info memory
docker exec redis-server redis-cli -a your_redis_password_123 info clients

# 查看配置
docker exec redis-server redis-cli -a your_redis_password_123 config get &quot;*&quot;

# 重新加载配置
docker exec redis-server redis-cli -a your_redis_password_123 config rewrite

# 查看慢查询
docker exec redis-server redis-cli -a your_redis_password_123 slowlog get 10

# 清空数据库（小心使用）
docker exec redis-server redis-cli -a your_redis_password_123 flushdb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;RabbitMQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;创建项目目录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建RabbitMQ项目目录
mkdir -p /opt/rabbitmq-docker/{data,logs,config,plugins}
cd /opt/rabbitmq-docker

# 目录结构说明
tree
/opt/rabbitmq-docker/
├── config/         # 配置文件目录
├── data/           # 数据持久化目录
├── logs/           # 日志文件目录
└── plugins/        # 插件目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建RabbitMQ配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建RabbitMQ配置文件
cat &amp;gt; /opt/rabbitmq-docker/config/rabbitmq.conf &amp;lt;&amp;lt; &apos;EOF&apos;
# ==================== 基础配置 ====================
# 监听地址（0.0.0.0允许所有IP访问）
listeners.tcp.default = 5672

# 日志级别：debug, info, warning, error, critical, none
log.console.level = info
log.file.level = info

# ==================== 管理界面配置 ====================
# 启用管理插件
management.tcp.port = 15672
management.tcp.ip = 0.0.0.0

# ==================== 用户和权限配置 ====================
# 禁用guest用户远程访问（安全考虑）
loopback_users.guest = false

# ==================== 内存和磁盘配置 ====================
# 内存高水位阈值（当内存使用超过此值时会阻塞生产者）
vm_memory_high_watermark.relative = 0.6

# 磁盘空间低水位阈值
disk_free_limit.relative = 2.0

# ==================== 集群配置 ====================
# 集群节点类型（disc：磁盘节点，ram：内存节点）
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config

# ==================== 心跳配置 ====================
# 客户端心跳间隔（秒）
heartbeat = 60

# ==================== 队列配置 ====================
# 默认队列类型
default_queue_type = classic

# ==================== SSL/TLS配置（可选） ====================
# 如果需要SSL，取消注释并配置证书路径
# listeners.ssl.default = 5671
# ssl_options.cacertfile = /etc/rabbitmq/certs/ca_certificate.pem
# ssl_options.certfile = /etc/rabbitmq/certs/server_certificate.pem
# ssl_options.keyfile = /etc/rabbitmq/certs/server_key.pem
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建用户初始化脚本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据实际情况，修改配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建用户管理脚本
cat &amp;gt; /opt/rabbitmq-docker/config/definitions.json &amp;lt;&amp;lt; &apos;EOF&apos;
{
  &quot;users&quot;: [
    {
      &quot;name&quot;: &quot;admin&quot;,
      &quot;password&quot;: &quot;your_admin_password_2024!&quot;,
      &quot;tags&quot;: [&quot;administrator&quot;]
    },
    {
      &quot;name&quot;: &quot;app_user&quot;,
      &quot;password&quot;: &quot;your_app_password_2024!&quot;,
      &quot;tags&quot;: [&quot;&quot;]
    }
  ],
  &quot;vhosts&quot;: [
    {&quot;name&quot;: &quot;/&quot;},
    {&quot;name&quot;: &quot;/dev&quot;},
    {&quot;name&quot;: &quot;/prod&quot;}
  ],
  &quot;permissions&quot;: [
    {
      &quot;user&quot;: &quot;admin&quot;,
      &quot;vhost&quot;: &quot;/&quot;,
      &quot;configure&quot;: &quot;.*&quot;,
      &quot;write&quot;: &quot;.*&quot;,
      &quot;read&quot;: &quot;.*&quot;
    },
    {
      &quot;user&quot;: &quot;admin&quot;,
      &quot;vhost&quot;: &quot;/dev&quot;,
      &quot;configure&quot;: &quot;.*&quot;,
      &quot;write&quot;: &quot;.*&quot;,
      &quot;read&quot;: &quot;.*&quot;
    },
    {
      &quot;user&quot;: &quot;admin&quot;,
      &quot;vhost&quot;: &quot;/prod&quot;,
      &quot;configure&quot;: &quot;.*&quot;,
      &quot;write&quot;: &quot;.*&quot;,
      &quot;read&quot;: &quot;.*&quot;
    },
    {
      &quot;user&quot;: &quot;app_user&quot;,
      &quot;vhost&quot;: &quot;/dev&quot;,
      &quot;configure&quot;: &quot;.*&quot;,
      &quot;write&quot;: &quot;.*&quot;,
      &quot;read&quot;: &quot;.*&quot;
    },
    {
      &quot;user&quot;: &quot;app_user&quot;,
      &quot;vhost&quot;: &quot;/prod&quot;,
      &quot;configure&quot;: &quot;.*&quot;,
      &quot;write&quot;: &quot;.*&quot;,
      &quot;read&quot;: &quot;.*&quot;
    }
  ],
  &quot;policies&quot;: [],
  &quot;queues&quot;: [],
  &quot;exchanges&quot;: [],
  &quot;bindings&quot;: []
}
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;权限说明：&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;configure&lt;/code&gt;: 允许创建/删除队列和交换机&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write&lt;/code&gt;: 允许发送消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;read&lt;/code&gt;: 允许接收消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.*&lt;/code&gt; 表示完全权限&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;运行RabbitMQ容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
  --name rabbitmq-server \
  --hostname rabbitmq-node1 \
  -p 5672:5672 \
  -p 15672:15672 \
  -p 25672:25672 \
  -e RABBITMQ_DEFAULT_USER=admin \
  -e RABBITMQ_DEFAULT_PASS=your_admin_password_2024! \
  -v /opt/rabbitmq-docker/data:/var/lib/rabbitmq \
  -v /opt/rabbitmq-docker/logs:/var/log/rabbitmq \
  -v /opt/rabbitmq-docker/config/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
  -v /opt/rabbitmq-docker/config/definitions.json:/etc/rabbitmq/definitions.json \
  -v /opt/rabbitmq-docker/plugins:/plugins  \
  --restart always \
  rabbitmq:3.12-management
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;端口&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;谁使用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5672&lt;/td&gt;
&lt;td&gt;AMQP端口&lt;/td&gt;
&lt;td&gt;应用程序连接RabbitMQ&lt;/td&gt;
&lt;td&gt;客户端应用（如SpringBoot应用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15672&lt;/td&gt;
&lt;td&gt;管理界面端口&lt;/td&gt;
&lt;td&gt;Web管理控制台&lt;/td&gt;
&lt;td&gt;管理员（浏览器访问）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25672&lt;/td&gt;
&lt;td&gt;集群通信端口&lt;/td&gt;
&lt;td&gt;RabbitMQ节点间通信&lt;/td&gt;
&lt;td&gt;RabbitMQ节点之间&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;--hostname的作用：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--hostname rabbitmq-node1&lt;/code&gt;设置了容器的主机名为&lt;code&gt;rabbitmq-node1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;为什么&lt;code&gt;RabbitMQ&lt;/code&gt;需要设置主机名？&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RabbitMQ&lt;/code&gt;的节点名称格式：&lt;code&gt;rabbit@&amp;lt;hostname&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 不设置hostname时（使用随机容器ID）
节点名: rabbit@a1b2c3d4e5f6  # 每次重启都会变化

# 设置hostname为rabbitmq-node1时
节点名: rabbit@rabbitmq-node1  # 固定且有意义
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;数据持久化的一致性&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;   # RabbitMQ在数据目录中会创建以节点名命名的文件夹
   /var/lib/rabbitmq/mnesia/rabbit@rabbitmq-node1/

   # 如果hostname变化，RabbitMQ会认为这是一个新节点
   # 可能导致数据无法正确加载
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;集群管理&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;   # 在集群中，节点通过名称相互识别
   rabbitmqctl cluster_status
   # 输出：[{nodes,[{disc,[rabbit@rabbitmq-node1,rabbit@rabbitmq-node2]}]}]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;监控和日志&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;   # 日志中会显示节点名，便于问题定位
   2024-01-01 10:00:00.123 [info] &amp;lt;0.123.0&amp;gt; accepting AMQP connection rabbit@rabbitmq-node1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不设置hostname会怎样？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 问题：
# 1. 节点名会是 rabbit@随机容器ID
# 2. 每次重启容器，节点名都会变化
# 3. 可能导致数据持久化问题
# 4. 监控和日志不易识别
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;验证部署&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看容器状态
docker ps | grep rabbitmq

# 查看RabbitMQ日志
docker logs rabbitmq-server

# 查看RabbitMQ状态
docker exec rabbitmq-server rabbitmqctl status

# 查看用户列表
docker exec rabbitmq-server rabbitmqctl list_users

# 查看虚拟主机
docker exec rabbitmq-server rabbitmqctl list_vhosts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;启用插件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it rabbitmq-server rabbitmq-plugins enable rabbitmq_delayed_message_exchange

# 查看已启用的插件
docker exec rabbitmq-server rabbitmq-plugins list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🔧 &lt;strong&gt;常用管理命令&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看RabbitMQ容器状态
docker ps | grep rabbitmq
docker stats rabbitmq-server

# 查看RabbitMQ日志
docker logs rabbitmq-server
docker logs -f rabbitmq-server --tail 100

# 重启RabbitMQ容器
docker restart rabbitmq-server

# 停止/启动RabbitMQ容器
docker stop rabbitmq-server
docker start rabbitmq-server

# 进入RabbitMQ容器
docker exec -it rabbitmq-server bash

# 查看集群状态
docker exec rabbitmq-server rabbitmqctl cluster_status
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Minio&lt;/h3&gt;
&lt;p&gt;有的最新版本功能不全，需注意&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull minio/minio:RELEASE.2025-04-22T22-12-26Z
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🐳 &lt;strong&gt;Docker安装MinIO详细步骤&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;创建项目目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建MinIO项目目录
mkdir -p /opt/minio-docker/{data,config,certs}
cd /opt/minio-docker

# 目录结构说明
tree
/opt/minio-docker/
├── config/         # 配置文件目录
├── data/           # 数据存储目录
└── certs/          # SSL证书目录（可选）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建环境配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;💡 记得显示隐藏目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建环境变量配置文件
cat &amp;gt; /opt/minio-docker/.env &amp;lt;&amp;lt; &apos;EOF&apos;
# ==================== MinIO基础配置 ====================
# 管理员用户名（不能是admin/minioadmin，建议用复杂用户名）
MINIO_ROOT_USER=xxxxxx

# 管理员密码（至少8位）
MINIO_ROOT_PASSWORD=MySecurePassword2024!

# ==================== 域名和地址配置 ====================
# MinIO服务器地址（生产环境建议配置域名）
MINIO_SERVER_URL=http://localhost:9000

# MinIO控制台地址
MINIO_BROWSER_REDIRECT_URL=http://localhost:9001

# ==================== 区域配置 ====================
# 存储区域
MINIO_REGION_NAME=us-east-1

# ==================== 安全配置 ====================
# 启用严格的S3兼容性
#MINIO_API_STRICT_S3_COMPAT=on

# 启用HTTPS重定向（如果使用SSL）
# MINIO_BROWSER_REDIRECT=on
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建MinIO配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建MinIO配置文件
cat &amp;gt; /opt/minio-docker/config/config.env &amp;lt;&amp;lt; &apos;EOF&apos;
# ==================== 性能配置 ====================
# 设置最大并发连接数
MINIO_API_REQUESTS_MAX=10000

# 设置读写缓冲区大小
MINIO_API_REQUESTS_DEADLINE=10s

# ==================== 日志配置 ====================
# 日志级别：ERROR, WARN, INFO, DEBUG
MINIO_LOG_LEVEL=INFO

# 启用控制台日志
MINIO_LOG_CONSOLE=on

# ==================== 存储配置 ====================
# 启用版本控制
MINIO_VERSIONING=on

# 设置默认存储类
#MINIO_STORAGE_CLASS_STANDARD=EC:2

# ==================== 监控配置 ====================
# 启用Prometheus指标
MINIO_PROMETHEUS_AUTH_TYPE=public

# ==================== 通知配置 ====================
# Webhook通知端点（可选）
# MINIO_NOTIFY_WEBHOOK_ENABLE=on
# MINIO_NOTIFY_WEBHOOK_ENDPOINT=http://your-webhook-url
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行MinIO容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
  --name minio-server \
  --hostname minio-node1 \
  -p 9000:9000 \
  -p 9001:9001 \
  --env-file /opt/minio-docker/.env \
  --env-file /opt/minio-docker/config/config.env \
  -v /opt/minio-docker/data:/data \
  -v /opt/minio-docker/config:/etc/minio \
  --restart always \
  --health-cmd &quot;curl -f http://localhost:9000/minio/health/live&quot; \
  --health-interval=30s \
  --health-timeout=20s \
  --health-retries=3 \
  minio/minio server /data --console-address &quot;:9001&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;code&gt;minio/minio server /data --console-address &quot;:9001&quot;&lt;/code&gt; 详解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minio/minio
#  ↑     ↑
#  |     └── 镜像名称
#  └────── 官方命名空间/组织名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;版本选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用最新版本
minio/minio:latest

# 使用特定版本（推荐生产环境）
minio/minio:RELEASE.2024-01-18T22-51-28Z

# 使用我们示例中的方式（默认latest）
minio/minio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;server&lt;/code&gt; - MinIO启动模式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 完整的MinIO命令格式
minio &amp;lt;command&amp;gt; [arguments...]

# 主要命令：
minio server    # 启动对象存储服务器（我们使用的）
minio gateway   # 启动网关模式（已废弃）
minio admin     # 管理命令
minio client    # 客户端命令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/data&lt;/code&gt; - 数据存储路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minio server /data
#            └──── 告诉MinIO在容器内的/data目录存储数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据存储层次：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在容器内部
/data/
├── .minio.sys/          # MinIO系统文件
│   ├── buckets/         # 存储桶元数据
│   ├── config/          # 配置信息
│   └── users/           # 用户信息
├── bucket1/             # 用户创建的存储桶1
│   ├── file1.jpg
│   └── file2.pdf
└── bucket2/             # 用户创建的存储桶2
    └── document.docx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与Docker挂载的关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 我们的Docker命令中
-v /opt/minio-docker/data:/data
#  ↑                      ↑
#  宿主机路径              容器内路径

# 实际效果：
# 容器内的 /data 目录 = 宿主机的 /opt/minio-docker/data 目录
# MinIO在容器内写入 /data/bucket1/file.jpg
# 实际保存在宿主机 /opt/minio-docker/data/bucket1/file.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--console-address &quot;:9001&quot;&lt;/code&gt; - 控制台地址配置
为什么需要指定控制台地址？&lt;/p&gt;
&lt;p&gt;不指定会怎样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ❌ 不指定控制台地址
minio server /data

# 问题：
# 1. 控制台可能使用随机端口
# 2. 或者与API端口冲突
# 3. 外部无法访问管理界面
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;📄 &lt;strong&gt;--env-file 环境变量文件的作用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;两个环境变量文件的用途：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--env-file /opt/minio-docker/.env \              # 基础配置文件
--env-file /opt/minio-docker/config/config.env \ # 高级配置文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; - 基础配置文件、存储敏感信息（用户名、密码）、存储基础连接信息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;config.env&lt;/code&gt; - 高级配置文件、存储性能调优参数 (运维人员可以单独调整性能参数，不用接触密码)&lt;/p&gt;
&lt;p&gt;实际使用场景：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 开发环境
--env-file .env.dev \
--env-file config.dev.env

# 生产环境
--env-file .env.prod \       # 不同的密码和地址
--env-file config.prod.env   # 不同的性能参数
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;🏥 &lt;strong&gt;健康检查参数详解&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--health-cmd &quot;curl -f http://localhost:9000/minio/health/live&quot; \
--health-interval=30s \
--health-timeout=20s \
--health-retries=3 \
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各参数详细说明：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;--health-cmd&lt;/td&gt;
&lt;td&gt;健康检查命令&lt;/td&gt;
&lt;td&gt;Docker定期执行此命令检查容器是否健康&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--health-interval&lt;/td&gt;
&lt;td&gt;检查间隔&lt;/td&gt;
&lt;td&gt;每30秒执行一次健康检查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--health-timeout&lt;/td&gt;
&lt;td&gt;超时时间&lt;/td&gt;
&lt;td&gt;如果命令20秒内没响应，视为失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--health-retries&lt;/td&gt;
&lt;td&gt;重试次数&lt;/td&gt;
&lt;td&gt;连续3次失败后，标记容器为unhealthy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;健康检查的工作流程：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-08_15-20-18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;查看健康检查状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -f http://localhost:9000/minio/health/live
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;返回200: 表示MinIO服务正常运行&lt;/li&gt;
&lt;li&gt;返回非200: 表示MinIO服务异常&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;核心作用&lt;/th&gt;
&lt;th&gt;不设置的后果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;--hostname&lt;/td&gt;
&lt;td&gt;为MinIO节点提供固定、有意义的标识&lt;/td&gt;
&lt;td&gt;节点名随机变化，集群管理困难，数据可能无法正确加载&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--env-file&lt;/td&gt;
&lt;td&gt;分离敏感配置和功能配置，便于管理&lt;/td&gt;
&lt;td&gt;配置混乱，安全性差，维护困难&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--health-*&lt;/td&gt;
&lt;td&gt;提供自动化的容器健康监控&lt;/td&gt;
&lt;td&gt;无法及时发现服务异常，故障排查困难&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;验证部署&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看容器状态
docker ps | grep minio

# 查看MinIO日志
docker logs minio-server

# 健康检查
docker exec minio-server curl -f http://localhost:9000/minio/health/live

# 查看MinIO版本和状态
docker exec minio-server minio --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name hm -p 8080:8080 --network xxx-network hmall
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DockerCompose&lt;/h2&gt;
&lt;p&gt;Docker Compose就可以帮助我们实现&lt;strong&gt;多个相互关联的Docker容器的快速部署&lt;/strong&gt;。它允许用户通过一个单独的 docker-compose.yml 模板文件（YAML 格式）来定义一组相关联的应用容器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对比如下：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;docker run 参数&lt;/th&gt;
&lt;th&gt;docker compose 指令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;--name&lt;/td&gt;
&lt;td&gt;container_name&lt;/td&gt;
&lt;td&gt;容器名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-p&lt;/td&gt;
&lt;td&gt;ports&lt;/td&gt;
&lt;td&gt;端口映射&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e&lt;/td&gt;
&lt;td&gt;environment&lt;/td&gt;
&lt;td&gt;环境变量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-v&lt;/td&gt;
&lt;td&gt;volumes&lt;/td&gt;
&lt;td&gt;数据卷配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--network&lt;/td&gt;
&lt;td&gt;networks&lt;/td&gt;
&lt;td&gt;网络&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;hmall:
  build:
    context: .
    dockerfile: Dockerfile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建Java项目，意为在当前目录下的Dockerfile，进行构建&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  mysql:
    image: mysql
    container_name: mysql
    ports:
      - &quot;3306:3306&quot;
    environment:
      TZ: Asia/Shanghai
      MYSQL_ROOT_PASSWORD: 123
    volumes:
      - &quot;./mysql/conf:/etc/mysql/conf.d&quot;
      - &quot;./mysql/data:/var/lib/mysql&quot;
      - &quot;./mysql/init:/docker-entrypoint-initdb.d&quot;
    networks:
      - hmall-network
  hmall:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: hmall
    ports:
      - &quot;8080:8080&quot;
    networks:
      - hmall-network
    depends_on:
      - mysql
  nginx:
    image: nginx
    container_name: nginx
    ports:
      - &quot;18080:18080&quot;
      - &quot;18081:18081&quot;
    volumes:
      - &quot;./nginx/nginx.conf:/etc/nginx/nginx.conf&quot;
      - &quot;./nginx/html:/usr/share/nginx/html&quot;
    depends_on:
      - hmall
    networks:
      - hmall-network
networks:
  hmall-network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# docker compose up -d

[+] Building 0.2s (8/8) FINISHED                                                                   docker:default
 =&amp;gt; [hmall internal] load build definition from Dockerfile                                                   0.0s
 =&amp;gt; =&amp;gt; transferring dockerfile: 358B                                                                         0.0s
 =&amp;gt; [hmall internal] load metadata for docker.io/library/openjdk:11.0-jre-buster                             0.0s
 =&amp;gt; [hmall internal] load .dockerignore                                                                      0.0s
 =&amp;gt; =&amp;gt; transferring context: 2B                                                                              0.0s
 =&amp;gt; [hmall 1/3] FROM docker.io/library/openjdk:11.0-jre-buster                                               0.0s
 =&amp;gt; [hmall internal] load build context                                                                      0.0s
 =&amp;gt; =&amp;gt; transferring context: 98B                                                                             0.0s
 =&amp;gt; CACHED [hmall 2/3] RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &amp;amp;&amp;amp; echo Asia/Shanghai &amp;gt;  0.0s
 =&amp;gt; CACHED [hmall 3/3] COPY hm-service.jar /app.jar                                                          0.0s
 =&amp;gt; [hmall] exporting to image                                                                               0.0s
 =&amp;gt; =&amp;gt; exporting layers                                                                                      0.0s
 =&amp;gt; =&amp;gt; writing image sha256:164bc4806232609ccf993f792dd31f00c5af7c1d51734487f2dce67a334fe87b                 0.0s
 =&amp;gt; =&amp;gt; naming to docker.io/library/root-hmall                                                                0.0s
[+] Running 4/4
 ✔ Network root_hmall-network  Created                                                                       0.2s
 ✔ Container mysql             Started                                                                       0.8s
 ✔ Container hmall             Started                                                                       1.3s
 ✔ Container nginx             Started                                                                       2.0s
[root@localhost ~]#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动成功后，就会在镜像中自动生成root-hmall的镜像&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;命令&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose [OPTIONS] [COMMAND]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，OPTIONS和COMMAND都是可选参数，比较常见的有：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;参数或指令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Options&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;指定 &lt;code&gt;compose&lt;/code&gt; 文件的路径和名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;指定项目名称。&lt;code&gt;project&lt;/code&gt; 就是当前 &lt;code&gt;compose&lt;/code&gt; 文件中设置的多个 &lt;code&gt;service&lt;/code&gt; 的集合，是逻辑概念&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Commands&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;up&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;创建并启动所有 &lt;code&gt;service&lt;/code&gt; 容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;停止并移除所有容器、网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出所有启动的容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看指定容器的日志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;停止容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启动容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重启容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;top&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看运行的进程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在指定的运行中容器中执行命令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>Elasticsearch</title><link>https://zzyang.top/posts/elastic-search/</link><guid isPermaLink="true">https://zzyang.top/posts/elastic-search/</guid><pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Elasticsearch&lt;/h1&gt;
&lt;p&gt;MySql存在的问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;查询效率较低&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于数据库模糊查询不走索引，在数据量较大的时候，查询性能很差。&lt;/p&gt;
&lt;p&gt;数据库模糊查询随着表数据量的增多，查询性能的下降会非常明显，而搜索引擎的性能则不会随着数据增多而下降太多。目前仅10万不到的数据量差距就如此明显，如果数据量达到百万、千万、甚至上亿级别，这个性能差距会非常夸张。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;功能单一&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;数据库的模糊搜索功能单一，匹配条件非常苛刻，必须恰好包含用户搜索的关键字。而在搜索引擎中，用户输入出现个别错字，或者用拼音搜索、同义词搜索都能正确匹配到数据。&lt;/p&gt;
&lt;p&gt;综上，在面临海量数据的搜索，或者有一些复杂搜索需求的时候，推荐使用专门的搜索引擎来实现搜索功能。&lt;/p&gt;
&lt;p&gt;目前全球的搜索引擎技术排名如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-10_20-28-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;认识ES&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Lucene&lt;/code&gt;是一个Java语言的搜索引擎类库，是Apache公司的顶级项目，由DougCutting于1999年研发。官网地址：https://lucene.apache.org/ 。&lt;/p&gt;
&lt;p&gt;Lucene的优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;易扩展&lt;/li&gt;
&lt;li&gt;高性能（基于倒排索引）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;2004年Shay Banon基于Lucene开发了Compass&lt;/li&gt;
&lt;li&gt;2010年Shay Banon 重写了Compass，取名为Elasticsearch。&lt;/li&gt;
&lt;li&gt;官网地址: https://www.elastic.co/cn/ ，目前最新的版本是：8.x.x&lt;/li&gt;
&lt;li&gt;elasticsearch具备下列优势：
&lt;ol&gt;
&lt;li&gt;支持分布式，可水平扩展&lt;/li&gt;
&lt;li&gt;提供Restful接口，可被任何语言调用&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;code&gt;Elasticsearch&lt;/code&gt;是由&lt;code&gt;elastic&lt;/code&gt;公司开发的一套搜索引擎技术，它是&lt;code&gt;elastic&lt;/code&gt;技术栈中的一部分。完整的技术栈包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Elasticsearch&lt;/code&gt;：用于数据存储、计算和搜索&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Logstash/Beats&lt;/code&gt;：用于数据收集&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Kibana&lt;/code&gt;：用于数据可视化
整套技术栈被称为&lt;code&gt;ELK&lt;/code&gt;，经常用来做日志收集、系统监控和状态分析等等：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整套技术栈的核心就是用来存储、搜索、计算的&lt;code&gt;Elasticsearch&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们要安装的内容包含2部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;elasticsearch&lt;/code&gt;：存储、搜索和运算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kibana&lt;/code&gt;：图形化展示&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先Elasticsearch不用多说，是提供核心的数据存储、搜索、分析功能的。&lt;/p&gt;
&lt;p&gt;然后是Kibana，Elasticsearch对外提供的是Restful风格的API，任何操作都可以通过发送http请求来完成。不过http请求的方式、路径、还有请求参数的格式都有严格的规范。这些规范我们肯定记不住，因此我们要借助于Kibana这个服务。&lt;/p&gt;
&lt;p&gt;Kibana是elastic公司提供的用于操作Elasticsearch的可视化控制台。它的功能非常强大，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对Elasticsearch数据的搜索、展示&lt;/li&gt;
&lt;li&gt;对Elasticsearch数据的统计、聚合，并形成图形化报表、图形&lt;/li&gt;
&lt;li&gt;对Elasticsearch的集群状态监控&lt;/li&gt;
&lt;li&gt;它还提供了一个开发控制台（DevTools），在其中对Elasticsearch的Restful的API接口提供了语法提示&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装ES&lt;/h2&gt;
&lt;p&gt;上传好我们的镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker load -i es.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker load -i kibana.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过下面的Docker命令即可安装单机版本的elasticsearch：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
  --name es \
  -e &quot;ES_JAVA_OPTS=-Xms512m -Xmx512m&quot; \
  -e &quot;discovery.type=single-node&quot; \
  -v es-data:/usr/share/elasticsearch/data \
  -v es-plugins:/usr/share/elasticsearch/plugins \
  --privileged \
  --network hm-net \
  -p 9200:9200 \
  -p 9300:9300 \
  elasticsearch:7.12.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info
&lt;code&gt;9200端口&lt;/code&gt;：我们访问的http端口&lt;/p&gt;
&lt;p&gt;&lt;code&gt;9300端口&lt;/code&gt;：将来若有集群，集群间通讯端口&lt;/p&gt;
&lt;p&gt;注意，这里我们采用的是elasticsearch的7.12.1版本，由于8以上版本的JavaAPI变化很大，在企业中应用并不广泛，企业中应用较多的还是8以下的版本。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;查看下运行日志&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker logs -f es
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，访问9200端口，即可看到响应的Elasticsearch服务的基本信息&lt;/p&gt;
&lt;p&gt;访问该地址：http://192.168.146.131:9200/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-10_21-18-09.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;安装Kibana&lt;/h2&gt;
&lt;p&gt;kibana的作用：方便的操作es(通过发送请求的方式)&lt;/p&gt;
&lt;p&gt;通过下面的Docker命令，即可部署Kibana：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hm-net \
-p 5601:5601  \
kibana:7.12.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
-e 环境变量配的是es的地址，用于操作es；该地址写的是&lt;code&gt;es&lt;/code&gt;,这是容器名，要根据es部署时的容器名来定；当然这里也可以写虚拟机ip地址；&lt;/p&gt;
&lt;p&gt;注意es与kibana网络也必须在同一个网络下
:::&lt;/p&gt;
&lt;p&gt;安装完成后，直接访问5601端口，
http://192.168.146.131:5601/&lt;/p&gt;
&lt;p&gt;选择&lt;code&gt;Explore on my own&lt;/code&gt;之后，进入主页面：&lt;/p&gt;
&lt;p&gt;然后选中&lt;code&gt;Dev tools&lt;/code&gt;，进入开发工具页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-10_21-25-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;倒排索引&lt;/h2&gt;
&lt;p&gt;elasticsearch之所以有如此高性能的搜索表现，正是得益于底层的倒排索引技术。&lt;/p&gt;
&lt;p&gt;倒排索引的概念是基于MySQL这样的正向索引而言的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正向索引&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们先来回顾一下正向索引。
例如有一张名为tb_goods的表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;小米手机&lt;/td&gt;
&lt;td&gt;3499&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;华为手机&lt;/td&gt;
&lt;td&gt;4999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;华为小米充电器&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;小米手环&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中的&lt;code&gt;id&lt;/code&gt;字段已经创建了索引，由于索引底层采用了B+树结构，因此我们根据id搜索的速度会非常快。但是其他字段例如&lt;code&gt;title&lt;/code&gt;，只在叶子节点上存在。&lt;/p&gt;
&lt;p&gt;因此要根据&lt;code&gt;title&lt;/code&gt;搜索的时候只能遍历树中的每一个叶子节点，判断title数据是否符合要求。&lt;/p&gt;
&lt;p&gt;比如用户的SQL语句为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from tb_goods where title like &apos;%手机%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那搜索的大概流程如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-10_21-34-57.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1）检查到搜索条件为like &apos;%手机%&apos;，需要找到title中包含手机的数据&lt;/li&gt;
&lt;li&gt;2）逐条遍历每行数据（每个叶子节点），比如第1次拿到id为1的数据&lt;/li&gt;
&lt;li&gt;3）判断数据中的title字段值是否符合条件&lt;/li&gt;
&lt;li&gt;4）如果符合则放入结果集，不符合则丢弃&lt;/li&gt;
&lt;li&gt;5）回到步骤1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上，根据id精确匹配时，可以走索引，查询效率较高。而当搜索条件为模糊匹配时，由于索引无法生效，导致从索引查询退化为全表扫描，效率很差。&lt;/p&gt;
&lt;p&gt;因此，正向索引适合于根据索引字段的精确搜索，不适合基于部分词条的模糊匹配。&lt;/p&gt;
&lt;p&gt;而倒排索引恰好解决的就是根据部分词条模糊匹配的问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;倒排索引&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;倒排索引中有两个非常重要的概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文档（&lt;code&gt;Document&lt;/code&gt;）：用来搜索的数据，其中的每一条数据就是一个文档。例如一个网页、一个商品信息&lt;/li&gt;
&lt;li&gt;词条（&lt;code&gt;Term&lt;/code&gt;）：文档按照语义分成的词语；对文档数据或用户搜索数据，利用某种算法分词，得到的具备含义的词语就是词条。例如：我是中国人，就可以分为：我、是、中国人、中国、国人这样的几个词条&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;创建倒排索引&lt;/strong&gt;是对正向索引的一种特殊处理和应用，流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将每一个文档的数据利用&lt;strong&gt;分词算法&lt;/strong&gt;根据语义拆分，得到一个个词条&lt;/li&gt;
&lt;li&gt;创建表，每行数据包括词条、词条所在文档id、位置等信息&lt;/li&gt;
&lt;li&gt;因为词条唯一性，可以给词条创建&lt;strong&gt;正向&lt;/strong&gt;索引
此时形成的这张以词条为索引的表，就是倒排索引表，两者对比如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-10_21-53-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;倒排索引的搜索流程如下（以搜索&quot;华为手机&quot;为例），如图：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-10_21-51-37.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;流程描述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1）用户输入条件&quot;华为手机&quot;进行搜索。&lt;/li&gt;
&lt;li&gt;2）对用户输入条件&lt;strong&gt;分词&lt;/strong&gt;，得到词条：华为、手机。&lt;/li&gt;
&lt;li&gt;3）拿着词条在倒排索引中查找（&lt;strong&gt;由于词条有索引，查询效率很高&lt;/strong&gt;），即可得到包含词条的文档id：1、2、3。&lt;/li&gt;
&lt;li&gt;4）拿着文档&lt;code&gt;id&lt;/code&gt;到正向索引中查找具体文档即可（由于&lt;code&gt;id&lt;/code&gt;也有索引，查询效率也很高）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然要先查询倒排索引，再查询正向索引，但是无论是词条、还是文档id都建立了索引，查询速度非常快！无需全表扫描。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;什么是文档和词条？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;每一条数据就是一个文档&lt;/li&gt;
&lt;li&gt;对文档中的内容分词，得到的词语就是词条&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;什么是正向索引？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;基于文档id创建索引。根据id查询快，但是查询词条时必须先找到文档，而后判断是否包含词条&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;什么是倒排索引？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;对文档内容分词，对词条创建索引，并记录词条所在文档的id。查询时先根据词条查询到文档id，而后根据文档id查询文档&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IK分词器&lt;/h2&gt;
&lt;p&gt;中文分词往往需要根据语义分析，比较复杂，这就需要用到中文分词器，例如&lt;strong&gt;IK分词器&lt;/strong&gt;。IK分词器是林良益在2006年开源发布的，其采用的正向迭代最细粒度切分算法一直沿用至今。&lt;/p&gt;
&lt;p&gt;其安装的方式也比较简单&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 下载ik分词器这个插件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;github 链接：https://github.com/medcl/elasticsearch-analysis-ik&lt;/p&gt;
&lt;p&gt;github 下载地址：https://github.com/medcl/elasticsearch-analysis-ik/releases&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 方案1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;运行一个命令即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it es ./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重启es容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker restart es
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 方案2&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果网速较差，也可以选择离线安装。
首先，查看之前安装的Elasticsearch容器的plugins数据卷目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker volume inspect es-plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到&lt;code&gt;elasticsearch&lt;/code&gt;的插件挂载到了&lt;code&gt;/var/lib/docker/volumes/es-plugins/_data&lt;/code&gt;这个目录。我们需要把IK分词器上传至这个目录。&lt;/p&gt;
&lt;p&gt;找到ik分词器插件，安装的是&lt;code&gt;7.12.1&lt;/code&gt;版本的ik分词器压缩文件;&lt;/p&gt;
&lt;p&gt;然后上传至虚拟机的&lt;code&gt;/var/lib/docker/volumes/es-plugins/_data&lt;/code&gt;这个目录：&lt;/p&gt;
&lt;p&gt;最后，重启es容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker restart es
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看下日志&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker logs -f es
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到如下字样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;message&quot;: &quot;loaded plugin [analysis-ik]&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;使用IK分词器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;IK分词器包含两种模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ik_smart&lt;/code&gt;：智能语义切分&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ik_max_word&lt;/code&gt;：最细粒度切分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们在Kibana的DevTools上来测试分词器，首先测试Elasticsearch官方提供的标准分词器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /_analyze
{
  &quot;analyzer&quot;: &quot;standard&quot;,
  &quot;text&quot;: &quot;黑马程序员学习java太棒了&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;tokens&quot; : [
    {
      &quot;token&quot; : &quot;黑&quot;,
      &quot;start_offset&quot; : 0,
      &quot;end_offset&quot; : 1,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 0
    },
    {
      &quot;token&quot; : &quot;马&quot;,
      &quot;start_offset&quot; : 1,
      &quot;end_offset&quot; : 2,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 1
    },
    {
      &quot;token&quot; : &quot;程&quot;,
      &quot;start_offset&quot; : 2,
      &quot;end_offset&quot; : 3,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 2
    },
    {
      &quot;token&quot; : &quot;序&quot;,
      &quot;start_offset&quot; : 3,
      &quot;end_offset&quot; : 4,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 3
    },
    {
      &quot;token&quot; : &quot;员&quot;,
      &quot;start_offset&quot; : 4,
      &quot;end_offset&quot; : 5,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 4
    },
    {
      &quot;token&quot; : &quot;学&quot;,
      &quot;start_offset&quot; : 5,
      &quot;end_offset&quot; : 6,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 5
    },
    {
      &quot;token&quot; : &quot;习&quot;,
      &quot;start_offset&quot; : 6,
      &quot;end_offset&quot; : 7,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 6
    },
    {
      &quot;token&quot; : &quot;java&quot;,
      &quot;start_offset&quot; : 7,
      &quot;end_offset&quot; : 11,
      &quot;type&quot; : &quot;&amp;lt;ALPHANUM&amp;gt;&quot;,
      &quot;position&quot; : 7
    },
    {
      &quot;token&quot; : &quot;太&quot;,
      &quot;start_offset&quot; : 11,
      &quot;end_offset&quot; : 12,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 8
    },
    {
      &quot;token&quot; : &quot;棒&quot;,
      &quot;start_offset&quot; : 12,
      &quot;end_offset&quot; : 13,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 9
    },
    {
      &quot;token&quot; : &quot;了&quot;,
      &quot;start_offset&quot; : 13,
      &quot;end_offset&quot; : 14,
      &quot;type&quot; : &quot;&amp;lt;IDEOGRAPHIC&amp;gt;&quot;,
      &quot;position&quot; : 10
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，标准分词器智能1字1词条，无法正确对中文做分词。&lt;/p&gt;
&lt;p&gt;我们再测试IK分词器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /_analyze
{
  &quot;analyzer&quot;: &quot;ik_smart&quot;,
  &quot;text&quot;: &quot;黑马程序员学习java太棒了&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;tokens&quot; : [
    {
      &quot;token&quot; : &quot;黑马&quot;,
      &quot;start_offset&quot; : 0,
      &quot;end_offset&quot; : 2,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 0
    },
    {
      &quot;token&quot; : &quot;程序员&quot;,
      &quot;start_offset&quot; : 2,
      &quot;end_offset&quot; : 5,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 1
    },
    {
      &quot;token&quot; : &quot;学习&quot;,
      &quot;start_offset&quot; : 5,
      &quot;end_offset&quot; : 7,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 2
    },
    {
      &quot;token&quot; : &quot;java&quot;,
      &quot;start_offset&quot; : 7,
      &quot;end_offset&quot; : 11,
      &quot;type&quot; : &quot;ENGLISH&quot;,
      &quot;position&quot; : 3
    },
    {
      &quot;token&quot; : &quot;太棒了&quot;,
      &quot;start_offset&quot; : 11,
      &quot;end_offset&quot; : 14,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 4
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;POST /_analyze
{
  &quot;analyzer&quot;: &quot;ik_max_word&quot;,
  &quot;text&quot;: &quot;黑马程序员学习java太棒了&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;tokens&quot; : [
    {
      &quot;token&quot; : &quot;黑马&quot;,
      &quot;start_offset&quot; : 0,
      &quot;end_offset&quot; : 2,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 0
    },
    {
      &quot;token&quot; : &quot;程序员&quot;,
      &quot;start_offset&quot; : 2,
      &quot;end_offset&quot; : 5,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 1
    },
    {
      &quot;token&quot; : &quot;程序&quot;,
      &quot;start_offset&quot; : 2,
      &quot;end_offset&quot; : 4,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 2
    },
    {
      &quot;token&quot; : &quot;员&quot;,
      &quot;start_offset&quot; : 4,
      &quot;end_offset&quot; : 5,
      &quot;type&quot; : &quot;CN_CHAR&quot;,
      &quot;position&quot; : 3
    },
    {
      &quot;token&quot; : &quot;学习&quot;,
      &quot;start_offset&quot; : 5,
      &quot;end_offset&quot; : 7,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 4
    },
    {
      &quot;token&quot; : &quot;java&quot;,
      &quot;start_offset&quot; : 7,
      &quot;end_offset&quot; : 11,
      &quot;type&quot; : &quot;ENGLISH&quot;,
      &quot;position&quot; : 5
    },
    {
      &quot;token&quot; : &quot;太棒了&quot;,
      &quot;start_offset&quot; : 11,
      &quot;end_offset&quot; : 14,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 6
    },
    {
      &quot;token&quot; : &quot;太棒&quot;,
      &quot;start_offset&quot; : 11,
      &quot;end_offset&quot; : 13,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 7
    },
    {
      &quot;token&quot; : &quot;了&quot;,
      &quot;start_offset&quot; : 13,
      &quot;end_offset&quot; : 14,
      &quot;type&quot; : &quot;CN_CHAR&quot;,
      &quot;position&quot; : 8
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;拓展词典&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;随着互联网的发展，“造词运动”也越发的频繁。出现了很多新的词语，在原有的词汇列表中并不存在。比如：“泰裤辣”，“传智播客” 等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /_analyze
{
  &quot;analyzer&quot;: &quot;ik_max_word&quot;,
  &quot;text&quot;: &quot;传智播客开设大学,真的泰裤辣！&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，传智播客和泰裤辣都无法正确分词。&lt;/p&gt;
&lt;p&gt;所以要想正确分词，IK分词器的词库也需要不断的更新，IK分词器提供了扩展词汇的功能。&lt;/p&gt;
&lt;p&gt;1）打开IK分词器config目录：&lt;code&gt;/var/lib/docker/volumes/es-plugins/_data/elasticsearch-analysis-ik-7.12.1/config/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IKAnalyzer.cfg.xml&lt;/code&gt;文件&lt;/p&gt;
&lt;p&gt;2）在&lt;code&gt;IKAnalyzer.cfg.xml&lt;/code&gt;配置文件内容添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE properties SYSTEM &quot;http://java.sun.com/dtd/properties.dtd&quot;&amp;gt;
&amp;lt;properties&amp;gt;
	&amp;lt;comment&amp;gt;IK Analyzer 扩展配置&amp;lt;/comment&amp;gt;
	&amp;lt;!--用户可以在这里配置自己的扩展字典 --&amp;gt;
	&amp;lt;entry key=&quot;ext_dict&quot;&amp;gt;ext.dic&amp;lt;/entry&amp;gt;
	 &amp;lt;!--用户可以在这里配置自己的扩展停止词字典--&amp;gt;
	&amp;lt;entry key=&quot;ext_stopwords&quot;&amp;gt;&amp;lt;/entry&amp;gt;
	&amp;lt;!--用户可以在这里配置远程扩展字典 --&amp;gt;
	&amp;lt;!-- &amp;lt;entry key=&quot;remote_ext_dict&quot;&amp;gt;words_location&amp;lt;/entry&amp;gt; --&amp;gt;
	&amp;lt;!--用户可以在这里配置远程扩展停止词字典--&amp;gt;
	&amp;lt;!-- &amp;lt;entry key=&quot;remote_ext_stopwords&quot;&amp;gt;words_location&amp;lt;/entry&amp;gt; --&amp;gt;
&amp;lt;/properties&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3）在IK分词器的config目录新建一个 &lt;code&gt;ext.dic&lt;/code&gt;，可以参考config目录下复制一个配置文件进行修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;传智播客
泰裤辣
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4）重启&lt;em&gt;elasticsearch&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker restart es

# 查看 日志
docker logs -f es
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次测试，可以发现传智播客和泰裤辣都正确分词了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;tokens&quot; : [
    {
      &quot;token&quot; : &quot;传智播客&quot;,
      &quot;start_offset&quot; : 0,
      &quot;end_offset&quot; : 4,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 0
    },
    {
      &quot;token&quot; : &quot;开设&quot;,
      &quot;start_offset&quot; : 4,
      &quot;end_offset&quot; : 6,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 1
    },
    {
      &quot;token&quot; : &quot;大学&quot;,
      &quot;start_offset&quot; : 6,
      &quot;end_offset&quot; : 8,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 2
    },
    {
      &quot;token&quot; : &quot;真的&quot;,
      &quot;start_offset&quot; : 9,
      &quot;end_offset&quot; : 11,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 3
    },
    {
      &quot;token&quot; : &quot;泰裤辣&quot;,
      &quot;start_offset&quot; : 11,
      &quot;end_offset&quot; : 14,
      &quot;type&quot; : &quot;CN_WORD&quot;,
      &quot;position&quot; : 4
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分词器的作用是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建倒排索引时，对文档分词&lt;/li&gt;
&lt;li&gt;用户搜索时，对输入的内容分词&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IK分词器有几种模式？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ik_smart&lt;/code&gt;：智能切分，粗粒度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ik_max_word&lt;/code&gt;：最细切分，细粒度IK分词器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如何拓展分词器词库中的词条？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用&lt;code&gt;config&lt;/code&gt;目录的&lt;code&gt;IkAnalyzer.cfg.xml&lt;/code&gt;文件添加拓展词典&lt;/li&gt;
&lt;li&gt;在词典中添加拓展词条&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;索引库操作&lt;/h2&gt;
&lt;h3&gt;基础概念&lt;/h3&gt;
&lt;p&gt;索引（index）：相同类型的文档的集合&lt;/p&gt;
&lt;p&gt;映射（mapping）：索引中文档的字段约束信息，类似表的结构约束
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/wechat_2025-06-11_222039_990.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-11_22-25-52.png&quot; alt=&quot;&quot; /&gt;
&lt;code&gt;Index&lt;/code&gt;就类似数据库表，&lt;code&gt;Mapping&lt;/code&gt;映射就类似表的结构。我们要向&lt;code&gt;es&lt;/code&gt;中存储数据，必须先创建&lt;code&gt;Index&lt;/code&gt;和&lt;code&gt;Mapping&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Mapping映射属性&lt;/h3&gt;
&lt;p&gt;Mapping是对索引库中文档的约束，常见的Mapping属性包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：字段数据类型，常见的简单类型有：
&lt;ul&gt;
&lt;li&gt;字符串：text（可分词的文本）、keyword（精确值，例如：品牌、国家、ip地址）&lt;/li&gt;
&lt;li&gt;数值：long、integer、short、byte、double、float、&lt;/li&gt;
&lt;li&gt;布尔：boolean&lt;/li&gt;
&lt;li&gt;日期：date&lt;/li&gt;
&lt;li&gt;对象：object&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index&lt;/code&gt;：是否创建索引，默认为&lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;analyzer&lt;/code&gt;：使用哪种分词器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;properties&lt;/code&gt;：该字段的子字段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如下面的json文档：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;age&quot;: 21,
    &quot;weight&quot;: 52.1,
    &quot;isMarried&quot;: false,
    &quot;info&quot;: &quot;黑马程序员Java讲师&quot;,
    &quot;email&quot;: &quot;zy@itcast.cn&quot;,
    &quot;score&quot;: [99.1, 99.5, 98.9],
    &quot;name&quot;: {
        &quot;firstName&quot;: &quot;云&quot;,
        &quot;lastName&quot;: &quot;赵&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;score是float类型&lt;/li&gt;
&lt;li&gt;如果对score进行排序，排序时，ES很智能，降序选取值最大的，升序选取值最小的&lt;/li&gt;
&lt;li&gt;是否需要创建索引，要看该字段是否需要搜索和排序
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;索引库的CRUD&lt;/h3&gt;
&lt;p&gt;由于Elasticsearch采用的是&lt;code&gt;Restful&lt;/code&gt;风格的API，因此其请求方式和路径相对都比较规范，而且请求参数也都采用JSON风格。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-12_21-27-08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;创建索引库和映射&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;基本语法：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求方式：PUT&lt;/li&gt;
&lt;li&gt;请求路径：/索引库名，可以自定义&lt;/li&gt;
&lt;li&gt;请求参数：mapping映射&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUT /索引库名称
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;字段名&quot;:{
        &quot;type&quot;: &quot;text&quot;,
        &quot;analyzer&quot;: &quot;ik_smart&quot;
      },
      &quot;字段名2&quot;:{
        &quot;type&quot;: &quot;keyword&quot;,
        &quot;index&quot;: &quot;false&quot;
      },
      &quot;字段名3&quot;:{
        &quot;properties&quot;: {
          &quot;子字段&quot;: {
            &quot;type&quot;: &quot;keyword&quot;
          }
        }
      },
      // ...略
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUT /heima
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;info&quot;:{
        &quot;type&quot;:&quot;text&quot;,
        &quot;analyzer&quot;: &quot;ik_smart&quot;
      },
      &quot;age&quot;:{
        &quot;type&quot;: &quot;byte&quot;
      },
      &quot;email&quot;:{
        &quot;type&quot;: &quot;keyword&quot;,
        &quot;index&quot;: false
      },
      &quot;name&quot;:{
        &quot;type&quot;: &quot;object&quot;,
        &quot;properties&quot;: {
          &quot;firstName&quot;:{
            &quot;type&quot;:&quot;keyword&quot;
          },
          &quot;lastName&quot;:{
            &quot;type&quot;:&quot;keyword&quot;
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;acknowledged&quot; : true,
  &quot;shards_acknowledged&quot; : true,
  &quot;index&quot; : &quot;heima&quot;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;查询索引库&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;基本语法：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求方式：GET&lt;/li&gt;
&lt;li&gt;请求路径：/索引库名&lt;/li&gt;
&lt;li&gt;请求参数：无&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /索引库名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /heima
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除索引库&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;语法：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求方式：DELETE&lt;/li&gt;
&lt;li&gt;请求路径：/索引库名&lt;/li&gt;
&lt;li&gt;请求参数：无&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE /索引库名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE /heima
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;修改索引库&lt;/h4&gt;
&lt;p&gt;倒排索引结构虽然不复杂，但是一旦数据结构改变（比如改变了分词器），就需要重新创建倒排索引，这简直是灾难。因此索引库&lt;strong&gt;一旦创建，无法修改mapping&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;虽然无法修改mapping中已有的字段，但是却允许添加新的字段到mapping中，因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段，或者更新索引库的基础属性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;语法说明：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUT /索引库名/_mapping
{
  &quot;properties&quot;: {
    &quot;新字段名&quot;:{
      &quot;type&quot;: &quot;integer&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUT /heima/_mapping
{
  &quot;properties&quot;: {
    &quot;age&quot;:{
      &quot;type&quot;: &quot;integer&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;PUT /heima/_mapping
{
  &quot;properties&quot;:{
    &quot;otherInfo&quot;:{
      &quot;type&quot;:&quot;text&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;索引库操作有哪些？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建索引库：PUT /索引库名&lt;/li&gt;
&lt;li&gt;查询索引库：GET /索引库名&lt;/li&gt;
&lt;li&gt;删除索引库：DELETE /索引库名&lt;/li&gt;
&lt;li&gt;修改索引库，添加字段：PUT /索引库名/_mapping&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到，对索引库的操作基本遵循的Restful的风格，因此API接口非常统一，方便记忆。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;文档CRUD&lt;/h3&gt;
&lt;p&gt;有了索引库，接下来就可以向索引库中添加数据了。&lt;/p&gt;
&lt;h4&gt;新增文档&lt;/h4&gt;
&lt;p&gt;语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /索引库名/_doc/文档id
{
    &quot;字段1&quot;: &quot;值1&quot;,
    &quot;字段2&quot;: &quot;值2&quot;,
    &quot;字段3&quot;: {
        &quot;子属性1&quot;: &quot;值3&quot;,
        &quot;子属性2&quot;: &quot;值4&quot;
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /heima/_doc/1
{
  &quot;info&quot;: &quot;zxyang study java&quot;,
    &quot;email&quot;: &quot;zzyang.cn&quot;,
    &quot;name&quot;: {
        &quot;firstName&quot;: &quot;云&quot;,
        &quot;lastName&quot;: &quot;赵&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;_index&quot; : &quot;heima&quot;,
  &quot;_type&quot; : &quot;_doc&quot;,
  &quot;_id&quot; : &quot;1&quot;,
  &quot;_version&quot; : 1,
  &quot;result&quot; : &quot;created&quot;,
  &quot;_shards&quot; : {
    &quot;total&quot; : 2,
    &quot;successful&quot; : 1,
    &quot;failed&quot; : 0
  },
  &quot;_seq_no&quot; : 0,
  &quot;_primary_term&quot; : 2
}

// 下划线代表内置变量，跟Python类似
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;查询文档&lt;/h4&gt;
&lt;p&gt;根据&lt;code&gt;Restful&lt;/code&gt;风格，新增是post，查询应该是get，不过查询一般都需要条件，这里我们把文档id带上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /{索引库名称}/_doc/{id}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /heima/_doc/1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;_index&quot; : &quot;heima&quot;,
  &quot;_type&quot; : &quot;_doc&quot;,
  &quot;_id&quot; : &quot;1&quot;,
  &quot;_version&quot; : 1,
  &quot;_seq_no&quot; : 0,
  &quot;_primary_term&quot; : 2,
  &quot;found&quot; : true,
  &quot;_source&quot; : {
    &quot;info&quot; : &quot;zxyang study java&quot;,
    &quot;email&quot; : &quot;zzyang.cn&quot;,
    &quot;name&quot; : {
      &quot;firstName&quot; : &quot;云&quot;,
      &quot;lastName&quot; : &quot;赵&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除文档&lt;/h4&gt;
&lt;p&gt;删除使用DELETE请求，同样，需要根据id进行删除：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE /{索引库名}/_doc/id值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE /heima/_doc/1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;_index&quot; : &quot;heima&quot;,
  &quot;_type&quot; : &quot;_doc&quot;,
  &quot;_id&quot; : &quot;1&quot;,
  &quot;_version&quot; : 2,
  &quot;result&quot; : &quot;deleted&quot;,
  &quot;_shards&quot; : {
    &quot;total&quot; : 2,
    &quot;successful&quot; : 1,
    &quot;failed&quot; : 0
  },
  &quot;_seq_no&quot; : 1,
  &quot;_primary_term&quot; : 2
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;修改文档&lt;/h4&gt;
&lt;p&gt;修改有两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全量修改：直接覆盖原来的文档&lt;/li&gt;
&lt;li&gt;局部修改：修改文档中的部分字段&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;全量修改&lt;/h5&gt;
&lt;p&gt;全量修改，会删除旧文档，添加新文档&lt;/p&gt;
&lt;p&gt;全量修改是覆盖原来的文档，其本质是两步操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据指定的id删除文档&lt;/li&gt;
&lt;li&gt;新增一个相同id的文档&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：如果根据id删除时，id不存在，第二步的新增也会执行，也就从修改变成了新增操作了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;语法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUT /{索引库名}/_doc/文档id
{
    &quot;字段1&quot;: &quot;值1&quot;,
    &quot;字段2&quot;: &quot;值2&quot;,
    // ... 略
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUT /heima/_doc/1
{
  &quot;info&quot;: &quot;zxyang STUDY java&quot;,
    &quot;email&quot;: &quot;ZZyang.cn&quot;,
    &quot;name&quot;: {
        &quot;firstName&quot;: &quot;云&quot;,
        &quot;lastName&quot;: &quot;赵&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;_index&quot; : &quot;heima&quot;,
  &quot;_type&quot; : &quot;_doc&quot;,
  &quot;_id&quot; : &quot;1&quot;,
  &quot;_version&quot; : 2,
  &quot;result&quot; : &quot;updated&quot;,
  &quot;_shards&quot; : {
    &quot;total&quot; : 2,
    &quot;successful&quot; : 1,
    &quot;failed&quot; : 0
  },
  &quot;_seq_no&quot; : 3,
  &quot;_primary_term&quot; : 2
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;局部修改&lt;/h5&gt;
&lt;p&gt;也叫&lt;strong&gt;增量修改&lt;/strong&gt;，
局部修改是只修改指定id匹配的文档中的部分字段。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;语法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /{索引库名}/_update/文档id
{
    &quot;doc&quot;: {
         &quot;字段名&quot;: &quot;新的值&quot;,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /heima/_update/1
{
  &quot;doc&quot;: {
    &quot;email&quot;: &quot;aaaa.cc&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;_index&quot; : &quot;heima&quot;,
  &quot;_type&quot; : &quot;_doc&quot;,
  &quot;_id&quot; : &quot;1&quot;,
  &quot;_version&quot; : 3,
  &quot;result&quot; : &quot;updated&quot;,
  &quot;_shards&quot; : {
    &quot;total&quot; : 2,
    &quot;successful&quot; : 1,
    &quot;failed&quot; : 0
  },
  &quot;_seq_no&quot; : 4,
  &quot;_primary_term&quot; : 2
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：不要使用全量修改的方式去修改部分字段，这样会导致修改后的数据，字段仅剩你修改的那一个了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;文档操作有哪些？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建文档：&lt;code&gt;POST /{索引库名}/_doc/文档id{ json文档 }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查询文档：&lt;code&gt;GET /{索引库名}/_doc/文档id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;删除文档：&lt;code&gt;DELETE /{索引库名}/_doc/文档id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改文档：
&lt;ul&gt;
&lt;li&gt;全量修改：&lt;code&gt;PUT /{索引库名}/_doc/文档id { json文档 }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;局部修改：&lt;code&gt;POST /{索引库名}/_update/文档id { &quot;doc&quot;: {字段}}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;批量处理&lt;/h4&gt;
&lt;p&gt;批处理采用POST请求，基本语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST _bulk
{ &quot;index&quot; : { &quot;_index&quot; : &quot;test&quot;, &quot;_id&quot; : &quot;1&quot; } }
{ &quot;field1&quot; : &quot;value1&quot; }
{ &quot;delete&quot; : { &quot;_index&quot; : &quot;test&quot;, &quot;_id&quot; : &quot;2&quot; } }
{ &quot;create&quot; : { &quot;_index&quot; : &quot;test&quot;, &quot;_id&quot; : &quot;3&quot; } }
{ &quot;field1&quot; : &quot;value3&quot; }
{ &quot;update&quot; : {&quot;_id&quot; : &quot;1&quot;, &quot;_index&quot; : &quot;test&quot;} }
{ &quot;doc&quot; : {&quot;field2&quot; : &quot;value2&quot;} }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;索引操作（Index）效果：如果文档 ID=1 已存在，则替换它；如果不存在，则创建新文档&lt;/p&gt;
&lt;p&gt;删除操作（Delete）效果： 删除 ID=2 的文档（如果存在）&lt;/p&gt;
&lt;p&gt;创建操作（Create）效果：创建 ID=3 的新文档，如果该 ID 已存在，则操作会失败(报错)&lt;/p&gt;
&lt;p&gt;更新操作（Update）效果：对 ID=1 的文档进行部分更新&lt;/p&gt;
&lt;p&gt;这里的&lt;code&gt;test&lt;/code&gt;是目标索引库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_index&lt;/code&gt;：指定索引库名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_id&lt;/code&gt;指定要操作的文档&lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;示例，批量新增：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /_bulk
{&quot;index&quot;: {&quot;_index&quot;:&quot;heima&quot;, &quot;_id&quot;: &quot;3&quot;}}
{&quot;info&quot;: &quot;黑马程序员C++讲师&quot;, &quot;email&quot;: &quot;ww@itcast.cn&quot;, &quot;name&quot;:{&quot;firstName&quot;: &quot;五&quot;, &quot;lastName&quot;:&quot;王&quot;}}
{&quot;index&quot;: {&quot;_index&quot;:&quot;heima&quot;, &quot;_id&quot;: &quot;4&quot;}}
{&quot;info&quot;: &quot;黑马程序员前端讲师&quot;, &quot;email&quot;: &quot;zhangsan@itcast.cn&quot;, &quot;name&quot;:{&quot;firstName&quot;: &quot;三&quot;, &quot;lastName&quot;:&quot;张&quot;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
注意这里的新增信息，不能换行
:::&lt;/p&gt;
&lt;p&gt;批量删除：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /_bulk
{&quot;delete&quot;:{&quot;_index&quot;:&quot;heima&quot;, &quot;_id&quot;: &quot;3&quot;}}
{&quot;delete&quot;:{&quot;_index&quot;:&quot;heima&quot;, &quot;_id&quot;: &quot;4&quot;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;RestClient&lt;/h3&gt;
&lt;p&gt;ES官方提供了各种不同语言的客户端，用来操作ES。这些客户端的本质就是组装DSL语句，通过http请求发送给ES。&lt;/p&gt;
&lt;p&gt;官方文档地址：
https://www.elastic.co/guide/en/elasticsearch/client/index.html&lt;/p&gt;
&lt;p&gt;由于ES目前最新版本是8.8，提供了全新版本的客户端，老版本的客户端已经被标记为过时。而我们采用的是&lt;code&gt;7.12版本&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;注意&amp;lt;/summary&amp;gt;
7.15以后的新版本都是基于lambda表达式的写法了；项目中用新版本的注意下；
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h4&gt;初始化RestClient&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;elasticsearch&lt;/code&gt;提供的API中，与&lt;code&gt;elasticsearch&lt;/code&gt;一切交互都封装在一个名为&lt;code&gt;RestHighLevelClient&lt;/code&gt;的类中，必须先完成这个对象的初始化，建立与&lt;code&gt;elasticsearch&lt;/code&gt;的连接。&lt;/p&gt;
&lt;p&gt;1）在item-service模块中引入es的RestHighLevelClient依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.elasticsearch.client&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;elasticsearch-rest-high-level-client&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）因为SpringBoot默认的ES版本是7.17.10，所以我们需要覆盖默认的ES版本：&lt;/p&gt;
&lt;p&gt;在父工程hmall的pom.xml中覆盖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;properties&amp;gt;
      &amp;lt;maven.compiler.source&amp;gt;11&amp;lt;/maven.compiler.source&amp;gt;
      &amp;lt;maven.compiler.target&amp;gt;11&amp;lt;/maven.compiler.target&amp;gt;
      &amp;lt;elasticsearch.version&amp;gt;7.12.1&amp;lt;/elasticsearch.version&amp;gt;
  &amp;lt;/properties&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3）初始化RestHighLevelClient：&lt;/p&gt;
&lt;p&gt;初始化的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
        HttpHost.create(&quot;http://192.168.150.101:9200&quot;)
));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在该服务下建一个测试，测试一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ElasticSearchTest {

    private RestHighLevelClient restHighLevelClient;

    @Test
    void testConnection() {
        System.out.println(restHighLevelClient);
    }

    @BeforeEach
    void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                HttpHost.create(&quot;http://192.168.146.131:9200&quot;)
        ));
    }

    @AfterEach
    void tearDown() throws IOException {
        if (restHighLevelClient != null) {
            restHighLevelClient.close();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如是集群部署，可以多节点配置​​&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;restHighLevelClient = new RestHighLevelClient(
    RestClient.builder(
        new HttpHost(&quot;192.168.146.131&quot;, 9200, &quot;http&quot;),
        new HttpHost(&quot;192.168.146.132&quot;, 9200, &quot;http&quot;)
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@BeforeEach&lt;/code&gt;初始化方法, 表示该方法在每个测试方法执行前都会运行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterEach&lt;/code&gt; 表示该方法在每个测试方法执行后都会运行
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;商品的Mapping映射&lt;/h4&gt;
&lt;p&gt;由于要实现对商品搜索，所以我们需要将商品添加到Elasticsearch中，不过需要根据搜索业务的需求来设定索引库结构，而不是一股脑的把MySQL数据写入Elasticsearch.&lt;/p&gt;
&lt;p&gt;实现搜索功能需要的字段包括三大部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;搜索过滤字段
&lt;ul&gt;
&lt;li&gt;分类&lt;/li&gt;
&lt;li&gt;品牌&lt;/li&gt;
&lt;li&gt;价格&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;排序字段
&lt;ul&gt;
&lt;li&gt;默认：按照更新时间降序排序&lt;/li&gt;
&lt;li&gt;销量&lt;/li&gt;
&lt;li&gt;价格&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;展示字段
&lt;ul&gt;
&lt;li&gt;商品id：用于点击后跳转&lt;/li&gt;
&lt;li&gt;图片地址&lt;/li&gt;
&lt;li&gt;是否是广告推广商品&lt;/li&gt;
&lt;li&gt;名称&lt;/li&gt;
&lt;li&gt;价格&lt;/li&gt;
&lt;li&gt;评价数量&lt;/li&gt;
&lt;li&gt;销量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是页面上展示的这些，都要存到es中
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-15_19-49-47.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;结合数据库表结构，以上字段对应的mapping映射属性如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-15_19-56-44.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此，最终我们的索引库文档结构应该是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUT /items
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;id&quot;: {
        &quot;type&quot;: &quot;keyword&quot;
      },
      &quot;name&quot;:{
        &quot;type&quot;: &quot;text&quot;,
        &quot;analyzer&quot;: &quot;ik_max_word&quot;
      },
      &quot;price&quot;:{
        &quot;type&quot;: &quot;integer&quot;
      },
      &quot;image&quot;:{
        &quot;type&quot;: &quot;keyword&quot;,
        &quot;index&quot;: false
      },
      &quot;category&quot;:{
        &quot;type&quot;: &quot;keyword&quot;
      },
      &quot;brand&quot;:{
        &quot;type&quot;: &quot;keyword&quot;
      },
      &quot;sold&quot;:{
        &quot;type&quot;: &quot;integer&quot;
      },
      &quot;commentCount&quot;:{
        &quot;type&quot;: &quot;integer&quot;,
        &quot;index&quot;: false
      },
      &quot;isAD&quot;:{
        &quot;type&quot;: &quot;boolean&quot;
      },
      &quot;updateTime&quot;:{
        &quot;type&quot;: &quot;date&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;索引库操作&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-15_20-32-21.png&quot; alt=&quot;&quot; /&gt;
代码分为三步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1）创建Request对象。
&lt;ul&gt;
&lt;li&gt;因为是创建索引库的操作，因此Request是CreateIndexRequest。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2）添加请求参数
&lt;ul&gt;
&lt;li&gt;其实就是Json格式的Mapping映射参数。因为json字符串很长，这里是定义了静态字符串常量MAPPING_TEMPLATE，让代码看起来更加优雅。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;3）发送请求
&lt;ul&gt;
&lt;li&gt;client.indices()方法的返回值是IndicesClient类型，封装了所有与索引库操作有关的方法。例如创建索引、删除索引、判断索引是否存在等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-15_20-34-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;创建索引库及mapping映射：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ElasticSearchTest {

    private RestHighLevelClient restHighLevelClient;

    @Test
    void testConnection() {
        System.out.println(restHighLevelClient);
    }

    @BeforeEach
    void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                HttpHost.create(&quot;http://192.168.146.131:9200&quot;)
        ));
    }

    @AfterEach
    void tearDown() throws IOException {
        if (restHighLevelClient != null) {
            restHighLevelClient.close();
        }
    }

    @Test
    void testCreateIndex() throws IOException {
        // 1.准备request对象
        CreateIndexRequest request = new CreateIndexRequest(&quot;items&quot;);
        // 2.准备请求参数
        request.source(MAPPING_TEMPLATE, XContentType.JSON);
        // 3.发送请求
        restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
    }

    private final static String MAPPING_TEMPLATE = &quot;{\n&quot; +
            &quot;  \&quot;mappings\&quot;: {\n&quot; +
            &quot;    \&quot;properties\&quot;: {\n&quot; +
            &quot;      \&quot;id\&quot;: {\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;keyword\&quot;\n&quot; +
            &quot;      },\n&quot; +
            &quot;      \&quot;name\&quot;:{\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;text\&quot;,\n&quot; +
            &quot;        \&quot;analyzer\&quot;: \&quot;ik_max_word\&quot;\n&quot; +
            &quot;      },\n&quot; +
            &quot;      \&quot;price\&quot;:{\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;integer\&quot;\n&quot; +
            &quot;      },\n&quot; +
            &quot;      \&quot;image\&quot;:{\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;keyword\&quot;,\n&quot; +
            &quot;        \&quot;index\&quot;: false\n&quot; +
            &quot;      },\n&quot; +
            &quot;      \&quot;category\&quot;:{\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;keyword\&quot;\n&quot; +
            &quot;      },\n&quot; +
            &quot;      \&quot;brand\&quot;:{\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;keyword\&quot;\n&quot; +
            &quot;      },\n&quot; +
            &quot;      \&quot;sold\&quot;:{\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;integer\&quot;\n&quot; +
            &quot;      },\n&quot; +
            &quot;      \&quot;commentCount\&quot;:{\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;integer\&quot;,\n&quot; +
            &quot;        \&quot;index\&quot;: false\n&quot; +
            &quot;      },\n&quot; +
            &quot;      \&quot;isAD\&quot;:{\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;boolean\&quot;\n&quot; +
            &quot;      },\n&quot; +
            &quot;      \&quot;updateTime\&quot;:{\n&quot; +
            &quot;        \&quot;type\&quot;: \&quot;date\&quot;\n&quot; +
            &quot;      }\n&quot; +
            &quot;    }\n&quot; +
            &quot;  }\n&quot; +
            &quot;}&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查询索引库&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testGetIndex() throws IOException {
        // 1.准备request对象
        GetIndexRequest request = new GetIndexRequest(&quot;items&quot;);
        // 3.发送请求
        GetIndexResponse getIndexResponse = restHighLevelClient.indices().get(request, RequestOptions.DEFAULT);
        System.out.println(getIndexResponse);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查是否存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testGetIndex() throws IOException {
        // 1.准备request对象
        GetIndexRequest request = new GetIndexRequest(&quot;items&quot;);
        // 3.发送请求
        boolean exists = restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
        System.out.println(exists);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;1）创建Request对象。这次是&lt;code&gt;GetIndexRequest&lt;/code&gt;对象&lt;/li&gt;
&lt;li&gt;2）准备参数。这里是无参，直接省略&lt;/li&gt;
&lt;li&gt;3）发送请求。改用exists方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;删除索引库&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testDeleteIndex() throws IOException {
        // 1.准备request对象
        DeleteIndexRequest request = new DeleteIndexRequest(&quot;items&quot;);
        // 3.发送请求
        restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意体现在Request对象上。流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1）创建Request对象。这次是&lt;code&gt;DeleteIndexRequest&lt;/code&gt;对象&lt;/li&gt;
&lt;li&gt;2）准备参数。这里是无参，因此省略&lt;/li&gt;
&lt;li&gt;3）发送请求。改用delete方法&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JavaRestClient&lt;/code&gt;操作elasticsearch的流程基本类似。核心是&lt;code&gt;client.indices()&lt;/code&gt;方法来获取索引库的操作对象。
索引库操作的基本步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化&lt;code&gt;RestHighLevelClient&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建XxxIndexRequest。XXX是&lt;code&gt;Create、Get、Delete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;准备请求参数（ Create时需要，其它是无参，可以省略）&lt;/li&gt;
&lt;li&gt;发送请求。调用&lt;code&gt;RestHighLevelClient#indices().xxx()&lt;/code&gt;方法，xxx是create、exists、delete&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;RestClient操作文档&lt;/h4&gt;
&lt;p&gt;索引库准备好以后，就可以操作文档了。为了与索引库操作分离，我们再次创建一个测试类&lt;/p&gt;
&lt;h5&gt;新增文档&lt;/h5&gt;
&lt;p&gt;新增文档的请求语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /{索引库名}/_doc/1
{
    &quot;name&quot;: &quot;Jack&quot;,
    &quot;age&quot;: 21
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的JavaAPI如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-16_19-44-12.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;索引库结构与数据库结构还存在一些差异，因此我们要定义一个索引库结构对应的实体。&lt;/p&gt;
&lt;p&gt;在item-service模块的com.hmall.item.domain.po包中定义一个新的：&lt;code&gt;ItemDoc&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.hmall.item.domain.po;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@ApiModel(description = &quot;索引库实体&quot;)
public class ItemDoc{

    @ApiModelProperty(&quot;商品id&quot;)
    private String id;

    @ApiModelProperty(&quot;商品名称&quot;)
    private String name;

    @ApiModelProperty(&quot;价格（分）&quot;)
    private Integer price;

    @ApiModelProperty(&quot;商品图片&quot;)
    private String image;

    @ApiModelProperty(&quot;类目名称&quot;)
    private String category;

    @ApiModelProperty(&quot;品牌名称&quot;)
    private String brand;

    @ApiModelProperty(&quot;销量&quot;)
    private Integer sold;

    @ApiModelProperty(&quot;评论数&quot;)
    private Integer commentCount;

    @ApiModelProperty(&quot;是否是推广广告，true/false&quot;)
    private Boolean isAD;

    @ApiModelProperty(&quot;更新时间&quot;)
    private LocalDateTime updateTime;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试类代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest(properties = &quot;spring.profiles.active=local&quot;)
public class ElasticSearchDocumentTest {

    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private IItemService itemService;


    @BeforeEach
    void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                HttpHost.create(&quot;http://192.168.146.131:9200&quot;)
        ));
    }

    @AfterEach
    void tearDown() throws IOException {
        if (restHighLevelClient != null) {
            restHighLevelClient.close();
        }
    }

    @Test
    void testIndexDoc() throws IOException {
        // 0.准备文档数据
        Item item = itemService.getById(546872L);
        // 把数据库数据转为文档数据
        ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
        // 1. 准备request
        IndexRequest request = new IndexRequest(&quot;items&quot;).id(item.getId().toString());
        // 2.准备请求参数
        request.source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON);
        // 3.发送请求
        restHighLevelClient.index(request, RequestOptions.DEFAULT);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到与索引库操作的API非常类似，同样是三步走：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1）创建Request对象，这里是&lt;code&gt;IndexRequest&lt;/code&gt;，因为添加文档就是创建倒排索引的过程&lt;/li&gt;
&lt;li&gt;2）准备请求参数，本例中就是Json文档&lt;/li&gt;
&lt;li&gt;3）发送请求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;变化的地方在于，这里直接使用&lt;code&gt;client.xxx()&lt;/code&gt;的API，不再需要&lt;code&gt;client.indices()&lt;/code&gt;了。&lt;/p&gt;
&lt;p&gt;:::info
&lt;code&gt;new IndexRequest&lt;/code&gt;这里的Index会先判断这个id是否存在，如果存在就会update，如果不存在就会create，并且这个id是可以不指定的，会自动创建&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@SpringBootTest&lt;/code&gt;，这是 Spring Boot 测试的核心注解，它会：​启动完整的 Spring 应用上下文​​（包括所有自动配置的 bean）、​模拟真实的应用程序启动过程​​、​​ 提供测试所需的完整环境​​（包括配置属性、数据库连接等）、支持依赖注入​​（可以使用 @Autowired 注入任何 Spring 管理的 bean）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;properties = &quot;spring.profiles.active=local&quot; 参数&lt;/code&gt;，这会加载&lt;code&gt;application-local.yml&lt;/code&gt; 配置文件，即使应用默认配置中设置了其他 profile，测试时也会强制使用 &quot;local&quot; profile&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BeanUtil.copyProperties&lt;/code&gt;(hutool) 根据一个已有的 Java Bean 对象（item），创建一个新的 ItemDoc 类型的对象，并将属性名相同的字段值复制过去。
:::&lt;/p&gt;
&lt;h5&gt;查询文档&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;GET /{索引库名}/_doc/{id}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-16_20-17-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testGetDoc() throws IOException {
        // 1. 准备request
        GetRequest request = new GetRequest(&quot;items&quot;, &quot;546872&quot;);
        // 2.发送请求
        GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
        // 3.解析结果
        String json = response.getSourceAsString();
        ItemDoc doc = JSONUtil.toBean(json, ItemDoc.class);
        System.out.println(&quot;doc = &quot; + doc);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;1）准备Request对象。这次是查询，所以是GetRequest&lt;/li&gt;
&lt;li&gt;2）发送请求，得到结果。因为是查询，这里调用client.get()方法&lt;/li&gt;
&lt;li&gt;3）解析结果，就是对JSON做反序列化&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;删除文档&lt;/h5&gt;
&lt;p&gt;删除的请求语句如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE /hotel/_doc/{id}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应API：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testDeleteDoc() throws IOException {
        // 1. 准备request
        DeleteRequest request = new DeleteRequest(&quot;items&quot;, &quot;546872&quot;);
        // 2.发送请求
        restHighLevelClient.delete(request, RequestOptions.DEFAULT);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;修改文档&lt;/h5&gt;
&lt;p&gt;修改文档数据有两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方式一：全量更新。再次写入id一样的文档，就会删除旧文档，添加新文档。与新增的JavaAPI一致。&lt;/li&gt;
&lt;li&gt;方式二：局部更新。只更新指定部分字段。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;全量更新&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;全量更新和新增是一样的操作API，如果这个id的数据存在则修改，不存在则添加&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testIndexDoc() throws IOException {
        // 0.准备文档数据
        Item item = itemService.getById(546872L);
        // 把数据库数据转为文档数据
        ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
        itemDoc.setPrice(9999999);
        // 1. 准备request
        IndexRequest request = new IndexRequest(&quot;items&quot;).id(item.getId().toString());
        // 2.准备请求参数
        request.source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON);
        // 3.发送请求
        IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
        System.out.println(&quot;response = &quot; + response);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;局部更新&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /{索引库名}/_update/{id}
{
  &quot;doc&quot;: {
    &quot;字段名&quot;: &quot;字段值&quot;,
    &quot;字段名&quot;: &quot;字段值&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-16_20-48-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testUpdateDoc() throws IOException {
        // 1. 准备request
        UpdateRequest request = new UpdateRequest(&quot;items&quot;, &quot;546872&quot;);
        // 2. 准备请求参数
        request.doc(  // 这里也可以用Map
                &quot;price&quot;, &quot;666&quot;,
                &quot;sold&quot;, &quot;36&quot;
        );
        // 3.发送请求
        restHighLevelClient.update(request, RequestOptions.DEFAULT);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;文档操作的基本步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化RestHighLevelClient&lt;/li&gt;
&lt;li&gt;创建XxxRequest。XXX是Index、Get、Update、Delete&lt;/li&gt;
&lt;li&gt;准备参数（Index和Update时需要）&lt;/li&gt;
&lt;li&gt;发送请求。调用RestHighLevelClient#.xxx()方法，xxx是index、get、update、delete&lt;/li&gt;
&lt;li&gt;解析结果（Get时需要）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;批处理&lt;/h4&gt;
&lt;p&gt;批处理与前面讲的文档的CRUD步骤基本一致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建&lt;code&gt;Request&lt;/code&gt;，但这次用的是&lt;code&gt;BulkRequest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;准备请求参数&lt;/li&gt;
&lt;li&gt;发送请求，这次要用到&lt;code&gt;client.bulk()&lt;/code&gt;方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;BulkRequest&lt;/code&gt;本身其实并没有请求参数，其本质就是将多个普通的CRUD请求组合在一起发送。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;批量新增文档，就是给每个文档创建一个&lt;code&gt;IndexRequest&lt;/code&gt;请求，然后封装到&lt;code&gt;BulkRequest&lt;/code&gt;中，一起发出。&lt;/li&gt;
&lt;li&gt;批量删除，就是创建N个&lt;code&gt;DeleteRequest&lt;/code&gt;请求，然后封装到&lt;code&gt;BulkRequest&lt;/code&gt;，一起发出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此&lt;code&gt;BulkRequest&lt;/code&gt;中提供了add方法，用以添加其它CRUD的请求：&lt;/p&gt;
&lt;p&gt;可以看到，能添加的请求有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IndexRequest&lt;/code&gt;，也就是新增&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UpdateRequest&lt;/code&gt;，也就是修改&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DeleteRequest&lt;/code&gt;，也就是删除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-16_21-17-10.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-16_21-16-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当我们要导入商品数据时，由于商品数量达到数十万，因此不可能一次性全部导入。建议采用循环遍历方式，每次导入1000条左右的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testBulkDoc() throws IOException {
        int pageNo = 1, pageSize = 500;
        // 准备文档数据
        while (true) {
            Page&amp;lt;Item&amp;gt; page = itemService.lambdaQuery()
                    .eq(Item::getStatus, 1)
                    .page(Page.of(pageNo, pageSize));
            List&amp;lt;Item&amp;gt; records = page.getRecords();
            if (records == null || records.isEmpty()) {
                return;
            }

            // 1. 准备request
            BulkRequest request = new BulkRequest();
            // 2. 准备请求参数
            for (Item item : records) {
                request.add(new IndexRequest(&quot;items&quot;)
                        .id(item.getId().toString())
                        .source(JSONUtil.toJsonStr(BeanUtil.copyProperties(item, ItemDoc.class)), XContentType.JSON));
            }
            // 3.发送请求
            restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
            pageNo++;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看索引库有多少条文档数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET items/_count
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DSL查询&lt;/h3&gt;
&lt;p&gt;我们来研究下elasticsearch的数据搜索功能。Elasticsearch提供了基于JSON的DSL&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/7.12/query-dsl.html&quot;&gt;(Domain Specific Language)&lt;/a&gt;语句来定义查询条件，其JavaAPI就是在组织DSL条件。&lt;/p&gt;
&lt;p&gt;因此，我们先学习DSL的查询语法，然后再基于DSL来对照学习JavaAPI，就会事半功倍。&lt;/p&gt;
&lt;p&gt;Elasticsearch的查询可以分为两大类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;叶子查询（Leaf query clauses）&lt;/strong&gt;：一般是在特定的字段里查询特定值，属于简单查询，很少单独使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复合查询（Compound query clauses）&lt;/strong&gt;：以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在查询以后，还可以对查询的结果做处理，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;排序：按照1个或多个字段值做排序&lt;/li&gt;
&lt;li&gt;分页：根据from和size做分页，类似MySQL&lt;/li&gt;
&lt;li&gt;高亮：对搜索结果中的关键字添加特殊样式，使其更加醒目&lt;/li&gt;
&lt;li&gt;聚合：对搜索结果做数据统计以形成报表&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;快速入门&lt;/h4&gt;
&lt;p&gt;我们依然在Kibana的DevTools中学习查询的DSL语法。首先来看查询的语法结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /{索引库名}/_search
{
  &quot;query&quot;: {
    &quot;查询类型&quot;: {
      // .. 查询条件
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /{索引库名}/_search&lt;/code&gt;：其中的_search是固定路径，不能修改&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，我们以最简单的无条件查询为例，无条件查询的类型是：match_all，因此其查询语句如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;query&quot;: {
    &quot;match_all&quot;: {

    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于match_all无条件，所以条件位置不写即可。&lt;/p&gt;
&lt;p&gt;执行结果如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-16_22-07-10.png&quot; alt=&quot;&quot; /&gt;
你会发现虽然是&lt;code&gt;match_all&lt;/code&gt;，但是响应结果中并不会包含索引库中的所有文档，而是仅有&lt;code&gt;10条&lt;/code&gt;。这是因为处于安全考虑，&lt;code&gt;elasticsearch&lt;/code&gt;设置了默认的查询页数。&lt;/p&gt;
&lt;h4&gt;叶子查询&lt;/h4&gt;
&lt;p&gt;叶子查询还可以进一步细分，常见的有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全文检索&lt;/strong&gt;（full text）查询：利用分词器对用户输入内容分词，然后去词条列表中匹配。例如：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;match_query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;multi_match_query&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;精确查询&lt;/strong&gt;：不对用户输入内容分词，直接精确匹配，一般是查找keyword、数值、日期、布尔等类型。例如：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ids&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;range&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;term&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地理（geo）查询&lt;/strong&gt;：用于搜索地理位置，搜索方式很多。例如：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;geo_distance&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;geo_bounding_box&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;全文检索查询&lt;/h5&gt;
&lt;p&gt;全文检索的种类也很多，详情可以参考官方文档：https://www.elastic.co/guide/en/elasticsearch/reference/7.12/full-text-queries.html&lt;/p&gt;
&lt;p&gt;以全文检索中的match为例，语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /{索引库名}/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;字段名&quot;: &quot;搜索条件&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET items/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;name&quot;: &quot;脱脂牛奶&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应结果：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-16_22-43-54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;::: tip
基础相关性评分（_score）：
按照匹配度打分排名，相关度评分是基于BM25（Best Matching 25）算法来计算的，这是一种改进的TF-IDF（Term Frequency-Inverse Document Frequency）算法。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;与&lt;code&gt;match&lt;/code&gt;类似的还有&lt;code&gt;multi_match&lt;/code&gt;，区别在于可以同时对多个字段搜索，条件是或的关系，满足 (字段1 有 搜索条件) || (字段2 有 搜索条件) 就会被搜索出来，语法示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /{索引库名}/_search
{
  &quot;query&quot;: {
    &quot;multi_match&quot;: {
      &quot;query&quot;: &quot;搜索条件&quot;,
      &quot;fields&quot;: [&quot;字段1&quot;, &quot;字段2&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;​query&lt;/code&gt;​​：要搜索的文本内容;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fields&lt;/code&gt;​​：要在哪些字段中搜索该内容&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;multi_match&lt;/code&gt; 查询的 &lt;code&gt;fields&lt;/code&gt; 参数中，字段之间的关系是 &lt;strong&gt;​​&quot;或&quot;（OR）关系&lt;/strong&gt;​​，而不是&quot;与&quot;（AND）关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET items/_search
{
  &quot;query&quot;: {&quot;multi_match&quot;: {
    &quot;query&quot;: &quot;小米&quot;,
    &quot;fields&quot;: [&quot;name&quot;,&quot;brand&quot;]
  }}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;精确查询&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;精确查询&lt;/strong&gt;，英文是&lt;code&gt;Term-level query&lt;/code&gt;，顾名思义，词条级别的查询。也就是说不会对用户输入的搜索条件再分词，而是作为一个词条，与搜索的字段内容精确值匹配。&lt;/p&gt;
&lt;p&gt;因此推荐查找keyword、数值、日期、boolean类型的字段。例如id、price、城市、地名、人名等作为一个整体才有含义的字段。&lt;/p&gt;
&lt;p&gt;详情可以查看官方文档：&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/7.12/term-level-queries.html&quot;&gt;es文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;以&lt;strong&gt;term查询&lt;/strong&gt;为例，其语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /{索引库名}/_search
{
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;字段名&quot;: {
        &quot;value&quot;: &quot;搜索条件&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET items/_search
{
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;brand&quot;: {
        &quot;value&quot;: &quot;德亚&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
当你输入的搜索条件不是词条，而是短语时，由于不做分词，你反而搜索不到！
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;再来看下&lt;strong&gt;range查询&lt;/strong&gt;，语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /{索引库名}/_search
{
  &quot;query&quot;: {
    &quot;range&quot;: {
      &quot;字段名&quot;: {
        &quot;gte&quot;: {最小值},
        &quot;lte&quot;: {最大值}
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;range是范围查询，对于范围筛选的关键字有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gte：大于等于&lt;/li&gt;
&lt;li&gt;gt：大于&lt;/li&gt;
&lt;li&gt;lte：小于等于&lt;/li&gt;
&lt;li&gt;lt：小于&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET items/_search
{
  &quot;query&quot;: {
    &quot;range&quot;: {
      &quot;price&quot;: {
        &quot;gte&quot;: 10000,
        &quot;lte&quot;: 20000
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;根据ids查询&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;query&quot;: {
    &quot;ids&quot;: {
      &quot;values&quot;: [
        &quot;613358&quot;,
        &quot;584392&quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
需要注意的是精确搜索是没有得分的，没有分数排名，因为是精确搜索，大家分数都一样
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;match&lt;/code&gt;和&lt;code&gt;multi_match&lt;/code&gt;的区别是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;match&lt;/code&gt;：根据一个字段查询&lt;/li&gt;
&lt;li&gt;&lt;code&gt;multi_match&lt;/code&gt;：根据多个字段查询，参与查询字段越多，查询性能越差&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;精确查询常见的有哪些？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;term查询&lt;/code&gt;：根据词条精确匹配，一般搜索keyword类型、数值类型、布尔类型、日期类型字段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;range查询&lt;/code&gt;：根据数值范围查询，可以是数值、日期的范围&lt;/li&gt;
&lt;li&gt;&lt;code&gt;根据ids查询&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;复合查询&lt;/h4&gt;
&lt;p&gt;复合查询大致可以分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一类：基于逻辑运算组合叶子查询，实现组合条件，例如&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;bool&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第二类：基于某种算法修改查询时的文档相关性算分，从而改变文档排名。例如：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;function_score&lt;/p&gt;
&lt;p&gt;dis_max&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其它复合查询及相关语法可以参考官方文档：
&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/7.12/compound-queries.html&quot;&gt;es文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;bool查询&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;bool查询，即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool查询支持的逻辑运算有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;must&lt;/code&gt;：必须匹配每个子查询，类似“与”&lt;/li&gt;
&lt;li&gt;&lt;code&gt;should&lt;/code&gt;：选择性匹配子查询，类似“或”&lt;/li&gt;
&lt;li&gt;&lt;code&gt;must_not&lt;/code&gt;：必须不匹配，不参与算分，类似“非”&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter&lt;/code&gt;：必须匹配，不参与算分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;bool查询的语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {&quot;match&quot;: {&quot;name&quot;: &quot;手机&quot;}}
      ],
      &quot;should&quot;: [
        {&quot;term&quot;: {&quot;brand&quot;: { &quot;value&quot;: &quot;vivo&quot; }}},
        {&quot;term&quot;: {&quot;brand&quot;: { &quot;value&quot;: &quot;小米&quot; }}}
      ],
      &quot;must_not&quot;: [
        {&quot;range&quot;: {&quot;price&quot;: {&quot;gte&quot;: 2500}}}
      ],
      &quot;filter&quot;: [
        {&quot;range&quot;: {&quot;price&quot;: {&quot;lte&quot;: 1000}}}
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需求：我们要搜索&quot;智能手机&quot;，但品牌必须是华为，价格必须是900~1599&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;出于性能考虑，与搜索关键字无关的查询尽量采用&lt;code&gt;must_not&lt;/code&gt;或&lt;code&gt;filter&lt;/code&gt;逻辑运算，避免参与相关性算分。&lt;/p&gt;
&lt;p&gt;例如商城的搜索页面：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-17_21-05-56.png&quot; alt=&quot;&quot; /&gt;
其中输入框的搜索条件肯定要参与相关性算分，可以采用&lt;code&gt;must&lt;/code&gt;。但是价格范围过滤、品牌过滤、分类过滤等尽量采用&lt;code&gt;filter&lt;/code&gt;，不要参与相关性算分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {
          &quot;match&quot;: {
            &quot;name&quot;: &quot;智能手机&quot;
          }
        }
      ],
      &quot;filter&quot;: [
        {
          &quot;term&quot;: {
            &quot;brand&quot;: &quot;华为&quot;
          }
        },
        {
          &quot;range&quot;: {
            &quot;price&quot;: {
              &quot;gte&quot;: 90000,
              &quot;lte&quot;: 159900
            }
          }
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-17_21-00-22.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::info
一般来说用户通过关键字进行搜索，我们是需要进行算分的(_score)，而通过条件进行筛选的我们是不需要算分的，比如价格区间筛选等，es即使这样；&lt;/p&gt;
&lt;p&gt;而且参与算分的越多，那么性能肯定也是会下降的，效率会变低；
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;排序和分页&lt;/h4&gt;
&lt;p&gt;语法说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /indexName/_search
{
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;sort&quot;: [
    {
      &quot;排序字段&quot;: {
        &quot;order&quot;: &quot;排序方式asc和desc&quot;
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sort关键字：进行排序操作，sort的值是一个数组，所以可以指定多个排序的字段&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需求:搜索商品，按照销量排序，销量一样则按照价格升序&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;sort&quot;: [
    {
      &quot;sold&quot;: &quot;desc&quot;
    },
    {
      &quot;price&quot;: &quot;asc&quot;
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;分页&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。&lt;/p&gt;
&lt;p&gt;elasticsearch中通过修改from、size参数来控制要返回的分页结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;from：从第几个文档开始&lt;/li&gt;
&lt;li&gt;size：总共查询几个文档
类似于mysql中的&lt;code&gt;limit ?, ?&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方文档如下：&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html&quot;&gt;文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;from&quot;: 0, // 分页开始的位置，默认为0
  &quot;size&quot;: 10,  // 每页文档数量，默认10
  &quot;sort&quot;: [
    {
      &quot;price&quot;: {
        &quot;order&quot;: &quot;desc&quot;
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;需求:搜索商品，查询出销量排名前10的商品，销量一样时按照价格升序&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;sort&quot;: [
    {
      &quot;sold&quot;: &quot;desc&quot;
    },
    {
      &quot;price&quot;: &quot;asc&quot;
    }
  ],
  &quot;from&quot;: 0,
  &quot;size&quot;: 10
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;&lt;strong&gt;深度分页&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;elasticsearch的数据一般会采用分片存储，也就是把一个索引中的数据分成N份，存储到不同节点上。这种存储方式比较有利于数据扩展，但给分页带来了一些麻烦。&lt;/p&gt;
&lt;p&gt;比如一个索引库中有100000条数据，分别存储到4个分片，每个分片25000条数据。现在每页查询10条，查询第99页。那么分页查询的条件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;from&quot;: 990, // 从第990条开始查询
  &quot;size&quot;: 10, // 每页查询10条
  &quot;sort&quot;: [
    {
      &quot;price&quot;: &quot;asc&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从语句来分析，要查询第990~1000名的数据。&lt;/p&gt;
&lt;p&gt;从实现思路来分析，肯定是将所有数据排序，找出前1000名，截取其中的990~1000的部分。但问题来了，我们如何才能找到所有数据中的前1000名呢？&lt;/p&gt;
&lt;p&gt;要知道每一片的数据都不一样，第1片上的第900~1000，在另1个节点上并不一定依然是900~1000名。所以我们只能在每一个分片上都找出排名前1000的数据，然后汇总到一起，重新排序，才能找出整个索引库中真正的前1000名，此时截取990~1000的数据即可。&lt;/p&gt;
&lt;p&gt;如图：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-17_21-44-27.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;试想一下，假如我们现在要查询的是第999页数据呢，是不是要找第9990~10000的数据，那岂不是需要把每个分片中的前10000名数据都查询出来，汇总在一起，在内存中排序？如果查询的分页深度更深呢，需要一次检索的数据岂不是更多？&lt;/p&gt;
&lt;p&gt;由此可知，当查询分页深度较大时，汇总数据过多，对内存和CPU会产生非常大的压力。&lt;/p&gt;
&lt;p&gt;因此elasticsearch会禁止&lt;strong&gt;from+size&lt;/strong&gt; 超过10000的请求。&lt;/p&gt;
&lt;p&gt;针对深度分页，elasticsearch提供了两种解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;search after&lt;/code&gt;：分页时需要排序，原理是从上一次的排序值开始，查询下一页数据。官方推荐使用的方式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scroll&lt;/code&gt;：原理将排序后的文档id形成快照，保存下来，基于快照做分页。官方已经不推荐使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;search after&lt;/code&gt;模式：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;优点：没有查询上限，支持深度分页&lt;/p&gt;
&lt;p&gt;缺点：只能向后逐页查询，不能随机翻页&lt;/p&gt;
&lt;p&gt;场景：数据迁移、手机滚动查询&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;详情见文档：&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html&quot;&gt;文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;::: tip
&lt;code&gt;search after&lt;/code&gt;的原理就是，当翻到某一页时，会记住当前的最后一页，那么下次再翻页时，就会把上次记住的那一页作为查询条件继续翻页，条件是大于上次记住的那一页，size限制xxx页数，这样就实现了每次翻页都是第一页&lt;/p&gt;
&lt;p&gt;假设你在看一本书，你已经翻到某一页（例如第10页），然后你想继续看第11页。search_after就像是“给你上一页的最后一行文字”，让你知道接下来你应该从哪里开始读，而不是从头开始翻书（就像from那样跳过很多页）
:::&lt;/p&gt;
&lt;p&gt;:::warning
总结：&lt;/p&gt;
&lt;p&gt;大多数情况下，我们采用普通分页就可以了。查看百度、京东等网站，会发现其分页都有限制。例如百度最多支持77页，每页不足20条。京东最多100页，每页最多60条。
因此，一般我们采用限制分页深度的方式即可，无需实现深度分页。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;高亮&lt;/h4&gt;
&lt;p&gt;什么是高亮显示呢？&lt;/p&gt;
&lt;p&gt;我们在百度，京东搜索时，关键字会变成红色，比较醒目，这叫高亮显示：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-17_22-14-16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;观察页面源码，你会发现两件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高亮词条都被加了&lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt;标签&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt;标签都添加了红色样式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;css样式肯定是前端实现页面的时候写好的，但是前端编写页面的时候是不知道页面要展示什么数据的，不可能给数据加标签。而服务端实现搜索功能，要是有elasticsearch做分词搜索，是知道哪些词条需要高亮的。&lt;/p&gt;
&lt;p&gt;因此词条的&lt;strong&gt;高亮标签肯定是由服务端提供数据的时候已经加上的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此实现高亮的思路就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户输入搜索关键字搜索数据&lt;/li&gt;
&lt;li&gt;服务端根据搜索关键字到elasticsearch搜索，并给搜索结果中的关键字词条添加html标签&lt;/li&gt;
&lt;li&gt;前端提前给约定好的html标签添加CSS样式&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;实现高亮&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;事实上elasticsearch已经提供了给搜索关键字加标签的语法，无需我们自己编码。&lt;/p&gt;
&lt;p&gt;基本语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /{索引库名}/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;搜索字段&quot;: &quot;搜索关键字&quot;
    }
  },
  &quot;highlight&quot;: {
    &quot;fields&quot;: {
      &quot;高亮字段名称&quot;: {
        &quot;pre_tags&quot;: &quot;&amp;lt;em&amp;gt;&quot;,
        &quot;post_tags&quot;: &quot;&amp;lt;/em&amp;gt;&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;name&quot;: &quot;脱脂牛奶&quot;
    }
  },
  &quot;highlight&quot;: {
    &quot;fields&quot;: {
      &quot;name&quot;: {
        &quot;pre_tags&quot;: &quot;&amp;lt;em&amp;gt;&quot;,
        &quot;post_tags&quot;: &quot;&amp;lt;/em&amp;gt;&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部分响应结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;took&quot; : 547,
  &quot;timed_out&quot; : false,
  &quot;_shards&quot; : {
    &quot;total&quot; : 1,
    &quot;successful&quot; : 1,
    &quot;skipped&quot; : 0,
    &quot;failed&quot; : 0
  },
  &quot;hits&quot; : {
    &quot;total&quot; : {
      &quot;value&quot; : 1149,
      &quot;relation&quot; : &quot;eq&quot;
    },
    &quot;max_score&quot; : 16.198345,
    &quot;hits&quot; : [
      {
        &quot;_index&quot; : &quot;items&quot;,
        &quot;_type&quot; : &quot;_doc&quot;,
        &quot;_id&quot; : &quot;33449279171&quot;,
        &quot;_score&quot; : 16.198345,
        &quot;_source&quot; : {
          &quot;id&quot; : &quot;33449279171&quot;,
          &quot;name&quot; : &quot;意大利 进口牛奶 葛兰纳诺脱脂纯牛奶 成人牛奶  进口脱脂纯牛奶1Lx6盒&quot;,
          &quot;price&quot; : 3500,
          &quot;image&quot; : &quot;https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/25045/9/2656/164517/5c20699dE9b7f4c9c/1a05e9bdd2c5d59e.jpg!q70.jpg.webp&quot;,
          &quot;category&quot; : &quot;牛奶&quot;,
          &quot;brand&quot; : &quot;葛兰纳诺&quot;,
          &quot;sold&quot; : 0,
          &quot;commentCount&quot; : 0,
          &quot;isAD&quot; : false,
          &quot;updateTime&quot; : 1556640000000
        },
        &quot;highlight&quot; : {
          &quot;name&quot; : [
            &quot;意大利 进口&amp;lt;em&amp;gt;牛奶&amp;lt;/em&amp;gt; 葛兰纳诺&amp;lt;em&amp;gt;脱脂&amp;lt;/em&amp;gt;纯&amp;lt;em&amp;gt;牛奶&amp;lt;/em&amp;gt; 成人&amp;lt;em&amp;gt;牛奶&amp;lt;/em&amp;gt;  进口&amp;lt;em&amp;gt;脱脂&amp;lt;/em&amp;gt;纯&amp;lt;em&amp;gt;牛奶&amp;lt;/em&amp;gt;1Lx6盒&quot;
          ]
        }
      },
    ]
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;搜索必须有查询条件，而且是全文检索类型的查询条件，例如match&lt;/li&gt;
&lt;li&gt;参与高亮的字段必须是text类型的字段&lt;/li&gt;
&lt;li&gt;默认情况下参与高亮的字段要与搜索字段一致，除非添加：required_field_match=false
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;查询的DSL是一个大的JSON对象，包含下列属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;query&lt;/code&gt;：查询条件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;from&lt;/code&gt;和&lt;code&gt;size&lt;/code&gt;：分页条件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sort&lt;/code&gt;：排序条件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;highlight&lt;/code&gt;：高亮条件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;搜索的完整语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;name&quot;: &quot;华为&quot;
    }
  },
  &quot;from&quot;: 0, // 分页开始的位置
  &quot;size&quot;: 20, // 期望获取的文档总数
  &quot;sort&quot;: [
    { &quot;price&quot;: &quot;asc&quot; }, // 普通排序
  ],
  &quot;highlight&quot;: {
    &quot;fields&quot;: { // 高亮字段
      &quot;name&quot;: {
        &quot;pre_tags&quot;: &quot;&amp;lt;em&amp;gt;&quot;,  // 高亮字段的前置标签
        &quot;post_tags&quot;: &quot;&amp;lt;/em&amp;gt;&quot; // 高亮字段的后置标签
      }
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;JavaRestClient查询&lt;/h3&gt;
&lt;p&gt;文档的查询依然使用昨天学习的 RestHighLevelClient对象，查询的基本步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1）创建request对象，这次是搜索，所以是SearchRequest&lt;/li&gt;
&lt;li&gt;2）准备请求参数，也就是查询DSL对应的JSON参数&lt;/li&gt;
&lt;li&gt;3）发起请求&lt;/li&gt;
&lt;li&gt;4）解析响应，响应结果相对复杂，需要逐层解析&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;快速入门&lt;/h4&gt;
&lt;p&gt;由于Elasticsearch对外暴露的接口都是Restful风格的接口，因此JavaAPI调用就是在发送Http请求。而我们核心要做的就是&lt;strong&gt;利用利用Java代码组织请求参数，解析响应结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送请求&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先以match_all查询为例，其DSL和JavaAPI的对比如图：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-18_00-16-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码解读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一步，创建&lt;code&gt;SearchRequest&lt;/code&gt;对象，指定索引库名&lt;/li&gt;
&lt;li&gt;第二步，利用&lt;code&gt;request.source()&lt;/code&gt;构建DSL，DSL中可以包含查询、分页、排序、高亮等&lt;/li&gt;
&lt;li&gt;query()：代表查询条件，利用&lt;code&gt;QueryBuilders.matchAllQuery()&lt;/code&gt;构建一个&lt;code&gt;match_all&lt;/code&gt;查询的&lt;code&gt;DSL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第三步，利用client.search()发送请求，得到响应&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里关键的API有两个，一个是&lt;code&gt;request.source()&lt;/code&gt;，它构建的就是DSL中的完整JSON参数。其中包含了&lt;code&gt;query、sort、from、size、highlight&lt;/code&gt;等所有功能&lt;/p&gt;
&lt;p&gt;另一个是QueryBuilders，其中包含了我们学习过的各种&lt;strong&gt;叶子查询、复合查询&lt;/strong&gt;等&lt;/p&gt;
&lt;p&gt;解析响应的结果
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-18_00-25-11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码解读：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;elasticsearch返回的结果是一个JSON字符串，结构包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hits&lt;/code&gt;：命中的结果
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;total&lt;/code&gt;：总条数，其中的value是具体的总条数值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_score&lt;/code&gt;：所有结果中得分最高的文档的相关性算分&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hits&lt;/code&gt;：搜索结果的文档数组，其中的每个文档都是一个json对象
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_source&lt;/code&gt;：文档中的原始数据，也是json对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，我们解析响应结果，就是逐层解析JSON字符串，流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SearchHits&lt;/code&gt;：通过&lt;code&gt;response.getHits()&lt;/code&gt;获取，就是JSON中的最外层的hits，代表命中的结果
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SearchHits.getTotalHits().value&lt;/code&gt;：获取总条数信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SearchHits.getHits()&lt;/code&gt;：获取SearchHit数组，也就是文档数组
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SearchHit.getSourceAsString()&lt;/code&gt;：获取文档结果中的&lt;code&gt;_source&lt;/code&gt;，也就是原始的json文档数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例代码：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest(properties = &quot;spring.profiles.active=local&quot;)
public class ElasticeSearchTest {


    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private IItemService itemService;


    @BeforeEach
    void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                HttpHost.create(&quot;http://192.168.146.131:9200&quot;)
        ));
    }

    @AfterEach
    void tearDown() throws IOException {
        if (restHighLevelClient != null) {
            restHighLevelClient.close();
        }
    }

    @Test
    void testSearch() throws IOException {
        // 1. 创建request对象
        SearchRequest request = new SearchRequest(&quot;items&quot;);
        // 2. 配置requset参数
        request.source().query(QueryBuilders.matchAllQuery());
        // 3.发送请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        // 4. 解析结果
        SearchHits searchHits = response.getHits();
        // 总条数
        long total = searchHits.getTotalHits().value;
        System.out.println(&quot;total = &quot; + total);
        // 命中的数据
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            String json = hit.getSourceAsString();
            ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class);
            System.out.println(&quot;itemDoc = &quot; + itemDoc);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;文档搜索的基本步骤是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建&lt;code&gt;SearchRequest&lt;/code&gt;对象&lt;/li&gt;
&lt;li&gt;准备&lt;code&gt;request.source()&lt;/code&gt;，也就是DSL。
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;QueryBuilders&lt;/code&gt;来构建查询条件&lt;/li&gt;
&lt;li&gt;传入&lt;code&gt;request.source()&lt;/code&gt; 的 &lt;code&gt;query()&lt;/code&gt; 方法&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;发送请求，得到结果&lt;/li&gt;
&lt;li&gt;解析结果（参考JSON结果，从外到内，逐层解析）&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;叶子查询&lt;/h4&gt;
&lt;p&gt;全文检索的查询条件构造API如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-18_20-56-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;精确查询的查询条件构造API如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-18_20-59-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所有的查询条件都是由QueryBuilders来构建的，叶子查询也不例外。因此整套代码中变化的部分仅仅是query条件构造的方式，其它不动。&lt;/p&gt;
&lt;p&gt;例如&lt;code&gt;match&lt;/code&gt;查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
void testMatch() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest(&quot;items&quot;);
    // 2.组织请求参数
    request.source().query(QueryBuilders.matchQuery(&quot;name&quot;, &quot;脱脂牛奶&quot;));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再比如&lt;code&gt;multi_match&lt;/code&gt;查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
void testMultiMatch() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest(&quot;items&quot;);
    // 2.组织请求参数
    request.source().query(QueryBuilders.multiMatchQuery(&quot;脱脂牛奶&quot;, &quot;name&quot;, &quot;category&quot;));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有&lt;code&gt;range&lt;/code&gt;查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
void testRange() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest(&quot;items&quot;);
    // 2.组织请求参数
    request.source().query(QueryBuilders.rangeQuery(&quot;price&quot;).gte(10000).lte(30000));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有&lt;code&gt;term&lt;/code&gt;查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
void testTerm() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest(&quot;items&quot;);
    // 2.组织请求参数
    request.source().query(QueryBuilders.termQuery(&quot;brand&quot;, &quot;华为&quot;));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;复合查询&lt;/h4&gt;
&lt;p&gt;布尔查询的查询条件构造API如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-18_20-59-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;需求：利用JavaRestClient实现搜索功能，条件如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;搜索关键字为脱脂牛奶&lt;/p&gt;
&lt;p&gt;品牌必须为德亚&lt;/p&gt;
&lt;p&gt;价格必须低于300&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest(properties = &quot;spring.profiles.active=local&quot;)
public class ElasticeSearchTest {


    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private IItemService itemService;


    @BeforeEach
    void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                HttpHost.create(&quot;http://192.168.146.131:9200&quot;)
        ));
    }

    @AfterEach
    void tearDown() throws IOException {
        if (restHighLevelClient != null) {
            restHighLevelClient.close();
        }
    }

    @Test
    void testSearchMatchAll() throws IOException {
        // 1. 创建request对象
        SearchRequest request = new SearchRequest(&quot;items&quot;);
        // 2. 配置requset参数
        request.source().query(QueryBuilders.matchAllQuery());
        // 3.发送请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        // 4. 解析结果
        parseResponseResult(response);
    }

    @Test
    void testSearch() throws IOException {
        // 1. 创建request对象
        SearchRequest request = new SearchRequest(&quot;items&quot;);

        // 2. 配置requset参数
        request.source().query(
                QueryBuilders.boolQuery()
                        .must(QueryBuilders.matchQuery(&quot;name&quot;, &quot;脱脂牛奶&quot;))
                        .filter(QueryBuilders.termQuery(&quot;brand&quot;, &quot;德亚&quot;))
                        .filter(QueryBuilders.rangeQuery(&quot;price&quot;).lt(&quot;30000&quot;))
        );
        // 3.发送请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        // 4. 解析结果
        parseResponseResult(response);
    }

    private static void parseResponseResult(SearchResponse response) {
        SearchHits searchHits = response.getHits();
        // 总条数
        long total = searchHits.getTotalHits().value;
        System.out.println(&quot;total = &quot; + total);
        // 命中的数据
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            String json = hit.getSourceAsString();
            ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class);
            System.out.println(&quot;itemDoc = &quot; + itemDoc);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;排序和分页&lt;/h4&gt;
&lt;p&gt;与query类似，排序和分页参数都是基于&lt;code&gt;request.source()&lt;/code&gt;来设置&lt;/p&gt;
&lt;p&gt;DSL和JavaAPI的对比如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-18_21-35-16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testSortAndPage() throws IOException {
        int pageNum = 1, pageSize = 5;
        // 1. 创建request对象
        SearchRequest request = new SearchRequest(&quot;items&quot;);
        // 2. query条件
        request.source().query(QueryBuilders.matchAllQuery());
        // 分页
        request.source().from((pageNum - 1) * pageSize).size(pageSize);
        // 排序
        request.source().sort(&quot;sold&quot;, SortOrder.DESC)  // 销量降序
                .sort(&quot;price&quot;, SortOrder.ASC);  // 价格升序
        // 3.发送请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        // 4. 解析结果
        parseResponseResult(response);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;高亮显示&lt;/h4&gt;
&lt;p&gt;高亮查询与前面的查询有两点不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;条件同样是在&lt;code&gt;request.source()&lt;/code&gt;中指定，只不过高亮条件要基于&lt;code&gt;HighlightBuilder&lt;/code&gt;来构造&lt;/li&gt;
&lt;li&gt;高亮响应结果与搜索的文档结果不在一起，需要单独解析&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先来看高亮条件构造，其DSL和JavaAPI的对比如图：&lt;/p&gt;
&lt;p&gt;高亮显示的条件构造API如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-18_21-48-36.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testHighlight() throws IOException {
        // 1. 创建request对象
        SearchRequest request = new SearchRequest(&quot;items&quot;);
        // 2. query条件
        request.source().query(QueryBuilders.matchQuery(&quot;name&quot;, &quot;脱脂牛奶&quot;));
        // 高亮条件  默认就是em标签
        request.source().highlighter(SearchSourceBuilder.highlight().field(&quot;name&quot;).preTags(&quot;&amp;lt;em&amp;gt;&quot;).postTags(&quot;&amp;lt;/em&amp;gt;&quot;));
        // 3.发送请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        // 4. 解析结果
        parseResponseResult(response);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来看结果解析，文档解析的部分不变，主要是高亮内容需要单独解析出来，其DSL和JavaAPI的对比如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-18_21-58-11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码解读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第3、4步：从结果中获取&lt;code&gt;_source&lt;/code&gt;。&lt;code&gt;hit.getSourceAsString()&lt;/code&gt;，这部分是非高亮结果，json字符串。还需要反序列为&lt;code&gt;ItemDoc&lt;/code&gt;对象&lt;/li&gt;
&lt;li&gt;第5步：获取高亮结果。&lt;code&gt;hit.getHighlightFields()&lt;/code&gt;，返回值是一个&lt;code&gt;Map&lt;/code&gt;，key是高亮字段名称，值是&lt;code&gt;HighlightField&lt;/code&gt;对象，代表高亮值&lt;/li&gt;
&lt;li&gt;第5.1步：从Map中根据高亮字段名称，获取高亮字段值对象&lt;code&gt;HighlightField&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第5.2步：从&lt;code&gt;HighlightField&lt;/code&gt;中获取&lt;code&gt;Fragments&lt;/code&gt;，并且转为字符串。这部分就是真正的高亮字符串了&lt;/li&gt;
&lt;li&gt;最后：用高亮的结果替换&lt;code&gt;ItemDoc&lt;/code&gt;中的非高亮结果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;完整代码如下：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testHighlight() throws IOException {
        // 1. 创建request对象
        SearchRequest request = new SearchRequest(&quot;items&quot;);
        // 2. query条件
        request.source().query(QueryBuilders.matchQuery(&quot;name&quot;, &quot;脱脂牛奶&quot;));
        // 高亮条件  默认就是em标签
        request.source().highlighter(SearchSourceBuilder.highlight().field(&quot;name&quot;).preTags(&quot;&amp;lt;em&amp;gt;&quot;).postTags(&quot;&amp;lt;/em&amp;gt;&quot;));
        // 3.发送请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        // 4. 解析结果
        SearchHits searchHits = response.getHits();
        // 总条数
        long total = searchHits.getTotalHits().value;
        System.out.println(&quot;total = &quot; + total);
        // 命中的数据
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            String json = hit.getSourceAsString();
            ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class);
            // 处理高亮结果
            Map&amp;lt;String, HighlightField&amp;gt; hfs = hit.getHighlightFields();
            if (hfs != null &amp;amp;&amp;amp; !hfs.isEmpty()) {
                // 根据高亮字段名，获取高亮结果
                HighlightField hf = hfs.get(&quot;name&quot;);
                // 覆盖高亮结果
                String hfName = hf.getFragments()[0].string();
                itemDoc.setName(hfName);
            }
            System.out.println(&quot;itemDoc = &quot; + itemDoc);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;String hfName = hf.getFragments()[0].string();&lt;/code&gt;这里简写了，如果查询字段的字符串超过一个阈值则会切割这个字符串为几个片段保存在高亮数组中，实际情况需要遍历拼接这个数组中的内容，最终组成一个字符串
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;数据聚合&lt;/h3&gt;
&lt;p&gt;聚合（&lt;code&gt;aggregations&lt;/code&gt;）可以让我们极其方便的实现对数据的统计、分析、运算。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;什么品牌的手机最受欢迎？&lt;/li&gt;
&lt;li&gt;这些手机的平均价格、最高价格、最低价格？&lt;/li&gt;
&lt;li&gt;这些手机每月的销售情况如何？
实现这些统计功能的比数据库的sql要方便的多，而且查询速度非常快，可以实现近实时搜索效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方文档：&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations.html&quot;&gt;文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;聚合（aggregations）可以实现对文档数据的统计、分析、运算。聚合常见的有三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;桶（Bucket）聚合：用来对文档做分组
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;TermAggregation&lt;/code&gt;：按照文档字段值分组&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Date Histogram&lt;/code&gt;：按照日期阶梯分组，例如一周为一组，或者一月为一组&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;度量（Metric）聚合：用以计算一些值，比如：最大值、最小值、平均值等
&lt;blockquote&gt;
&lt;p&gt;Avg：求平均值&lt;/p&gt;
&lt;p&gt;Max：求最大值&lt;/p&gt;
&lt;p&gt;Min：求最小值&lt;/p&gt;
&lt;p&gt;Stats：同时求max、min、avg、sum等&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;管道（pipeline）聚合：其它聚合的结果为基础做聚合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;::: tip
注意：参加聚合的字段必须是keyword、日期、数值、布尔类型；（不分词的字段）
:::&lt;/p&gt;
&lt;h4&gt;DSL聚合&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Bucket聚合&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如我们要统计所有商品中共有哪些商品分类，其实就是以分类（category）字段对数据分组。category值一样的放在同一组，属于&lt;code&gt;Bucket&lt;/code&gt;聚合中的&lt;code&gt;Term&lt;/code&gt;聚合。&lt;/p&gt;
&lt;p&gt;基本语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;size&quot;: 0,
  &quot;aggs&quot;: {
    &quot;category_agg&quot;: {
      &quot;terms&quot;: {
        &quot;field&quot;: &quot;category&quot;,
        &quot;size&quot;: 20
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语法说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;size：设置size为0，就是每页查0条，则结果中就不包含文档，只包含聚合&lt;/li&gt;
&lt;li&gt;aggs：定义聚合
&lt;ul&gt;
&lt;li&gt;category_agg：聚合名称，自定义，但不能重复
&lt;ul&gt;
&lt;li&gt;terms：聚合的类型，按分类聚合，所以用term
&lt;ul&gt;
&lt;li&gt;field：参与聚合的字段名称&lt;/li&gt;
&lt;li&gt;size：希望返回的聚合结果的最大数量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;聚合三要素：名称、类型、字段&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;size&quot;: 0,
  &quot;aggs&quot;: {
    &quot;cate_agg&quot;: {
      &quot;terms&quot;: {
        &quot;field&quot;: &quot;category&quot;,
        &quot;size&quot;: 5
      }
    },
    &quot;brand_agg&quot;:{
      &quot;terms&quot;: {
        &quot;field&quot;: &quot;brand&quot;,
        &quot;size&quot;: 5
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;took&quot; : 16,
  &quot;timed_out&quot; : false,
  &quot;_shards&quot; : {
    &quot;total&quot; : 1,
    &quot;successful&quot; : 1,
    &quot;skipped&quot; : 0,
    &quot;failed&quot; : 0
  },
  &quot;hits&quot; : {
    &quot;total&quot; : {
      &quot;value&quot; : 10000,
      &quot;relation&quot; : &quot;gte&quot;
    },
    &quot;max_score&quot; : null,
    &quot;hits&quot; : [ ]
  },
  &quot;aggregations&quot; : {
    &quot;brand_agg&quot; : {
      &quot;doc_count_error_upper_bound&quot; : 0,
      &quot;sum_other_doc_count&quot; : 73002,
      &quot;buckets&quot; : [
        {
          &quot;key&quot; : &quot;华为&quot;,
          &quot;doc_count&quot; : 7145
        },
        {
          &quot;key&quot; : &quot;南极人&quot;,
          &quot;doc_count&quot; : 2432
        },
        {
          &quot;key&quot; : &quot;奥古狮登&quot;,
          &quot;doc_count&quot; : 2035
        },
        {
          &quot;key&quot; : &quot;森马&quot;,
          &quot;doc_count&quot; : 2005
        },
        {
          &quot;key&quot; : &quot;恒源祥&quot;,
          &quot;doc_count&quot; : 1856
        }
      ]
    },
    &quot;cate_agg&quot; : {
      &quot;doc_count_error_upper_bound&quot; : 0,
      &quot;sum_other_doc_count&quot; : 7583,
      &quot;buckets&quot; : [
        {
          &quot;key&quot; : &quot;休闲鞋&quot;,
          &quot;doc_count&quot; : 20612
        },
        {
          &quot;key&quot; : &quot;牛仔裤&quot;,
          &quot;doc_count&quot; : 19611
        },
        {
          &quot;key&quot; : &quot;老花镜&quot;,
          &quot;doc_count&quot; : 16222
        },
        {
          &quot;key&quot; : &quot;拉杆箱&quot;,
          &quot;doc_count&quot; : 14347
        },
        {
          &quot;key&quot; : &quot;手机&quot;,
          &quot;doc_count&quot; : 10100
        }
      ]
    }
  }
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-18_22-56-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;带条件聚合&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;默认情况下，Bucket聚合是对索引库的所有文档做聚合&lt;/p&gt;
&lt;p&gt;但真实场景下，用户会输入搜索条件，因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
例如，我想知道价格高于3000元的手机品牌有哪些，该怎么统计呢？
我们需要从需求中分析出搜索查询的条件和聚合的目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;搜索查询条件：
&lt;ul&gt;
&lt;li&gt;价格高于3000&lt;/li&gt;
&lt;li&gt;必须是手机&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;聚合目标：统计的是品牌，肯定是对&lt;code&gt;brand&lt;/code&gt;字段做&lt;code&gt;term&lt;/code&gt;聚合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;filter&quot;: [
        {
          &quot;term&quot;: {
            &quot;category&quot;: &quot;手机&quot;
          }
        },
        {
          &quot;range&quot;: {
            &quot;price&quot;: {
              &quot;gte&quot;: 300000
            }
          }
        }
      ]
    }
  },
  &quot;aggs&quot;: {
    &quot;brand_agg&quot;: {
      &quot;terms&quot;: {
        &quot;field&quot;: &quot;brand&quot;,
        &quot;size&quot;: 10
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;响应：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;took&quot; : 2,
  &quot;timed_out&quot; : false,
  &quot;_shards&quot; : {
    &quot;total&quot; : 1,
    &quot;successful&quot; : 1,
    &quot;skipped&quot; : 0,
    &quot;failed&quot; : 0
  },
  &quot;hits&quot; : {
    &quot;total&quot; : {
      &quot;value&quot; : 11,
      &quot;relation&quot; : &quot;eq&quot;
    },
    &quot;max_score&quot; : null,
    &quot;hits&quot; : [ ]
  },
  &quot;aggregations&quot; : {
    &quot;brand_agg&quot; : {
      &quot;doc_count_error_upper_bound&quot; : 0,
      &quot;sum_other_doc_count&quot; : 0,
      &quot;buckets&quot; : [
        {
          &quot;key&quot; : &quot;Apple&quot;,
          &quot;doc_count&quot; : 7
        },
        {
          &quot;key&quot; : &quot;华为&quot;,
          &quot;doc_count&quot; : 2
        },
        {
          &quot;key&quot; : &quot;三星&quot;,
          &quot;doc_count&quot; : 1
        },
        {
          &quot;key&quot; : &quot;小米&quot;,
          &quot;doc_count&quot; : 1
        }
      ]
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Metric聚合&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们统计了价格高于3000的手机品牌，形成了一个个桶。现在我们需要对桶内的商品做运算，获取每个品牌价格的最小值、最大值、平均值。&lt;/p&gt;
&lt;p&gt;这就要用到&lt;code&gt;Metric&lt;/code&gt;聚合了，例如&lt;code&gt;stat&lt;/code&gt;聚合，就可以同时获取&lt;code&gt;min&lt;/code&gt;、&lt;code&gt;max&lt;/code&gt;、&lt;code&gt;avg&lt;/code&gt;等结果。&lt;/p&gt;
&lt;p&gt;语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /items/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;filter&quot;: [
        {
          &quot;term&quot;: {
            &quot;category&quot;: &quot;手机&quot;
          }
        }
      ]
    }
  },
  &quot;aggs&quot;: {
    &quot;brand_agg&quot;: {
      &quot;terms&quot;: {
        &quot;field&quot;: &quot;brand&quot;,
        &quot;size&quot;: 5
      },
      &quot;aggs&quot;: {
        &quot;price_stats&quot;: {
          &quot;stats&quot;: {
            &quot;field&quot;: &quot;price&quot;
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;响应：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;took&quot; : 7,
  &quot;timed_out&quot; : false,
  &quot;_shards&quot; : {
    &quot;total&quot; : 1,
    &quot;successful&quot; : 1,
    &quot;skipped&quot; : 0,
    &quot;failed&quot; : 0
  },
  &quot;hits&quot; : {
    &quot;total&quot; : {
      &quot;value&quot; : 10000,
      &quot;relation&quot; : &quot;gte&quot;
    },
    &quot;max_score&quot; : null,
    &quot;hits&quot; : [ ]
  },
  &quot;aggregations&quot; : {
    &quot;brand_agg&quot; : {
      &quot;doc_count_error_upper_bound&quot; : 0,
      &quot;sum_other_doc_count&quot; : 547,
      &quot;buckets&quot; : [
        {
          &quot;key&quot; : &quot;华为&quot;,
          &quot;doc_count&quot; : 7145,
          &quot;price_stats&quot; : {
            &quot;count&quot; : 7145,
            &quot;min&quot; : 0.0,
            &quot;max&quot; : 544000.0,
            &quot;avg&quot; : 50073.561931420576,
            &quot;sum&quot; : 3.577756E8
          }
        },
        {
          &quot;key&quot; : &quot;小米&quot;,
          &quot;doc_count&quot; : 1227,
          &quot;price_stats&quot; : {
            &quot;count&quot; : 1227,
            &quot;min&quot; : 200.0,
            &quot;max&quot; : 889400.0,
            &quot;avg&quot; : 51005.86797066015,
            &quot;sum&quot; : 6.25842E7
          }
        },
        {
          &quot;key&quot; : &quot;Apple&quot;,
          &quot;doc_count&quot; : 577,
          &quot;price_stats&quot; : {
            &quot;count&quot; : 577,
            &quot;min&quot; : 100.0,
            &quot;max&quot; : 688000.0,
            &quot;avg&quot; : 57975.73656845754,
            &quot;sum&quot; : 3.3452E7
          }
        },
        {
          &quot;key&quot; : &quot;OPPO&quot;,
          &quot;doc_count&quot; : 430,
          &quot;price_stats&quot; : {
            &quot;count&quot; : 430,
            &quot;min&quot; : 0.0,
            &quot;max&quot; : 99500.0,
            &quot;avg&quot; : 50212.558139534885,
            &quot;sum&quot; : 2.15914E7
          }
        },
        {
          &quot;key&quot; : &quot;vivo&quot;,
          &quot;doc_count&quot; : 174,
          &quot;price_stats&quot; : {
            &quot;count&quot; : 174,
            &quot;min&quot; : 0.0,
            &quot;max&quot; : 99800.0,
            &quot;avg&quot; : 52264.36781609195,
            &quot;sum&quot; : 9094000.0
          }
        }
      ]
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到我们在&lt;code&gt;brand_agg&lt;/code&gt;聚合的内部，我们新加了一个&lt;code&gt;aggs&lt;/code&gt;参数。这个聚合就是&lt;code&gt;brand_agg&lt;/code&gt;的子聚合，会对&lt;code&gt;brand_agg&lt;/code&gt;形成的每个桶中的文档分别统计。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;price_stats&lt;/code&gt;：聚合名称
&lt;ul&gt;
&lt;li&gt;stats：聚合类型，stats是metric聚合的一种
&lt;ul&gt;
&lt;li&gt;field：聚合字段，这里选择price，统计价格&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于&lt;code&gt;stats&lt;/code&gt;是对&lt;code&gt;brand_agg&lt;/code&gt;形成的每个品牌桶内文档分别做统计，因此每个品牌都会统计出自己的价格最小、最大、平均值。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aggs&lt;/code&gt;代表聚合，与&lt;code&gt;query&lt;/code&gt;同级，此时&lt;code&gt;query&lt;/code&gt;的作用是？&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;限定聚合的的文档范围&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;聚合必须的三要素：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;聚合名称&lt;/p&gt;
&lt;p&gt;聚合类型&lt;/p&gt;
&lt;p&gt;聚合字段&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;聚合可配置属性有：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;size：指定聚合结果数量 (聚合结果有多个桶，size可以选择保留多少个桶)&lt;/p&gt;
&lt;p&gt;order：指定聚合结果排序方式&lt;/p&gt;
&lt;p&gt;field：指定聚合字段&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;RestClient实现聚合&lt;/h4&gt;
&lt;p&gt;可以看到在DSL中，&lt;code&gt;aggs&lt;/code&gt;聚合条件与&lt;code&gt;query&lt;/code&gt;条件是同一级别，都属于查询JSON参数。因此依然是利用&lt;code&gt;request.source()&lt;/code&gt;方法来设置。&lt;/p&gt;
&lt;p&gt;不过聚合条件的要利用&lt;code&gt;AggregationBuilders&lt;/code&gt;这个工具类来构造。DSL与JavaAPI的语法对比如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-19_21-04-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testAgg() throws IOException {
        // 1. 创建request对象
        SearchRequest request = new SearchRequest(&quot;items&quot;);
        // 2. 分页
        request.source().size(0);
        // 聚合条件
        String brandAggName = &quot;brandAgg&quot;;
        request.source().aggregation(
                AggregationBuilders.terms(brandAggName)  // 类型、名称
                        .field(&quot;brand&quot;)     // 字段
                        .size(10)
        );
        // 3.发送请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        System.out.println(&quot;response = &quot; + response);
        // 4. 解析结果
        Aggregations aggregations = response.getAggregations();
        // 总条数
        Terms brandTerms = aggregations.get(brandAggName);
        List&amp;lt;? extends Terms.Bucket&amp;gt; buckets = brandTerms.getBuckets();
        for (Terms.Bucket bucket : buckets) {
            String keyAsString = bucket.getKeyAsString();
            long docCount = bucket.getDocCount();
            System.out.println(&quot;brand = &quot; + keyAsString);
            System.out.println(&quot;count = &quot; + docCount);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
{
  &quot;took&quot;: 21,
  &quot;timed_out&quot;: false,
  &quot;_shards&quot;: {
    &quot;total&quot;: 1,
    &quot;successful&quot;: 1,
    &quot;skipped&quot;: 0,
    &quot;failed&quot;: 0
  },
  &quot;hits&quot;: {
    &quot;total&quot;: {
      &quot;value&quot;: 10000,
      &quot;relation&quot;: &quot;gte&quot;
    },
    &quot;max_score&quot;: null,
    &quot;hits&quot;: []
  },
  &quot;aggregations&quot;: {
    &quot;sterms#brandAgg&quot;: {
      &quot;doc_count_error_upper_bound&quot;: 0,
      &quot;sum_other_doc_count&quot;: 65272,
      &quot;buckets&quot;: [
        {
          &quot;key&quot;: &quot;华为&quot;,
          &quot;doc_count&quot;: 7145
        },
        {
          &quot;key&quot;: &quot;南极人&quot;,
          &quot;doc_count&quot;: 2432
        },
        {
          &quot;key&quot;: &quot;奥古狮登&quot;,
          &quot;doc_count&quot;: 2035
        },
        {
          &quot;key&quot;: &quot;森马&quot;,
          &quot;doc_count&quot;: 2005
        },
        {
          &quot;key&quot;: &quot;恒源祥&quot;,
          &quot;doc_count&quot;: 1856
        },
        {
          &quot;key&quot;: &quot;回力&quot;,
          &quot;doc_count&quot;: 1695
        },
        {
          &quot;key&quot;: &quot;其他品牌&quot;,
          &quot;doc_count&quot;: 1590
        },
        {
          &quot;key&quot;: &quot;斯凯奇&quot;,
          &quot;doc_count&quot;: 1565
        },
        {
          &quot;key&quot;: &quot;小米&quot;,
          &quot;doc_count&quot;: 1498
        },
        {
          &quot;key&quot;: &quot;北极绒&quot;,
          &quot;doc_count&quot;: 1382
        }
      ]
    }
  }
}


brand = 华为
count = 7145
brand = 南极人
count = 2432
brand = 奥古狮登
count = 2035
brand = 森马
count = 2005
brand = 恒源祥
count = 1856
brand = 回力
count = 1695
brand = 其他品牌
count = 1590
brand = 斯凯奇
count = 1565
brand = 小米
count = 1498
brand = 北极绒
count = 1382
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Vue3工程化</title><link>https://zzyang.top/posts/vue3-project/</link><guid isPermaLink="true">https://zzyang.top/posts/vue3-project/</guid><pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Vue3工程化&lt;/h1&gt;
&lt;h2&gt;项目初始化&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;D:\workspace\vscode\vue_admin_template&amp;gt;pnpm create vite
.../19788950c58-c70                      |   +1 +
.../19788950c58-c70                      | Progress: resolved 1, reused 0, downloaded 1, added 1, done
|
o  Project name:
|  project
|
o  Select a framework:
|  Vue
|
o  Select a variant:
|  TypeScript
|
o  Scaffolding project in D:\workspace\vscode\vue_admin_template\project...
|
—  Done. Now run:

  cd project
  pnpm install
  pnpm run dev


D:\workspace\vscode\vue_admin_template&amp;gt;cd project

D:\workspace\vscode\vue_admin_template\project&amp;gt;pnpm i

   ╭───────────────────────────────────────────────────────────────────╮
   │                                                                   │
   │                Update available! 9.15.4 → 10.12.1.                │
   │   Changelog: https://github.com/pnpm/pnpm/releases/tag/v10.12.1   │
   │                 Run &quot;pnpm add -g pnpm&quot; to update.                 │
   │                                                                   │
   ╰───────────────────────────────────────────────────────────────────╯

Packages: +50
++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 94, reused 11, downloaded 39, added 50, done
node_modules/.pnpm/esbuild@0.25.5/node_modules/esbuild: Running postinstall script, done in 620ms

dependencies:
+ vue 3.5.17

devDependencies:
+ @vitejs/plugin-vue 5.2.4
+ @vue/tsconfig 0.7.0
+ typescript 5.8.3
+ vite 6.3.5
+ vue-tsc 2.2.10

Done in 3.6s

D:\workspace\vscode\vue_admin_template\project&amp;gt;pnpm run dev

&amp;gt; project@0.0.0 dev D:\workspace\vscode\vue_admin_template\project
&amp;gt; vite

Port 5173 is in use, trying another one...

  VITE v6.3.5  ready in 503 ms

  ➜  Local:   http://localhost:5174/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.ts&lt;/code&gt;中无需引入&lt;code&gt;style.css&lt;/code&gt;，把&lt;code&gt;style.css&lt;/code&gt;删掉即可&lt;/p&gt;
&lt;p&gt;&lt;code&gt;components&lt;/code&gt;中清空，&lt;code&gt;assert&lt;/code&gt;清空&lt;/p&gt;
&lt;p&gt;在package.json中配置如下，启动项目时，可自动打开浏览器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite --open&quot;,
    &quot;build&quot;: &quot;vue-tsc -b &amp;amp;&amp;amp; vite build&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;eslint&lt;/h2&gt;
&lt;p&gt;eslint中文官网:http://eslint.cn/&lt;/p&gt;
&lt;p&gt;ESLint最初是由&lt;a href=&quot;http://nczonline.net/&quot;&gt;Nicholas C. Zakas&lt;/a&gt; 于2013年6月创建的开源项目。它的目标是提供一个插件化的&lt;strong&gt;javascript代码检测工具&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先安装eslint&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm i eslint -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成配置文件:&lt;code&gt;eslint.config.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx eslint --init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;选项：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS D:\workspace\vscode\vue_admin_template&amp;gt; npx eslint --init
You can also run this command directly using &apos;npm init @eslint/config@latest&apos;.
Need to install the following packages:
@eslint/create-config@1.9.0
Ok to proceed? (y) y


&amp;gt; vue_admin_template@0.0.0 npx
&amp;gt; create-config

@eslint/create-config: v1.9.0

√ What do you want to lint? · javascript
√ How would you like to use ESLint? · problems
√ What type of modules does your project use? · esm
√ Which framework does your project use? · vue
√ Does your project use TypeScript? · no / yes
√ Where does your code run? · browser
The config that you&apos;ve selected requires the following dependencies:

eslint, @eslint/js, globals, typescript-eslint, eslint-plugin-vue
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · pnpm
☕️Installing...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;vue3环境代码校验插件&lt;/strong&gt;
安装指令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel/eslint-parser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;eslint.config.js&lt;/code&gt;配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//eslint.config.js

// 导入 ESLint 相关插件和解析器

import pluginJs from &apos;@eslint/js&apos; // ESLint JavaScript 规则插件

import tseslint from &apos;@typescript-eslint/eslint-plugin&apos; // TypeScript ESLint 插件

import tsParser from &apos;@typescript-eslint/parser&apos; // TypeScript 解析器

import pluginVue from &apos;eslint-plugin-vue&apos; // Vue.js ESLint 插件

import vueEslintParser from &apos;vue-eslint-parser&apos; // Vue 解析器
import globals from &apos;globals&apos;

// 导出 ESLint 配置数组

export default [
  {
    // 适用于的文件类型

    files: [&apos;**/*.{js,mjs,cjs,ts,vue}&apos;],

    // 忽略的文件和文件夹

    ignores: [&apos;node_modules&apos;, &apos;dist&apos;, &apos;*.config.js&apos;],

    languageOptions: {
      globals: { ...globals.browser, ...globals.node }, // 使用浏览器全局变量

      ecmaVersion: &apos;latest&apos;, // 使用最新的 ECMAScript 版本

      sourceType: &apos;module&apos;, // 使用模块类型

      parser: tsParser, // 使用 TypeScript 解析器
    },

    // 配置使用的插件

    plugins: {
      vue: pluginVue, // 引入 Vue 插件

      &apos;@typescript-eslint&apos;: tseslint, // 引入 TypeScript ESLint 插件
    },

    // 定义 ESLint 规则

    rules: {
      ...pluginJs.configs.recommended.rules, // JavaScript 推荐规则

      ...tseslint.configs.recommended.rules, // TypeScript 推荐规则

      ...pluginVue.configs[&apos;flat/essential&apos;].rules, // Vue 推荐规则

      // JavaScript 规则

      &apos;no-var&apos;: &apos;error&apos;, // 禁止使用 var

      &apos;no-multiple-empty-lines&apos;: [&apos;warn&apos;, { max: 1 }], // 允许最多一行空行

      &apos;no-console&apos;: process.env.NODE_ENV === &apos;production&apos; ? &apos;error&apos; : &apos;off&apos;, // 在生产环境中禁止使用 console

      &apos;no-debugger&apos;: process.env.NODE_ENV === &apos;production&apos; ? &apos;error&apos; : &apos;off&apos;, // 在生产环境中禁止使用 debugger

      &apos;no-unexpected-multiline&apos;: &apos;error&apos;, // 禁止意外的多行

      &apos;no-useless-escape&apos;: &apos;off&apos;, // 关闭不必要的转义

      // TypeScript 规则

      &apos;@typescript-eslint/no-unused-vars&apos;: &apos;off&apos;, // 允许未使用的变量

      &apos;@typescript-eslint/prefer-ts-expect-error&apos;: &apos;error&apos;, // 优先使用 ts-expect-error

      &apos;@typescript-eslint/no-explicit-any&apos;: &apos;off&apos;, // 允许使用 any 类型

      &apos;@typescript-eslint/no-non-null-assertion&apos;: &apos;off&apos;, // 允许使用非空断言

      &apos;@typescript-eslint/no-namespace&apos;: &apos;off&apos;, // 允许使用命名空间

      &apos;@typescript-eslint/semi&apos;: &apos;off&apos;, // 关闭分号规则

      // Vue 规则

      &apos;vue/multi-word-component-names&apos;: &apos;off&apos;, // 关闭组件名称必须是多词的规则

      // &quot;vue/script-setup-uses-vars&quot;: &quot;error&quot;, // 检查 script setup 中的变量

      &apos;vue/no-mutating-props&apos;: &apos;off&apos;, // 允许在 props 中进行变更

      &apos;vue/attribute-hyphenation&apos;: &apos;off&apos;, // 允许不使用连字符的属性命名
    },
  },

  {
    // 适用于 Vue 文件

    files: [&apos;**/*.vue&apos;],

    languageOptions: {
      parser: vueEslintParser, // 使用 Vue 解析器

      parserOptions: {
        parser: tsParser, // 使用 TypeScript 解析器

        ecmaVersion: &apos;latest&apos;, // 使用最新的 ECMAScript 版本

        sourceType: &apos;module&apos;, // 使用模块类型
      },
    },
  },
]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;package.json新增两个运行脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
    &quot;lint&quot;: &quot;eslint src&quot;,
    &quot;fix&quot;: &quot;eslint --config ./eslint.config.js src --fix&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置&lt;strong&gt;prettier&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;有了eslint，为什么还要有prettier？eslint针对的是javascript，他是一个检测工具，包含js语法以及少部分格式问题，在eslint看来，语法对了就能保证代码正常运行，格式问题属于其次；&lt;/p&gt;
&lt;p&gt;而prettier属于格式化工具，它看不惯格式不统一，所以它就把eslint没干好的事接着干，另外，prettier支持&lt;/p&gt;
&lt;p&gt;包含js在内的多种语言。&lt;/p&gt;
&lt;p&gt;总结起来，&lt;strong&gt;eslint和prettier这俩兄弟一个保证js代码质量，一个保证代码美观。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;安装依赖包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install prettier --save-dev
# 或者
yarn add prettier --dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.prettierrc.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;singleQuote&quot;: true,
  &quot;semi&quot;: false,
  &quot;bracketSpacing&quot;: true,
  &quot;htmlWhitespaceSensitivity&quot;: &quot;ignore&quot;,
  &quot;endOfLine&quot;: &quot;auto&quot;,
  &quot;trailingComma&quot;: &quot;all&quot;,
  &quot;tabWidth&quot;: 2,
  &quot;printWidth&quot;: 80,
  &quot;vueIndentScriptAndStyle&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.prettierignore&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/dist/*
/html/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pacakage.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite --open&quot;,
    &quot;build&quot;: &quot;vue-tsc -b &amp;amp;&amp;amp; vite build&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;,
    &quot;lint&quot;: &quot;eslint src&quot;,
    &quot;fix&quot;: &quot;eslint --config ./eslint.config.js src --fix&quot;,
    &quot;format&quot;: &quot;prettier --write .&quot;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;setting.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;editor.codeActionsOnSave&quot;: {
    &quot;source.fixAll.eslint&quot;: &quot;explicit&quot;
  },
  &quot;eslint.format.enable&quot;: true,
  &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;,
  &quot;editor.formatOnSave&quot;: true,
  &quot;[typescript]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;
  },
  &quot;[vue]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置stylelint&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://stylelint.io/&quot;&gt;stylelint&lt;/a&gt;为css的lint工具。可格式化css代码，检查css语法错误与不合理的写法，指定css书写顺序等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;官网:https://stylelint.bootcss.com/&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们的项目中使用scss作为预处理器，安装以下依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm add sass sass-loader stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order stylelint-config-standard-scss -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.stylelintrc.cjs&lt;/code&gt;&lt;strong&gt;配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// @see https://stylelint.bootcss.com/

module.exports = {
  extends: [
    &apos;stylelint-config-standard&apos;, // 配置stylelint拓展插件
    &apos;stylelint-config-html/vue&apos;, // 配置 vue 中 template 样式格式化
    &apos;stylelint-config-standard-scss&apos;, // 配置stylelint scss插件
    &apos;stylelint-config-recommended-vue/scss&apos;, // 配置 vue 中 scss 样式格式化
    &apos;stylelint-config-recess-order&apos;, // 配置stylelint css属性书写顺序插件,
    &apos;stylelint-config-prettier&apos;, // 配置stylelint和prettier兼容
  ],
  overrides: [
    {
      files: [&apos;**/*.(scss|css|vue|html)&apos;],
      customSyntax: &apos;postcss-scss&apos;,
    },
    {
      files: [&apos;**/*.(html|vue)&apos;],
      customSyntax: &apos;postcss-html&apos;,
    },
  ],
  ignoreFiles: [
    &apos;**/*.js&apos;,
    &apos;**/*.jsx&apos;,
    &apos;**/*.tsx&apos;,
    &apos;**/*.ts&apos;,
    &apos;**/*.json&apos;,
    &apos;**/*.md&apos;,
    &apos;**/*.yaml&apos;,
  ],
  /**
   * null  =&amp;gt; 关闭该规则
   * always =&amp;gt; 必须
   */
  rules: {
    &apos;value-keyword-case&apos;: null, // 在 css 中使用 v-bind，不报错
    &apos;no-descending-specificity&apos;: null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器
    &apos;function-url-quotes&apos;: &apos;always&apos;, // 要求或禁止 URL 的引号 &quot;always(必须加上引号)&quot;|&quot;never(没有引号)&quot;
    &apos;no-empty-source&apos;: null, // 关闭禁止空源码
    &apos;selector-class-pattern&apos;: null, // 关闭强制选择器类名的格式
    &apos;property-no-unknown&apos;: null, // 禁止未知的属性(true 为不允许)
    &apos;block-opening-brace-space-before&apos;: &apos;always&apos;, //大括号之前必须有一个空格或不能有空白符
    &apos;value-no-vendor-prefix&apos;: null, // 关闭 属性值前缀 --webkit-box
    &apos;property-no-vendor-prefix&apos;: null, // 关闭 属性前缀 -webkit-mask
    &apos;selector-pseudo-class-no-unknown&apos;: [
      // 不允许未知的选择器
      true,
      {
        ignorePseudoClasses: [&apos;global&apos;, &apos;v-deep&apos;, &apos;deep&apos;], // 忽略属性，修改element默认样式的时候能使用到
      },
    ],
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.stylelintignore&lt;/code&gt;忽略文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/node_modules/*
/dist/*
/html/*
/public/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
	&quot;lint:style&quot;: &quot;stylelint src/**/*.{css,scss,vue} --cache --fix&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后配置统一的prettier来格式化我们的js和css，html代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite --open&quot;,
    &quot;build&quot;: &quot;vue-tsc -b &amp;amp;&amp;amp; vite build&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;,
    &quot;lint&quot;: &quot;eslint src&quot;,
    &quot;fix&quot;: &quot;eslint --config ./eslint.config.js src --fix&quot;,
    &quot;format&quot;: &quot;prettier --write \&quot;./**/*.{html,vue,ts,js,json,md}\&quot;&quot;,
    &quot;lint:eslint&quot;: &quot;eslint src/**/*.{ts,vue} --cache --fix&quot;,
    &quot;lint:style&quot;: &quot;stylelint src/**/*.{css,scss,vue} --cache --fix&quot;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
如果报了这个错：&lt;code&gt;“Issues with peer dependencies found ”错误&lt;/code&gt;
执行该命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm config set auto-install-peers true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;auto-install-peers 设置为 true ，在运行pnpm后，缺失的peer dependenices 会自动安装。&lt;/p&gt;
&lt;p&gt;当然，也可以删除node_modules，再重新安装&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;如果报错&lt;code&gt;module&apos; is not defined.eslintno-undef&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;.eslint.config.js&lt;/strong&gt;👇&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default [
  {
    // 适用于的文件类型

    files: [&apos;**/*.{js,mjs,cjs,ts,vue}&apos;],

    // 忽略的文件和文件夹

    ignores: [&apos;node_modules&apos;, &apos;dist&apos;, &apos;*.config.js&apos;], // ⬅️

    languageOptions: {
      globals: { ...globals.browser, ...globals.node }, // 使用浏览器全局变量

      ecmaVersion: &apos;latest&apos;, // 使用最新的 ECMAScript 版本

      sourceType: &apos;module&apos;, // 使用模块类型

      parser: tsParser, // 使用 TypeScript 解析器
    },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;配置husky&lt;/h2&gt;
&lt;p&gt;在上面我们已经集成好了我们代码校验工具，但是需要每次手动的去执行命令才会格式化我们的代码。如果有人没有格式化就提交了远程仓库中，那这个规范就没什么用。所以我们需要强制让开发人员按照代码规范来提交。&lt;/p&gt;
&lt;p&gt;要做到这件事情，就需要利用husky在代码提交之前触发git hook(git在客户端的钩子)，然后执行&lt;code&gt;pnpm run format&lt;/code&gt;来自动的格式化我们的代码。&lt;/p&gt;
&lt;p&gt;安装&lt;code&gt;husky&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install -D husky
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx husky-init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会在根目录下生成个一个.husky目录，在这个目录下面会有一个pre-commit文件，这个文件里面的命令在我们执行commit的时候就会执行&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;.husky/pre-commit&lt;/code&gt;文件添加如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env sh
. &quot;$(dirname -- &quot;$0&quot;)/_/husky.sh&quot;
pnpm run format
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们对代码进行commit操作的时候，就会执行命令，对代码进行格式化，然后再提交。&lt;/p&gt;
&lt;h2&gt;配置commitlint&lt;/h2&gt;
&lt;p&gt;对于我们的commit信息，也是有统一规范的，不能随便写,要让每个人都按照统一的标准来执行，我们可以利用&lt;strong&gt;commitlint&lt;/strong&gt;来实现。&lt;/p&gt;
&lt;p&gt;安装包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm add @commitlint/config-conventional @commitlint/cli -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加配置文件，新建&lt;code&gt;commitlint.config.cjs&lt;/code&gt;(注意是cjs)，然后添加下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  extends: [&apos;@commitlint/config-conventional&apos;],
  // 校验规则
  rules: {
    &apos;type-enum&apos;: [
      2,
      &apos;always&apos;,
      [
        &apos;feat&apos;,
        &apos;fix&apos;,
        &apos;docs&apos;,
        &apos;style&apos;,
        &apos;refactor&apos;,
        &apos;perf&apos;,
        &apos;test&apos;,
        &apos;chore&apos;,
        &apos;revert&apos;,
        &apos;build&apos;,
      ],
    ],
    &apos;type-case&apos;: [0],
    &apos;type-empty&apos;: [0],
    &apos;scope-empty&apos;: [0],
    &apos;scope-case&apos;: [0],
    &apos;subject-full-stop&apos;: [0, &apos;never&apos;],
    &apos;subject-case&apos;: [0, &apos;never&apos;],
    &apos;header-max-length&apos;: [0, &apos;always&apos;, 72],
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;package.json&lt;/code&gt;中配置scripts命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
&quot;scripts&quot;: {
    &quot;commitlint&quot;: &quot;commitlint --config commitlint.config.cjs -e -V&quot;
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置结束，现在当我们填写&lt;code&gt;commit&lt;/code&gt;信息的时候，前面就需要带着下面的&lt;code&gt;subject&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;feat&apos;,//新特性、新功能
&apos;fix&apos;,//修改bug
&apos;docs&apos;,//文档修改
&apos;style&apos;,//代码格式修改, 注意不是 css 修改
&apos;refactor&apos;,//代码重构
&apos;perf&apos;,//优化相关，比如提升性能、体验
&apos;test&apos;,//测试用例修改
&apos;chore&apos;,//其他修改, 比如改变构建流程、或者增加依赖库、工具等
&apos;revert&apos;,//回滚到上一个版本
&apos;build&apos;,//编译相关的修改，例如发布版本、对项目构建或者依赖的改动
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配置husky&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx husky add .husky/commit-msg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在生成的commit-msg文件中添加下面的命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env sh
. &quot;$(dirname -- &quot;$0&quot;)/_/husky.sh&quot;
pnpm commitlint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们 commit 提交信息时，就不能再随意写了，必须是 git commit -m &apos;fix: xxx&apos; 符合类型的才可以，&lt;strong&gt;需要注意的是类型的后面需要用英文的 :，并且冒号后面是需要空一格的，这个是不能省略的&lt;/strong&gt;；&lt;/p&gt;
&lt;h2&gt;强制使用pnpm包管理器工具&lt;/h2&gt;
&lt;p&gt;团队开发项目的时候，需要统一包管理器工具,因为不同包管理器工具下载同一个依赖,可能版本不一样,&lt;/p&gt;
&lt;p&gt;导致项目出现bug问题,因此包管理器工具需要统一管理！！！&lt;/p&gt;
&lt;p&gt;在根目录创建&lt;code&gt;scritps/preinstall.js&lt;/code&gt;文件，添加下面的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!/pnpm/.test(process.env.npm_execpath || &apos;&apos;)) {
  console.warn(
    `\u001b[33mThis repository must using pnpm as the package manager ` +
    ` for scripts to work properly.\u001b[39m\n`,
  )
  process.exit(1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
	&quot;preinstall&quot;: &quot;node ./scripts/preinstall.js&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;当我们使用npm或者yarn来安装包的时候，就会报错了。原理就是在install的时候会触发preinstall（npm提供的生命周期钩子）这个文件里面的代码。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;集成element-plus&lt;/h2&gt;
&lt;p&gt;安装以下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm i @element-plus/icons-vue
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;pnpm install element-plus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;main.ts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createApp } from &apos;vue&apos;
import App from &apos;./App.vue&apos;
import ElementPlus from &apos;element-plus&apos;
import &apos;element-plus/dist/index.css&apos;
//@ts-expect-error忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from &apos;element-plus/dist/locale/zh-cn.mjs&apos;

const app = createApp(App)
app.use(ElementPlus, {
  locale: zhCn,
})
app.mount(&apos;#app&apos;)

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;element 图标注册为全局组件&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;src/components/index.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import SvgIcon from &apos;./SvgIcon/index.vue&apos;
import type { App, Component } from &apos;vue&apos;
const components: { [name: string]: Component } = { SvgIcon }
import * as ElementPlusIconsVue from &apos;@element-plus/icons-vue&apos;

export default {
  install(app: App) {
    Object.keys(components).forEach((key: string) =&amp;gt; {
      app.component(key, components[key])
    })
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
      app.component(key, component)
    }
  },
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;src别名的配置&lt;/h2&gt;
&lt;p&gt;在开发项目的时候文件与文件关系可能很复杂，因此我们需要给src文件夹配置一个别名&lt;/p&gt;
&lt;p&gt;编辑&lt;code&gt;vite.config.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {defineConfig} from &apos;vite&apos;
import vue from &apos;@vitejs/plugin-vue&apos;
import path from &apos;path&apos;
export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
            &quot;@&quot;: path.resolve(&quot;./src&quot;) // 相对路径别名配置，使用 @ 代替 src
        }
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;TypeScript 编译配置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tsconfig.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;./&quot;, // 解析非相对模块的基地址，默认是当前目录
    &quot;paths&quot;: { //路径映射，相对于baseUrl
      &quot;@/*&quot;: [&quot;src/*&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tsconfig.app.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;extends&quot;: &quot;@vue/tsconfig/tsconfig.dom.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;tsBuildInfoFile&quot;: &quot;./node_modules/.tmp/tsconfig.app.tsbuildinfo&quot;,
    &quot;baseUrl&quot;: &quot;./&quot;, // 解析非相对模块的基地址，默认是当前目录
    &quot;paths&quot;: {
      //路径映射，相对于baseUrl
      &quot;@/*&quot;: [&quot;src/*&quot;]
    },
    /* Linting */
    &quot;strict&quot;: true,
    &quot;noUnusedLocals&quot;: false,
    &quot;noUnusedParameters&quot;: true,
    &quot;erasableSyntaxOnly&quot;: true,
    &quot;noFallthroughCasesInSwitch&quot;: true,
    &quot;noUncheckedSideEffectImports&quot;: true
  },
  &quot;include&quot;: [&quot;src/**/*.ts&quot;, &quot;src/**/*.tsx&quot;, &quot;src/**/*.vue&quot;]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;环境变量的配置&lt;/h2&gt;
&lt;p&gt;开发环境（development）
顾名思义，开发使用的环境，每位开发人员在自己的dev分支上干活，开发到一定程度，同事会合并代码，进行联调。&lt;/p&gt;
&lt;p&gt;测试环境（testing）
测试同事干活的环境啦，一般会由测试同事自己来部署，然后在此环境进行测试&lt;/p&gt;
&lt;p&gt;生产环境（production）
生产环境是指正式提供对外服务的，一般会关掉错误报告，打开错误日志。(正式提供给客户使用的环境。)&lt;/p&gt;
&lt;p&gt;注意:一般情况下，一个环境对应一台服务器,也有的公司开发与测试环境是一台服务器&lt;/p&gt;
&lt;p&gt;项目根目录分别添加 开发、生产和测试环境的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.env.development
.env.production
.env.test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;文件内容:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = &apos;development&apos;
VITE_APP_TITLE = &apos;硅谷甄选运营平台&apos;
VITE_APP_BASE_API = &apos;/dev-api&apos;
VITE_SERVE=&quot;http://xxxx.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NODE_ENV = &apos;production&apos;
VITE_APP_TITLE = &apos;硅谷甄选运营平台&apos;
VITE_APP_BASE_API = &apos;/prod-api&apos;
VITE_SERVE=&quot;http://xxxx.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = &apos;test&apos;
VITE_APP_TITLE = &apos;硅谷甄选运营平台&apos;
VITE_APP_BASE_API = &apos;/test-api&apos;
VITE_SERVE=&quot;http://xxxx.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置运行命令：package.json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite --open&quot;,
    &quot;build:test&quot;: &quot;vue-tsc &amp;amp;&amp;amp; vite build --mode test&quot;,
    &quot;build:pro&quot;: &quot;vue-tsc &amp;amp;&amp;amp; vite build --mode production&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过import.meta.env获取环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(import.meta.env);
console.log(import.meta.env.BASE_URL);
console.log(import.meta.env.VITE_SERVE);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SVG图标的封装&lt;/h2&gt;
&lt;p&gt;在开发项目的时候经常会用到svg矢量图,而且我们使用SVG以后，页面上加载的不再是图片资源,&lt;/p&gt;
&lt;p&gt;这对页面性能来说是个很大的提升，而且我们SVG文件比img要小的很多，放在项目中几乎不占用资源。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;安装SVG依赖插件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install vite-plugin-svg-icons -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;在&lt;code&gt;vite.config.ts&lt;/code&gt;中配置插件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createSvgIconsPlugin } from &apos;vite-plugin-svg-icons&apos;
import path from &apos;path&apos;
export default () =&amp;gt; {
  return {
    plugins: [
      createSvgIconsPlugin({
        // Specify the icon folder to be cached
        iconDirs: [path.resolve(process.cwd(), &apos;src/assets/icons&apos;)],
        // Specify symbolId format
        symbolId: &apos;icon-[dir]-[name]&apos;,
      }),
    ],
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;入口文件导入&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;virtual:svg-icons-register&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;vite-env.d.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;reference types=&quot;vite-plugin-svg-icons/client&quot; /&amp;gt;

declare module &apos;virtual:svg-icons-register&apos; {
  const component: any;
  export default component;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在assert目录下创建&lt;code&gt;icons&lt;/code&gt;文件夹，将svg图标放入icons文件夹中；要与vite.config.ts中配置的路径一致&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用方式:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h2&amp;gt;SVG的使用&amp;lt;/h2&amp;gt;
    &amp;lt;svg style=&quot;width: 100px; height: 100px;&quot;&amp;gt;
      &amp;lt;!-- xlink:href 执行用哪一个图标，属性值务必以#icon开头-图标名字 --&amp;gt;
       &amp;lt;!-- fill可以填充图标颜色 --&amp;gt;
      &amp;lt;use xlink:href=&quot;#icon-load&quot; fill=&quot;red&quot;&amp;gt;&amp;lt;/use&amp;gt;
    &amp;lt;/svg&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为项目很多模块需要使用图标,因此把它封装为全局组件&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在src/components目录下创建一个SvgIcon组件:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;svg :style=&quot;{ width: width, height: height }&quot;&amp;gt;
      &amp;lt;use :xlink:href=&quot;prefix + name&quot; :fill=&quot;color&quot;&amp;gt;&amp;lt;/use&amp;gt;
    &amp;lt;/svg&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
defineProps({
  //xlink:href属性值的前缀
  prefix: {
    type: String,
    default: &apos;#icon-&apos;
  },
  //svg矢量图的名字
  name: String,
  //svg图标的颜色
  color: {
    type: String,
    default: &quot;&quot;
  },
  //svg宽度
  width: {
    type: String,
    default: &apos;16px&apos;
  },
  //svg高度
  height: {
    type: String,
    default: &apos;16px&apos;
  }

})
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h2&amp;gt;SVG的使用&amp;lt;/h2&amp;gt;
    &amp;lt;SvgIcon name=&quot;home&quot; color=&quot;red&quot; width=&quot;100px&quot; height=&quot;100px&quot;&amp;gt;&amp;lt;/SvgIcon&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import SvgIcon from &apos;@/components/SvgIcon/index.vue&apos;
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;封装为全局组件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为项目很多模块需要使用图标,因此把它封装为全局组件&lt;/p&gt;
&lt;p&gt;在src文件夹&lt;code&gt;components&lt;/code&gt;目录下创建一个&lt;code&gt;index.ts&lt;/code&gt;文件，用于注册&lt;code&gt;components&lt;/code&gt;文件夹内部全部全局组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import SvgIcon from &apos;./SvgIcon/index.vue&apos;;
import type { App, Component } from &apos;vue&apos;;
const components: { [name: string]: Component } = { SvgIcon };
export default {
    install(app: App) {
        Object.keys(components).forEach((key: string) =&amp;gt; {
            app.component(key, components[key]);
        })
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在入口文件main.ts引入src/index.ts文件,通过app.use方法安装自定义插件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import globalComponent from &apos;@/components&apos;
app.use(globalComponent)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;集成sass&lt;/h2&gt;
&lt;p&gt;我们目前在组件内部已经可以使用scss样式,因为在配置styleLint工具的时候，项目当中已经安装过&lt;code&gt;sass&lt;/code&gt; &lt;code&gt;sass-loader&lt;/code&gt;,因此我们再组件内可以使用&lt;code&gt;scss&lt;/code&gt;语法需要加上&lt;code&gt;lang=&quot;scss&quot;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style scoped lang=&quot;scss&quot;&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在src下创建styles，并创建index.scss&lt;/p&gt;
&lt;p&gt;接下来我们为项目添加一些全局的样式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引入全局样式&lt;/strong&gt;
在main.ts中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;@/styles/index.scss&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建&lt;code&gt;src/styles/reset.scss&lt;/code&gt;
npm地址：&lt;a href=&quot;https://www.npmjs.com/package/reset.scss?activeTab=code&quot;&gt;地址&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * ENGINE
 * v0.2 | 20150615
 * License: none (public domain)
 */

*,
*:after,
*:before {
  box-sizing: border-box;

  outline: none;
}

html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
  font: inherit;
  font-size: 100%;

  margin: 0;
  padding: 0;

  vertical-align: baseline;

  border: 0;
}

article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
  display: block;
}

body {
  line-height: 1;
}

ol,
ul {
  list-style: none;
}

blockquote,
q {
  quotes: none;
  &amp;amp;:before,
  &amp;amp;:after {
    content: &apos;&apos;;
    content: none;
  }
}

sub,
sup {
  font-size: 75%;
  line-height: 0;

  position: relative;

  vertical-align: baseline;
}
sup {
  top: -0.5em;
}
sub {
  bottom: -0.25em;
}

table {
  border-spacing: 0;
  border-collapse: collapse;
}

input,
textarea,
button {
  font-family: inhert;
  font-size: inherit;

  color: inherit;
}

select {
  text-indent: 0.01px;
  text-overflow: &apos;&apos;;

  border: 0;
  border-radius: 0;

  -webkit-appearance: none;
  -moz-appearance: none;
}
select::-ms-expand {
  display: none;
}

code,
pre {
  font-family: monospace, monospace;
  font-size: 1em;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;index.scss&lt;/code&gt;中引入reset.scss&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@use &apos;./reset.scss&apos; as *;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是你会发现在&lt;code&gt;src/styles/index.scss&lt;/code&gt;全局样式文件中没有办法使用&lt;code&gt;$&lt;/code&gt;变量;因此需要给项目中引入全局变量&lt;code&gt;$&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;style/variable.scss&lt;/code&gt;创建一个&lt;code&gt;variable.scss&lt;/code&gt;文件&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;vite.config.ts&lt;/code&gt;文件配置如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default defineConfig((config) =&amp;gt; {
  css:{
    preprocessorOptions:{
      scss:{
        additionalData: &apos;@use &quot;@/styles/variable.scss&quot; as *;&apos;
      }
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用方式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$bgColor: #f0f2f5;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style scoped lang=&quot;scss&quot;&amp;gt;
  .app {
    background-color: $bgColor;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;mock数据&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm install -D vite-plugin-mock mockjs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置&lt;code&gt;vite.config.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
import vue from &apos;@vitejs/plugin-vue&apos;
import path from &apos;path&apos;
import { createSvgIconsPlugin } from &apos;vite-plugin-svg-icons&apos;
import { viteMockServe } from &apos;vite-plugin-mock&apos;

// https://vite.dev/config/
export default defineConfig((command) =&amp;gt; {
  return {
    plugins: [
      vue(),
      createSvgIconsPlugin({
        // Specify the icon folder to be cached
        iconDirs: [path.resolve(process.cwd(), &apos;src/assets/icons&apos;)],
        // Specify symbolId format
        symbolId: &apos;icon-[dir]-[name]&apos;,
      }),
      viteMockServe({
        enable: command.command === &apos;serve&apos;,
      }),
    ],
    resolve: {
      alias: {
        &apos;@&apos;: path.resolve(&apos;./src&apos;), // 相对路径别名配置，使用 @ 代替 src
      },
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: &apos;@use &quot;@/styles/variable.scss&quot; as *;&apos;,
        },
      },
    },
  }
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根目录创建&lt;code&gt;mock&lt;/code&gt;文件夹&lt;/p&gt;
&lt;p&gt;建一个&lt;code&gt;user.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//用户信息数据
function createUserList() {
  return [
    {
      userId: 1,
      avatar: &apos;https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif&apos;,
      username: &apos;admin&apos;,
      password: &apos;111111&apos;,
      desc: &apos;平台管理员&apos;,
      roles: [&apos;平台管理员&apos;],
      buttons: [&apos;cuser.detail&apos;],
      routes: [&apos;home&apos;],
      token: &apos;Admin Token&apos;,
    },
    {
      userId: 2,
      avatar: &apos;https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif&apos;,
      username: &apos;system&apos;,
      password: &apos;111111&apos;,
      desc: &apos;系统管理员&apos;,
      roles: [&apos;系统管理员&apos;],
      buttons: [&apos;cuser.detail&apos;, &apos;cuser.user&apos;],
      routes: [&apos;home&apos;],
      token: &apos;System Token&apos;,
    },
  ]
}

export default [
  // 用户登录接口
  {
    url: &apos;/api/user/login&apos;, //请求地址
    method: &apos;post&apos;, //请求方式
    response: ({ body }) =&amp;gt; {
      //获取请求体携带过来的用户名与密码
      const { username, password } = body
      //调用获取用户信息函数,用于判断是否有此用户
      const checkUser = createUserList().find((item) =&amp;gt; item.username === username &amp;amp;&amp;amp; item.password === password)
      //没有用户返回失败信息
      if (!checkUser) {
        return { code: 201, data: { message: &apos;账号或者密码不正确&apos; } }
      }
      //如果有返回成功信息
      const { token } = checkUser
      return { code: 200, data: { token } }
    },
  },
  // 获取用户信息
  {
    url: &apos;/api/user/info&apos;,
    method: &apos;get&apos;,
    response: (request) =&amp;gt; {
      //获取请求头携带token
      const token = request.headers.token
      //查看用户信息是否包含有次token用户
      const checkUser = createUserList().find((item) =&amp;gt; item.token === token)
      //没有返回失败的信息
      if (!checkUser) {
        return { code: 201, data: { message: &apos;获取用户信息失败&apos; } }
      }
      //如果有返回成功信息
      return { code: 200, data: { checkUser } }
    },
  },
]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;main.ts中测试下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import axios from &apos;axios&apos;

axios({
  url: &apos;/api/user/login&apos;,
  method: &apos;post&apos;,
  data: {
    username: &apos;admin&apos;,
    password: &apos;111111&apos;,
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;axios&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm install axios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在开发项目的时候避免不了与后端进行交互,因此我们需要使用axios插件实现发送网络请求。在开发项目的时候&lt;/p&gt;
&lt;p&gt;我们经常会把axios进行二次封装。&lt;/p&gt;
&lt;p&gt;目的:&lt;/p&gt;
&lt;p&gt;1:使用请求拦截器，可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)&lt;/p&gt;
&lt;p&gt;2:使用响应拦截器，可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)&lt;/p&gt;
&lt;p&gt;在根目录下创建&lt;code&gt;utils/request.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// axios二次封装
import axios from &apos;axios&apos;
import { ElMessage } from &apos;element-plus&apos;

let request = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API, // 基础路径带上/api
  timeout: 5000,
})

//请求拦截器
request.interceptors.request.use((config) =&amp;gt; {
  //获取token,在请求头携带
  const token = localStorage.getItem(&apos;Authorization&apos;)
  if (token) {
    config.headers.Authorization = token
  }
  return config
})

//响应拦截器
request.interceptors.response.use(
  (response) =&amp;gt; {
    return response.data
  },
  (error) =&amp;gt; {
    let msg: string = &apos;&apos;
    let status: number = error.response.status
    switch (status) {
      case 401:
        msg = &apos;token过期&apos;
        break
      case 403:
        msg = &apos;无权访问&apos;
        break
      case 404:
        msg = &apos;请求地址错误&apos;
        break
      case 500:
        msg = &apos;服务器错误&apos;
        break
      default:
        msg = &apos;未知错误&apos;
        break
    }
    ElMessage.error(msg)
    return Promise.reject(error)
  },
)

export default request

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = &apos;development&apos;
VITE_APP_TITLE = &apos;ZZY后台&apos;
VITE_APP_BASE_API = &apos;/api&apos;
VITE_SERVE=&apos;http://127.0.0.1:8080&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单测试下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { onMounted } from &apos;vue&apos;;
import request from &apos;./utils/request&apos;;

onMounted(() =&amp;gt; {
  request({
    url: &apos;/user/login&apos;,
    method: &apos;post&apos;,
    data: {
      username: &apos;admin&apos;,
      password: &apos;111111&apos;,
    },
  }).then((res) =&amp;gt; {
    console.log(res);
  })
})
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;API接口统一管理&lt;/h3&gt;
&lt;p&gt;在开发项目的时候,接口可能很多需要统一管理。&lt;/p&gt;
&lt;p&gt;在src目录下去创建api文件夹去统一管理项目的接口；&lt;/p&gt;
&lt;p&gt;&lt;code&gt;api&lt;/code&gt;创建user文件夹放用户相关接口&lt;/p&gt;
&lt;p&gt;user下创建&lt;code&gt;index.ts&lt;/code&gt;及&lt;code&gt;type.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 同意管理用户相关接口

import request from &apos;@/utils/request&apos;
import type { loginForm, loginResponse, userResponseData } from &apos;./type&apos;

// 管理接口地址
enum API {
  LOGIN_URL = &apos;/user/login&apos;,
  USER_INFO_URL = &apos;/user/info&apos;,
}

// 暴露请求函数

export const reqLogin = (data: loginForm) =&amp;gt;
  request.post&amp;lt;any, loginResponse&amp;gt;(API.LOGIN_URL, data)

export const reqUserInfo = () =&amp;gt;
  request.get&amp;lt;any, userResponseData&amp;gt;(API.USER_INFO_URL)

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 登录接口的参数ts类型
export interface loginForm {
  username: string
  password: string
}

export interface loginResponse {
  code: number
  data: dataType
}

interface dataType {
  token: string
}

interface userInfo {
  userId: number
  avatar: string
  username: string
  password: string
  desc: string
  roles: string[]
  buttons: string[]
  routes: string[]
  token: string
}

interface user {
  checkUser: userInfo
}

export interface userResponseData {
  code: number
  data: user
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
  import { ref, reactive, toRefs, onMounted } from &apos;vue&apos;
  import { reqLogin } from &apos;./api/user&apos;
  onMounted(() =&amp;gt; {
    reqLogin({ username: &apos;admin&apos;, password: &apos;111111&apos; }).then((res) =&amp;gt; {
      console.log(res)
    })
  })
&amp;lt;/script&amp;gt;

&amp;lt;style scoped lang=&quot;scss&quot;&amp;gt;&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;router&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm install vue-router
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建&lt;code&gt;router&lt;/code&gt;,&lt;code&gt;views&lt;/code&gt;文件夹&lt;/p&gt;
&lt;p&gt;以及&lt;code&gt;view/home/index.vue&lt;/code&gt;，&lt;code&gt;view/login/index.vue&lt;/code&gt;，&lt;code&gt;view/404/index.vue&lt;/code&gt;，&lt;code&gt;router/index.ts&lt;/code&gt;，&lt;code&gt;router/routes.ts&lt;/code&gt;文件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;main.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import router from &apos;@/router&apos;

app.use(router)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 对外暴露配置路由（常量路由）
export const constantRoute = [
  {
    path: &apos;/login&apos;,
    name: &apos;login&apos;,
    component: () =&amp;gt; import(&apos;@/views/login/index.vue&apos;),
  },
  {
    path: &apos;/&apos;,
    name: &apos;home&apos;,
    component: () =&amp;gt; import(&apos;@/views/home/index.vue&apos;),
  },
  {
    path: &apos;/404&apos;,
    name: &apos;404&apos;,
    component: () =&amp;gt; import(&apos;@/views/404/index.vue&apos;),
  },
  {
    // 任意路由
    path: &apos;/:pathMatch(.*)*&apos;,
    name: &apos;Any&apos;,
    redirect: &apos;/404&apos;,
  },
]

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 通过vue-router实现模版路由配置
import { createRouter, createWebHistory } from &apos;vue-router&apos;
import { constantRoute } from &apos;./routes&apos;

const router = createRouter({
  history: createWebHistory(),
  routes: constantRoute,
  // 滚动行为
  scrollBehavior() {
    return {
      left: 0,
      top: 0,
    }
  },
})

export default router


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;App.vue&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;router-view&amp;gt;&amp;lt;/router-view&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;style scoped lang=&quot;scss&quot;&amp;gt;&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pinia&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm i pinia
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建&lt;code&gt;src/store/index.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createPinia } from &apos;pinia&apos;

let pinia = createPinia()

export default pinia

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pinia from &apos;./store&apos;

app.use(pinia)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建&lt;code&gt;src/store/modules/user.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineStore } from &apos;pinia&apos;
import { reqLogin } from &apos;@/api/user&apos;
import type { loginForm } from &apos;@/api/user/type&apos;

let useUserStore = defineStore(&apos;User&apos;, {
  state: () =&amp;gt; {
    return {
      token: localStorage.getItem(&apos;Authorization&apos;) || &apos;&apos;,
    }
  },
  actions: {
    async userLogin(val: loginForm) {
      let response: any = await reqLogin(val)
      if (response.code === 200) {
        this.token = response.data.token
        localStorage.setItem(&apos;Authorization&apos;, response.data.token)
        return &apos;ok&apos;
      } else {
        return Promise.reject(new Error(response.data.message))
      }
    },
  },
  getters: {},
})

export default useUserStore

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;持久化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm i pinia-plugin-persistedstate

yarn add pinia-plugin-persistedstate

pnpm i pinia-plugin-persistedstate

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/store/index.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createPinia } from &quot;pinia&quot; //引入pinia
import piniaPluginPersistedstate from &apos;pinia-plugin-persistedstate&apos; //引入持久化插件

const pinia = createPinia() //创建pinia实例
pinia.use(piniaPluginPersistedstate) //将插件添加到 pinia 实例上

export default pinia //导出pinia用于main.js注册
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;基本使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将 persist 选项设置为 true，整个 Store 将使用默认持久化配置保存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineStore } from &quot;pinia&quot;

const useUserInfoStore = defineStore(&apos;userInfo&apos;, {
  // defineStore(&apos;userInfo&apos;,{})  userInfo就是这个仓库的名称name
  state: () =&amp;gt; ({
    username:&apos;赫赫&apos;,
    age: 23,
    like: &apos;girl&apos;,
  }),
  getters: {
        ...........
  },
  action：{
    .........
  },
  persist: true,
})

export default useUserInfoStore
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;i18n&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm install vue-i18n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;建个组件&lt;code&gt;/src/components/LanguageSwitcher/index.vue&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;!-- 主容器，需要 relative 定位 --&amp;gt;
  &amp;lt;div
    class=&quot;custom-hover-language-switcher&quot;
    @mouseenter=&quot;openMenu&quot;
    @mouseleave=&quot;closeMenuWithDelay&quot;
  &amp;gt;
    &amp;lt;!-- 触发器 --&amp;gt;
    &amp;lt;span class=&quot;switcher-trigger&quot;&amp;gt;
      &amp;lt;!-- 使用 Element Plus 的图标组件 --&amp;gt;
      &amp;lt;el-icon :size=&quot;15&quot;&amp;gt;
        &amp;lt;!-- &amp;lt;ChatDotRound /&amp;gt;  --&amp;gt;
        &amp;lt;SvgIcon name=&quot;internationalization&quot; height=&quot;19px&quot; width=&quot;20px&quot;&amp;gt;&amp;lt;/SvgIcon&amp;gt;
      &amp;lt;/el-icon&amp;gt;
      &amp;lt;!-- 可选：显示当前语言 --&amp;gt;
      &amp;lt;span class=&quot;current-lang-text&quot;&amp;gt;{{ currentLanguage.toUpperCase() }}&amp;lt;/span&amp;gt;
    &amp;lt;/span&amp;gt;

    &amp;lt;!-- 自定义下拉菜单，使用 v-if 控制显示隐藏 --&amp;gt;
    &amp;lt;!-- 注意：菜单本身不需要监听 hover 事件，因为父容器已经处理了 --&amp;gt;
    &amp;lt;transition name=&quot;dropdown-fade&quot;&amp;gt;
      &amp;lt;!-- 添加一个过渡效果，让显示/隐藏更平滑 --&amp;gt;
      &amp;lt;div v-if=&quot;isMenuOpen&quot; class=&quot;dropdown-menu&quot;&amp;gt;
        &amp;lt;!-- 遍历语言列表生成菜单项 --&amp;gt;
        &amp;lt;div
          v-for=&quot;lang in languages&quot;
          :key=&quot;lang.code&quot;
          class=&quot;menu-item&quot;
          :class=&quot;{ &apos;is-active&apos;: lang.code === currentLanguage }&quot;
          @click=&quot;handleCommand(lang.code)&quot;
        &amp;gt;
          {{ lang.name }}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/transition&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
  import { ref, defineProps, defineEmits, onUnmounted } from &apos;vue&apos; // 需要 onUnmounted 来清理定时器
  import { ElIcon } from &apos;element-plus&apos;
  import { ChatDotRound } from &apos;@element-plus/icons-vue&apos;
  import { useI18n } from &apos;vue-i18n&apos; // 假设你使用 vue-i18n

  const { locale, t } = useI18n()

  const props = defineProps({
    languages: {
      type: Array,
      default: () =&amp;gt; [
        { code: &apos;zh&apos;, name: &apos;中文&apos; },
        { code: &apos;en&apos;, name: &apos;English&apos; },
      ],
    },
  })

  // 响应式变量，控制下拉菜单的显示/隐藏
  const isMenuOpen = ref(false)

  // 响应式变量，存储当前选中的语言代码，用于高亮显示
  // 从 localStorage 读取或使用默认值 &apos;zh&apos;
  const currentLanguage = ref(localStorage.getItem(&apos;language&apos;) || &apos;zh&apos;)

  // 用于存储定时器的变量
  let closeTimer = null

  // 打开菜单的方法 (清除任何待定的关闭定时器)
  const openMenu = () =&amp;gt; {
    clearTimeout(closeTimer) // 清除定时器
    isMenuOpen.value = true
  }

  // 延迟关闭菜单的方法
  const closeMenuWithDelay = () =&amp;gt; {
    // 先清除旧的定时器，避免重复设置
    clearTimeout(closeTimer)
    // 设置一个新的定时器
    closeTimer = setTimeout(() =&amp;gt; {
      isMenuOpen.value = false
    }, 150) // 延迟 150 毫秒关闭，这个值可以根据需要调整
  }

  // 立即关闭菜单的方法 (用于点击菜单项后调用)
  const closeMenu = () =&amp;gt; {
    clearTimeout(closeTimer) // 立即关闭时也要清除定时器
    isMenuOpen.value = false
  }

  // 处理菜单项点击事件
  const handleCommand = (command) =&amp;gt; {
    console.log(&apos;切换到语言:&apos;, command)

    // 更新当前语言响应式变量，用于高亮显示
    currentLanguage.value = command

    // 执行您的语言切换逻辑
    locale.value = command
    localStorage.setItem(&apos;language&apos;, command)

    // 触发父组件的事件 (如果需要)
    // emit(&apos;changeLanguage&apos;, command);

    // 点击菜单项后立即关闭菜单
    closeMenu()
  }

  // 组件卸载时，确保清除定时器，防止内存泄漏
  onUnmounted(() =&amp;gt; {
    clearTimeout(closeTimer)
  })
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
  /* 主容器样式 */
  .custom-hover-language-switcher {
    position: relative; /* 相对定位，为下拉菜单提供定位参考 */
    display: inline-block; /* 使容器宽度包裹内容 */
    vertical-align: middle; /* 如果在行内使用，可以帮助对齐 */
    /* 确保有足够宽度包含触发器和菜单 */
    /* background-color: rgba(255,0,0,0.1); /* 临时添加背景色，用于调试 hover 区域 */
  }

  /* 触发器样式 */
  .switcher-trigger {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    padding: 8px 12px;
    /* background-color: rgba(64, 158, 255, 0.1); */
    border-radius: 6px;
    transition: all 0.3s ease;
    gap: 6px;
  }

  .switcher-trigger:hover {
    background-color: rgba(64, 158, 255, 0.2);
    transform: translateY(-1px);
  }

  /* 自定义下拉菜单样式 */
  .dropdown-menu {
    position: absolute;
    top: 100%;
    right: 0;
    z-index: 100;
    background-color: #fff;
    border: none;
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
    padding: 8px;
    margin-top: 8px;
    min-width: 120px;
    overflow: hidden;
    backdrop-filter: blur(10px);
  }

  /* 菜单项样式 */
  .menu-item {
    padding: 10px 16px;
    line-height: 20px;
    cursor: pointer;
    color: #606266;
    font-size: 14px;
    border-radius: 6px;
    margin: 2px 0;
    transition: all 0.3s ease;
  }

  /* 菜单项悬停样式 */
  .menu-item:hover {
    background-color: #f0f9ff;
    color: #409eff;
    transform: translateX(4px);
  }

  /* 当前激活菜单项样式 */
  .menu-item.is-active {
    font-weight: 600;
    color: #409eff;
    background-color: rgba(64, 158, 255, 0.1);
  }

  /* 过渡效果样式 */
  .dropdown-fade-enter-active {
    transition: all 0.3s ease;
  }
  .dropdown-fade-leave-active {
    transition: all 0.2s ease;
  }
  .dropdown-fade-enter-from {
    opacity: 0;
    transform: translateY(-10px) scale(0.95);
  }
  .dropdown-fade-leave-to {
    opacity: 0;
    transform: translateY(-5px) scale(0.98);
  }
  .current-lang-text {
    font-size: 11px;
    font-weight: 500;
    color: var(--el-color-primary);
  }

  @media (max-width: 768px) {
    .dropdown-menu {
      right: -10px;
      min-width: 100px;
    }

    .current-lang-text {
      display: none;
    }
  }
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建&lt;code&gt;src/i18n/index.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createI18n } from &apos;vue-i18n&apos;
import en from &apos;./lang/en&apos;
import zh from &apos;./lang/zh&apos;
let language = localStorage.getItem(&apos;language&apos;)
const i18n = createI18n({
  locale: language ? language : &apos;zh&apos;, // 默认是中文
  //   fallbackLocale: &apos;en&apos;, // 语言切换的时候是英文
  globalInjection: true, //全局配置$t
  legacy: false, //vue3写法
  messages: { en, zh },
})

export default i18n

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建&lt;code&gt;src/i18n/lang/en.ts&lt;/code&gt;,&lt;code&gt;src/i18n/lang/zh.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 中文语言包

export default {
  common: {
    login: &apos;登录&apos;,
    logout: &apos;退出登录&apos;,
    home: &apos;首页&apos;,
    admin: &apos;管理员&apos;,
  },
  login: {
    username: &apos;用户名&apos;,
    password: &apos;密码&apos;,
    loginBtn: &apos;立即登录&apos;,
  },
  greeting: {
    morning: &apos;早上好！&apos;,
    noon: &apos;上午好！&apos;,
    afternoon: &apos;下午好！&apos;,
    evening: &apos;晚上好！&apos;,
  },
  menu: {
    system: &apos;系统&apos;,
  },
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 英文语言包

export default {
  common: {
    login: &apos;Login&apos;,
    logout: &apos;Logout&apos;,
    home: &apos;Home&apos;,
    admin: &apos;Administrator&apos;,
  },
  login: {
    username: &apos;Username&apos;,
    password: &apos;Password&apos;,
    loginBtn: &apos;Sign In&apos;,
  },
  greeting: {
    morning: &apos;Good Morning!&apos;,
    noon: &apos;Good Morning!&apos;,
    afternoon: &apos;Good Afternoon!&apos;,
    evening: &apos;Good Evening!&apos;,
  },
  menu: {
    system: &apos;System&apos;,
  },
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;App.vue&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;el-config-provider :locale=&quot;ellocale&quot;&amp;gt;
    &amp;lt;RouterView&amp;gt;&amp;lt;/RouterView&amp;gt;
  &amp;lt;/el-config-provider&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
  import zhCn from &apos;element-plus/es/locale/lang/zh-cn&apos;
  import en from &apos;element-plus/es/locale/lang/en&apos;

  import { useI18n } from &apos;vue-i18n&apos;
  import { computed } from &apos;vue&apos;

  const { locale } = useI18n()
  const ellocale = computed(() =&amp;gt; (locale.value == &apos;zh&apos; ? zhCn : en))
&amp;lt;/script&amp;gt;

&amp;lt;style lang=&quot;scss&quot; scoped&amp;gt;&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import i18n from &apos;@/i18n&apos;

app.use(i18n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vue文件使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;span&amp;gt;{{ t(&apos;menu.system&apos;) }}&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ts使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import { useI18n } from &apos;vue-i18n&apos;
  const { locale, t } = useI18n()
  console.log(t(&apos;menu.system&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;nprogress&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm i nprogress
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;vite-env.d.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare module &apos;nprogress&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/permission.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 路由鉴权
import router from &apos;@/router&apos;
import nprogress from &apos;nprogress&apos;
import &apos;nprogress/nprogress.css&apos;

// 全局前置守卫
router.beforeEach((to, from, next) =&amp;gt; {
  // to: 即将要进入的目标路由对象
  // from: 当前导航正要离开的路由
  // next: 调用该方法后，才能进入下一个钩子

  nprogress.start()
  next()
})

// 全局后置守卫
router.afterEach((to, from) =&amp;gt; {
  nprogress.done()
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;./permission&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/zxyang3636/vue3_admin_template&quot;&gt;地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;reference:&lt;a href=&quot;https://gitee.com/jch1011/vue3_admin_template-bj1&quot;&gt;reference&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;笔记参考：&lt;a href=&quot;https://www.yuque.com/aosika-j6ubd/kucrsm/sggz6rsnecr0hhlf?singleDoc#WgHRr&quot;&gt;语雀&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yuque.com/aosika-j6ubd/kucrsm/gyw43hwgahtz3tzg#faJNh&quot;&gt;语雀&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Vue3</title><link>https://zzyang.top/posts/vue3-s/</link><guid isPermaLink="true">https://zzyang.top/posts/vue3-s/</guid><pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Vue3 快速上手&lt;/h1&gt;
&lt;h2&gt;Vue3 简介&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;2020 年 9 月 18 日，&lt;code&gt;Vue.js&lt;/code&gt;发布版&lt;code&gt;3.0&lt;/code&gt;版本，代号：&lt;code&gt;One Piece&lt;/code&gt;（n&lt;/li&gt;
&lt;li&gt;经历了：&lt;a href=&quot;https://github.com/vuejs/core/commits/main&quot;&gt;4800+次提交&lt;/a&gt;、&lt;a href=&quot;https://github.com/vuejs/rfcs/tree/master/active-rfcs&quot;&gt;40+个 RFC&lt;/a&gt;、&lt;a href=&quot;https://github.com/vuejs/vue-next/pulls?q=is%3Apr+is%3Amerged+-author%3Aapp%2Fdependabot-preview+&quot;&gt;600+次 PR&lt;/a&gt;、&lt;a href=&quot;https://github.com/vuejs/core/graphs/contributors&quot;&gt;300+贡献者&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;官方发版地址：&lt;a href=&quot;https://github.com/vuejs/core/releases/tag/v3.0.0&quot;&gt;Release v3.0.0 One Piece · vuejs/core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;截止 2023 年 10 月，最新的公开版本为：&lt;code&gt;3.3.4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;性能的提升&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;打包大小减少&lt;code&gt;41%&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初次渲染快&lt;code&gt;55%&lt;/code&gt;, 更新渲染快&lt;code&gt;133%&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存减少&lt;code&gt;54%&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;新的特性&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Composition API&lt;/code&gt;（组合&lt;code&gt;API&lt;/code&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setup&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ref&lt;/code&gt;与&lt;code&gt;reactive&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;computed&lt;/code&gt;与&lt;code&gt;watch&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;......&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建 Vue3 工程&lt;/h2&gt;
&lt;h3&gt;基于 vue-cli 创建&lt;/h3&gt;
&lt;p&gt;点击查看&lt;a href=&quot;https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;备注：目前&lt;code&gt;vue-cli&lt;/code&gt;已处于维护模式，官方推荐基于 &lt;code&gt;Vite&lt;/code&gt; 创建项目。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;## 查看@vue/cli版本，确保@vue/cli版本在4.5.0以上
vue --version

## 安装或者升级你的@vue/cli
npm install -g @vue/cli

## 执行创建命令
vue create vue_test

##  随后选择3.x
##  Choose a version of Vue.js that you want to start the project with (Use arrow keys)
##  &amp;gt; 3.x
##    2.x

## 启动
cd vue_test
npm run serve
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基于 vite 创建&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;vite&lt;/code&gt; 是新一代前端构建工具，官网地址：&lt;a href=&quot;https://vitejs.cn/&quot;&gt;https://vitejs.cn&lt;/a&gt;，&lt;code&gt;vite&lt;/code&gt;的优势如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;轻量快速的热重载（&lt;code&gt;HMR&lt;/code&gt;），能实现极速的服务启动。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;TypeScript&lt;/code&gt;、&lt;code&gt;JSX&lt;/code&gt;、&lt;code&gt;CSS&lt;/code&gt; 等支持开箱即用。&lt;/li&gt;
&lt;li&gt;真正的按需编译，不再等待整个应用编译完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体操作如下（点击查看&lt;a href=&quot;https://cn.vuejs.org/guide/quick-start.html#creating-a-vue-application&quot;&gt;官方文档&lt;/a&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 1.创建命令
npm create vue@latest

## 2.具体配置
## 配置项目名称
√ Project name: vue3_test
## 是否添加TypeScript支持
√ Add TypeScript?  Yes
## 是否添加JSX支持
√ Add JSX Support?  No
## 是否添加路由环境
√ Add Vue Router for Single Page Application development?  No
## 是否添加pinia环境
√ Add Pinia for state management?  No
## 是否添加单元测试
√ Add Vitest for Unit Testing?  No
## 是否添加端到端测试方案
√ Add an End-to-End Testing Solution? » No
## 是否添加ESLint语法检查
√ Add ESLint for code quality?  Yes
## 是否添加Prettiert代码格式化
√ Add Prettier for code formatting?  No
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自己动手编写一个 App 组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;app&quot;&amp;gt;
    &amp;lt;h1&amp;gt;你好啊！&amp;lt;/h1&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot;&amp;gt;
export default {
  name: &quot;App&quot;, //组件名
};
&amp;lt;/script&amp;gt;

&amp;lt;style&amp;gt;
.app {
  background-color: #ddd;
  box-shadow: 0 0 10px;
  border-radius: 10px;
  padding: 20px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Vite&lt;/code&gt; 项目中，&lt;code&gt;index.html&lt;/code&gt; 是项目的入口文件，在项目最外层。&lt;/li&gt;
&lt;li&gt;加载&lt;code&gt;index.html&lt;/code&gt;后，&lt;code&gt;Vite&lt;/code&gt; 解析 &lt;code&gt;&amp;lt;script type=&quot;module&quot; src=&quot;xxx&quot;&amp;gt;&lt;/code&gt; 指向的&lt;code&gt;JavaScript&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vue3&lt;/code&gt;中是通过 &lt;code&gt;createApp&lt;/code&gt; 函数创建一个应用实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vue3 核心语法&lt;/h2&gt;
&lt;h3&gt;OptionsAPI 与 CompositionAPI&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Vue2&lt;/code&gt;的&lt;code&gt;API&lt;/code&gt;设计是&lt;code&gt;Options&lt;/code&gt;（配置）风格的。选项式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vue3&lt;/code&gt;的&lt;code&gt;API&lt;/code&gt;设计是&lt;code&gt;Composition&lt;/code&gt;（组合）风格的。组合式&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Options API 的弊端&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Options&lt;/code&gt;类型的 &lt;code&gt;API&lt;/code&gt;，数据、方法、计算属性等，是分散在：&lt;code&gt;data&lt;/code&gt;、&lt;code&gt;methods&lt;/code&gt;、&lt;code&gt;computed&lt;/code&gt;中的，若想新增或者修改一个需求，就需要分别修改：&lt;code&gt;data&lt;/code&gt;、&lt;code&gt;methods&lt;/code&gt;、&lt;code&gt;computed&lt;/code&gt;，不便于维护和复用。&lt;/p&gt;
&lt;h3&gt;Composition API 的优势&lt;/h3&gt;
&lt;p&gt;可以用函数的方式，更加优雅的组织代码，让相关功能的代码更加有序的组织在一起。&lt;/p&gt;
&lt;h3&gt;setup 概述&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;setup&lt;/code&gt;是&lt;code&gt;Vue3&lt;/code&gt;中一个新的配置项，值是一个函数，它是 &lt;code&gt;Composition API&lt;/code&gt; &lt;strong&gt;“表演的舞台&lt;/strong&gt;&lt;em&gt;&lt;strong&gt;”&lt;/strong&gt;&lt;/em&gt;，组件中所用到的：数据、方法、计算属性、监视......等等，均配置在&lt;code&gt;setup&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;特点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setup&lt;/code&gt;函数返回的对象中的内容，可直接在模板中使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setup&lt;/code&gt;中访问&lt;code&gt;this&lt;/code&gt;是&lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setup&lt;/code&gt;函数会在&lt;code&gt;beforeCreate&lt;/code&gt;之前调用，它是“领先”所有钩子执行的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;setup 的返回值&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;若返回一个&lt;strong&gt;对象&lt;/strong&gt;：则对象中的：属性、方法等，在模板中均可以直接使用**（重点关注）。**&lt;/li&gt;
&lt;li&gt;若返回一个&lt;strong&gt;函数&lt;/strong&gt;：则可以自定义渲染内容，代码如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;setup(){ return ()=&amp;gt; &apos;你好啊！&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;setup 与 Options API 的关系&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Vue2&lt;/code&gt; 的配置（&lt;code&gt;data&lt;/code&gt;、&lt;code&gt;methos&lt;/code&gt;......）中&lt;strong&gt;可以访问到&lt;/strong&gt; &lt;code&gt;setup&lt;/code&gt;中的属性、方法。&lt;/li&gt;
&lt;li&gt;但在&lt;code&gt;setup&lt;/code&gt;中&lt;strong&gt;不能访问到&lt;/strong&gt;&lt;code&gt;Vue2&lt;/code&gt;的配置（&lt;code&gt;data&lt;/code&gt;、&lt;code&gt;methos&lt;/code&gt;......）。&lt;/li&gt;
&lt;li&gt;如果与&lt;code&gt;Vue2&lt;/code&gt;冲突，则&lt;code&gt;setup&lt;/code&gt;优先。setup 可以与 data、methods 共存但不推荐&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;setup 语法糖&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;setup&lt;/code&gt;函数有一个语法糖，这个语法糖，可以让我们把&lt;code&gt;setup&lt;/code&gt;独立出去，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h2&amp;gt;姓名：{{ name }}&amp;lt;/h2&amp;gt;
    &amp;lt;h2&amp;gt;年龄：{{ age }}&amp;lt;/h2&amp;gt;
    &amp;lt;button @click=&quot;changeName&quot;&amp;gt;修改名字&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;changeAge&quot;&amp;gt;年龄+1&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;showTel&quot;&amp;gt;点我查看联系方式&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;!-- 下面的写法是setup语法糖 --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
// 数据，原来写在data中（注意：此时的name、age、tel数据都不是响应式数据）
let name = &quot;张三&quot;;
let age = 18;
let tel = &quot;13888888888&quot;;

// 方法，原来写在methods中
function changeName() {
  name = &quot;zhang-san&quot;; //注意：此时这么修改name页面是不变化的
  console.log(name);
}
function changeAge() {
  age += 1; //注意：此时这么修改age页面是不变化的
  console.log(age);
}
function showTel() {
  alert(tel);
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;指定组件名字&lt;/h4&gt;
&lt;p&gt;扩展：上述代码，还需要编写一个不写&lt;code&gt;setup&lt;/code&gt;的&lt;code&gt;script&lt;/code&gt;标签，去指定组件名字，比较麻烦，我们可以借助&lt;code&gt;vite&lt;/code&gt;中的插件简化&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一步：&lt;code&gt;npm i vite-plugin-vue-setup-extend -D&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第二步：&lt;code&gt;vite.config.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &quot;vite&quot;;
import VueSetupExtend from &quot;vite-plugin-vue-setup-extend&quot;;

export default defineConfig({
  plugins: [VueSetupExtend()],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Vue 3.3+ 中引入了 &lt;code&gt;defineOptions&lt;/code&gt;，它可以让我们在 &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; 中直接定义这些组件选项，而不需要切换回传统的 &lt;code&gt;export default&lt;/code&gt; 语法。&lt;/p&gt;
&lt;p&gt;defineOptions 的作用是集中管理组件的元信息和配置选项。以下是一些常见的用途：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义组件名称 （name）：用于调试工具（如 Vue DevTools）或递归组件。&lt;/li&gt;
&lt;li&gt;控制属性继承 （inheritAttrs）：决定是否将父组件传递的非 prop 属性自动绑定到根元素。&lt;/li&gt;
&lt;li&gt;自定义选项 ：可以定义任意自定义的组件选项。&lt;/li&gt;
&lt;li&gt;其他高级配置 ：如 customElement 配置等。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;这是一个组件&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;{{ message }}&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
import { ref } from &quot;vue&quot;;

// 使用 defineOptions 定义组件选项
defineOptions({
  name: &quot;MyComponent&quot;, // 组件名称
  inheritAttrs: false, // 禁用属性继承
  customOption: &quot;This is a custom option&quot;, // 自定义选项
});

const message = ref(&quot;Hello, Vue 3!&quot;);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;ref 创建：基本类型的响应式数据&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：定义响应式变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语法：&lt;/strong&gt;&lt;code&gt;let xxx = ref(初始值)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回值&lt;/strong&gt;：一个&lt;code&gt;RefImpl&lt;/code&gt;的实例对象，简称&lt;code&gt;ref对象&lt;/code&gt;或&lt;code&gt;ref&lt;/code&gt;，&lt;code&gt;ref&lt;/code&gt;对象的&lt;code&gt;value&lt;/code&gt;&lt;strong&gt;属性是响应式的&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意点：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JS&lt;/code&gt;中操作数据需要：&lt;code&gt;xxx.value&lt;/code&gt;，但模板中不需要&lt;code&gt;.value&lt;/code&gt;，直接使用即可。&lt;/li&gt;
&lt;li&gt;对于&lt;code&gt;let name = ref(&apos;张三&apos;)&lt;/code&gt;来说，&lt;code&gt;name&lt;/code&gt;不是响应式的，&lt;code&gt;name.value&lt;/code&gt;是响应式的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h2&amp;gt;姓名：{{ name }}&amp;lt;/h2&amp;gt;
    &amp;lt;h2&amp;gt;年龄：{{ age }}&amp;lt;/h2&amp;gt;
    &amp;lt;button @click=&quot;changeName&quot;&amp;gt;修改名字&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;changeAge&quot;&amp;gt;年龄+1&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;showTel&quot;&amp;gt;点我查看联系方式&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { ref } from &quot;vue&quot;;
// name和age是一个RefImpl的实例对象，简称ref对象，它们的value属性是响应式的。
let name = ref(&quot;张三&quot;);
let age = ref(18);
// tel就是一个普通的字符串，不是响应式的
let tel = &quot;13888888888&quot;;

function changeName() {
  // JS中操作ref对象时候需要.value
  name.value = &quot;李四&quot;;
  console.log(name.value);

  // 注意：name不是响应式的，name.value是响应式的，所以如下代码并不会引起页面的更新。
  // name = ref(&apos;zhang-san&apos;)
}
function changeAge() {
  // JS中操作ref对象时候需要.value
  age.value += 1;
  console.log(age.value);
}
function showTel() {
  alert(tel);
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;reactive 创建：对象类型的响应式数据&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;strong&gt;定义一个&lt;/strong&gt;响应式对象&lt;/strong&gt;（基本类型不要用它，要用&lt;code&gt;ref&lt;/code&gt;，否则报错）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语法：&lt;/strong&gt;&lt;code&gt;let 响应式对象= reactive(源对象)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回值&lt;/strong&gt;：一个&lt;code&gt;Proxy&lt;/code&gt;的实例对象，简称：响应式对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意点：&lt;/strong&gt;&lt;code&gt;reactive&lt;/code&gt;定义的响应式数据是“深层次”的。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h2&amp;gt;汽车信息：一台{{ car.brand }}汽车，价值{{ car.price }}万&amp;lt;/h2&amp;gt;
    &amp;lt;h2&amp;gt;游戏列表：&amp;lt;/h2&amp;gt;
    &amp;lt;ul&amp;gt;
      &amp;lt;li v-for=&quot;g in games&quot; :key=&quot;g.id&quot;&amp;gt;{{ g.name }}&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
    &amp;lt;h2&amp;gt;测试：{{ obj.a.b.c.d }}&amp;lt;/h2&amp;gt;
    &amp;lt;button @click=&quot;changeCarPrice&quot;&amp;gt;修改汽车价格&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;changeFirstGame&quot;&amp;gt;修改第一游戏&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;test&quot;&amp;gt;测试&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup name=&quot;Person&quot;&amp;gt;
import { reactive } from &quot;vue&quot;;

// 数据
let car = reactive({ brand: &quot;奔驰&quot;, price: 100 });
let games = reactive([
  { id: &quot;ahsgdyfa01&quot;, name: &quot;英雄联盟&quot; },
  { id: &quot;ahsgdyfa02&quot;, name: &quot;王者荣耀&quot; },
  { id: &quot;ahsgdyfa03&quot;, name: &quot;原神&quot; },
]);
let obj = reactive({
  a: {
    b: {
      c: {
        d: 666,
      },
    },
  },
});

function changeCarPrice() {
  car.price += 10;
}
function changeFirstGame() {
  games[0].name = &quot;流星蝴蝶剑&quot;;
}
function test() {
  obj.a.b.c.d = 999;
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ref 创建：对象类型的响应式数据&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;其实&lt;code&gt;ref&lt;/code&gt;接收的数据可以是：&lt;strong&gt;基本类型&lt;/strong&gt;、&lt;strong&gt;对象类型&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;若&lt;code&gt;ref&lt;/code&gt;接收的是对象类型，内部其实也是调用了&lt;code&gt;reactive&lt;/code&gt;函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h2&amp;gt;汽车信息：一台{{ car.brand }}汽车，价值{{ car.price }}万&amp;lt;/h2&amp;gt;
    &amp;lt;h2&amp;gt;游戏列表：&amp;lt;/h2&amp;gt;
    &amp;lt;ul&amp;gt;
      &amp;lt;li v-for=&quot;g in games&quot; :key=&quot;g.id&quot;&amp;gt;{{ g.name }}&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
    &amp;lt;h2&amp;gt;测试：{{ obj.a.b.c.d }}&amp;lt;/h2&amp;gt;
    &amp;lt;button @click=&quot;changeCarPrice&quot;&amp;gt;修改汽车价格&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;changeFirstGame&quot;&amp;gt;修改第一游戏&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;test&quot;&amp;gt;测试&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup name=&quot;Person&quot;&amp;gt;
import { ref } from &quot;vue&quot;;

// 数据
let car = ref({ brand: &quot;奔驰&quot;, price: 100 });
let games = ref([
  { id: &quot;ahsgdyfa01&quot;, name: &quot;英雄联盟&quot; },
  { id: &quot;ahsgdyfa02&quot;, name: &quot;王者荣耀&quot; },
  { id: &quot;ahsgdyfa03&quot;, name: &quot;原神&quot; },
]);
let obj = ref({
  a: {
    b: {
      c: {
        d: 666,
      },
    },
  },
});

console.log(car);

function changeCarPrice() {
  car.value.price += 10;
}
function changeFirstGame() {
  games.value[0].name = &quot;流星蝴蝶剑&quot;;
}
function test() {
  obj.value.a.b.c.d = 999;
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ref 对比 reactive&lt;/h2&gt;
&lt;p&gt;宏观角度看：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ref&lt;/code&gt;用来定义：&lt;strong&gt;基本类型数据&lt;/strong&gt;、&lt;strong&gt;对象类型数据&lt;/strong&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;reactive&lt;/code&gt;用来定义：&lt;strong&gt;对象类型数据&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;区别：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ref&lt;/code&gt;创建的变量必须使用&lt;code&gt;.value&lt;/code&gt;（可以使用&lt;code&gt;volar&lt;/code&gt;插件自动添加&lt;code&gt;.value&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reactive&lt;/code&gt;重新分配一个新对象，会&lt;strong&gt;失去&lt;/strong&gt;响应式（可以使用&lt;code&gt;Object.assign&lt;/code&gt;去整体替换）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 Vue 3 的响应式系统中，如果你直接对一个由 reactive 创建的响应式对象重新赋值为一个新的对象（例如 state = newState），那么这个新的对象将失去响应式特性 。这是因为 Vue 的响应式系统是基于代理（Proxy）实现的，直接替换整个对象会导致 Vue 无法继续追踪新对象的变化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { reactive } from &quot;vue&quot;;

const state = reactive({ count: 0 });
state.count++; // 修改 count 的值会触发视图更新
// 当你直接重新分配一个新对象时，比如：
state = { count: 10 };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这实际上会将 state 指向一个全新的普通对象 { count: 10 }，而这个新对象并没有被 reactive 包裹，因此它不再是响应式的。Vue 的响应式系统只能追踪最初通过 reactive 创建的对象及其属性变化。&lt;/p&gt;
&lt;p&gt;为了避免失去响应式，可以使用 &lt;code&gt;Object.assign&lt;/code&gt; 或解构赋值的方式，将新对象的属性合并到现有的响应式对象中。这样可以确保 Vue 的响应式系统仍然能够追踪这些属性的变化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { reactive } from &quot;vue&quot;;

const state = reactive({ count: 0 });

// 使用 Object.assign 合并新对象
Object.assign(state, { count: 10, name: &quot;Vue&quot; });

console.log(state); // 输出：{ count: 10, name: &apos;Vue&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你也可以通过解构赋值的方式逐个更新属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const newState = { count: 10, name: &quot;Vue&quot; };

state.count = newState.count;
state.name = newState.name;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用原则：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;若需要一个基本类型的响应式数据，必须使用&lt;code&gt;ref&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;若需要一个响应式对象，层级不深，&lt;code&gt;ref&lt;/code&gt;、&lt;code&gt;reactive&lt;/code&gt;都可以。&lt;/li&gt;
&lt;li&gt;若需要一个响应式对象，且层级较深，推荐使用&lt;code&gt;reactive&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;toRefs 与 toRef&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;作用：将一个响应式对象中的每一个属性，转换为&lt;code&gt;ref&lt;/code&gt;对象。&lt;/li&gt;
&lt;li&gt;备注：&lt;code&gt;toRefs&lt;/code&gt;与&lt;code&gt;toRef&lt;/code&gt;功能一致，但&lt;code&gt;toRefs&lt;/code&gt;可以批量转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h2&amp;gt;姓名：{{ person.name }}&amp;lt;/h2&amp;gt;
    &amp;lt;h2&amp;gt;年龄：{{ person.age }}&amp;lt;/h2&amp;gt;
    &amp;lt;h2&amp;gt;性别：{{ person.gender }}&amp;lt;/h2&amp;gt;
    &amp;lt;button @click=&quot;changeName&quot;&amp;gt;修改名字&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;changeAge&quot;&amp;gt;年龄+1&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;showTel&quot;&amp;gt;点我查看联系方式&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { reactive, toRefs, toRef } from &quot;vue&quot;;

let person = reactive({
  name: &quot;张三&quot;,
  age: 18,
  gender: &quot;男&quot;,
  tel: &quot;18966666666&quot;,
});

// 通过toRefs将person对象中的n个属性批量取出，且依然保持响应式的能力
let { age, name } = toRefs(person);

// 通过toRef将person对象中的gender属性取出，且依然保持响应式的能力
let gender = toRef(person, &quot;gender&quot;);

const changeName = () =&amp;gt; {
  person.name += &quot;~&quot;;
};

const changeAge = () =&amp;gt; {
  person.age += 1;
};

const showTel = () =&amp;gt; {
  alert(person.tel);
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;computed&lt;/h2&gt;
&lt;p&gt;作用：根据已有数据计算出新数据（和&lt;code&gt;Vue2&lt;/code&gt;中的&lt;code&gt;computed&lt;/code&gt;作用一致）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;姓: &amp;lt;input type=&quot;text&quot; v-model=&quot;firstName&quot; /&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;名: &amp;lt;input type=&quot;text&quot; v-model=&quot;lastName&quot; /&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;全名: {{ fullName }}&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { ref, computed } from &quot;vue&quot;;

let firstName = ref(&quot;Zhang&quot;);
let lastName = ref(&quot;San&quot;);

let fullName = computed(() =&amp;gt; {
  return firstName.value + &quot;-&quot; + lastName.value;
});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-03-12_22-20-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;若想直接修改 fullName，是不可以的，这么定义的 fullName 是一个计算属性，且是只读的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;姓: &amp;lt;input type=&quot;text&quot; v-model=&quot;firstName&quot; /&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;名: &amp;lt;input type=&quot;text&quot; v-model=&quot;lastName&quot; /&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;全名: {{ fullName }}&amp;lt;/h3&amp;gt;
    &amp;lt;div&amp;gt;&amp;lt;button @click=&quot;updateFullName&quot;&amp;gt;update fullName&amp;lt;/button&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { ref, computed } from &quot;vue&quot;;

let firstName = ref(&quot;Zhang&quot;);
let lastName = ref(&quot;San&quot;);

let fullName = computed(() =&amp;gt; {
  return firstName.value + &quot;-&quot; + lastName.value;
});

const updateFullName = () =&amp;gt; {
  fullName.value = &quot;liSi&quot;;
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-03-12_22-38-22.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-03-12_22-27-15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这么定义的 fullName 是一个计算属性，可读可写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;姓: &amp;lt;input type=&quot;text&quot; v-model=&quot;firstName&quot; /&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;名: &amp;lt;input type=&quot;text&quot; v-model=&quot;lastName&quot; /&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;全名: {{ fullName }}&amp;lt;/h3&amp;gt;
    &amp;lt;div&amp;gt;&amp;lt;button @click=&quot;updateFullName&quot;&amp;gt;update fullName&amp;lt;/button&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { ref, computed } from &quot;vue&quot;;

let firstName = ref(&quot;Zhang&quot;);
let lastName = ref(&quot;San&quot;);

let fullName = computed({
  get() {
    return firstName.value + &quot;-&quot; + lastName.value;
  },
  set(val) {
    // val是updateFullName方法修改后返回的新值
    // console.log(val);
    const [str1, str2] = val.split(&quot;-&quot;);
    firstName.value = str1;
    lastName.value = str2;
  },
});

const updateFullName = () =&amp;gt; {
  fullName.value = &quot;Li-Si&quot;;
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/1899836063048794112.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;watch&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;作用：监视数据的变化（和&lt;code&gt;Vue2&lt;/code&gt;中的&lt;code&gt;watch&lt;/code&gt;作用一致）&lt;/li&gt;
&lt;li&gt;特点：&lt;code&gt;Vue3&lt;/code&gt;中的&lt;code&gt;watch&lt;/code&gt;只能监视以下&lt;strong&gt;四种数据&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ref&lt;/code&gt;定义的数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reactive&lt;/code&gt;定义的数据。&lt;/li&gt;
&lt;li&gt;函数返回一个值（&lt;code&gt;getter&lt;/code&gt;函数）。&lt;/li&gt;
&lt;li&gt;一个包含上述内容的数组。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们在&lt;code&gt;Vue3&lt;/code&gt;中使用&lt;code&gt;watch&lt;/code&gt;的时候，通常会遇到以下几种情况：&lt;/p&gt;
&lt;h3&gt;情况一&lt;/h3&gt;
&lt;p&gt;监视&lt;code&gt;ref&lt;/code&gt;定义的【基本类型】数据：直接写数据名即可，监视的是其&lt;code&gt;value&lt;/code&gt;值的改变。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;情况一：监视【ref】定义的【基本类型】数据&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;{{ sum }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;add&quot;&amp;gt;点击++&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { ref, watch } from &quot;vue&quot;;

let sum = ref(0);

const add = () =&amp;gt; {
  sum.value++;
};

let stopWatch = watch(sum, (newValue, oldValue) =&amp;gt; {
  console.log(&quot;新&quot;, newValue);
  console.log(&quot;旧&quot;, oldValue);
  if (sum.value == 10) {
    stopWatch(); // 停止监听
  }
});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-03-17_20-42-09.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;情况二&lt;/h3&gt;
&lt;p&gt;监视&lt;code&gt;ref&lt;/code&gt;定义的【对象类型】数据：直接写数据名，监视的是对象的【地址值】，若想监视对象内部的数据，要手动开启深度监视。&lt;/p&gt;
&lt;p&gt;:::tip
注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若修改的是&lt;code&gt;ref&lt;/code&gt;定义的对象中的属性，&lt;code&gt;newValue&lt;/code&gt; 和 &lt;code&gt;oldValue&lt;/code&gt; 都是新值，因为它们是同一个对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若修改整个&lt;code&gt;ref&lt;/code&gt;定义的对象，&lt;code&gt;newValue&lt;/code&gt; 是新值， &lt;code&gt;oldValue&lt;/code&gt; 是旧值，因为不是同一个对象了。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;情况二：监视【ref】定义的【对象类型】数据&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.name }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.age }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeName&quot;&amp;gt;update Name&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeAge&quot;&amp;gt;update Age&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changePerson&quot;&amp;gt;修改整个&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { ref, watch } from &quot;vue&quot;;

let person = ref({
  name: &quot;张三&quot;,
  age: 30,
});

const changeName = () =&amp;gt; {
  person.value.name += &quot;~&quot;;
};
const changeAge = () =&amp;gt; {
  person.value.age++;
};

const changePerson = () =&amp;gt; {
  person.value = { name: &quot;Jack&quot;, age: 60 };
};

// 监视【ref】定义的【对象类型】数据，监视的是对象的地址值，若想监视对象内部属性的变化，需要手动开启深度监视
// watch的第一个参数是:被监视的数据
// watch的第二个参数是:监视的回调
// watch的第三个参数是:配置对象(deep、immediate等等)
watch(
  person,
  (newValue, oldValue) =&amp;gt; {
    console.log(&quot;新&quot;, newValue);
    console.log(&quot;旧&quot;, oldValue);
  },
  { deep: true, immediate: true }
);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;情况三&lt;/h3&gt;
&lt;p&gt;监视&lt;code&gt;reactive&lt;/code&gt;定义的【对象类型】数据，且默认开启了深度监视。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;深度监视不可关闭，且新值旧值都是新值，因为地址没变&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;情况三：监视【reactive】定义的【对象类型】数据&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.name }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.age }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeName&quot;&amp;gt;update Name&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeAge&quot;&amp;gt;update Age&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changePerson&quot;&amp;gt;修改整个&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { reactive, watch } from &quot;vue&quot;;

let person = reactive({
  name: &quot;张三&quot;,
  age: 30,
});

const changeName = () =&amp;gt; {
  person.name += &quot;~&quot;;
};
const changeAge = () =&amp;gt; {
  person.age++;
};

const changePerson = () =&amp;gt; {
  person = Object.assign(person, { name: &quot;Jack&quot;, age: 60 });
};

watch(person, (newValue, oldValue) =&amp;gt; {
  console.log(&quot;新&quot;, newValue);
  console.log(&quot;旧&quot;, oldValue);
});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;情况四&lt;/h3&gt;
&lt;p&gt;监视&lt;code&gt;ref&lt;/code&gt;或&lt;code&gt;reactive&lt;/code&gt;定义的【对象类型】数据中的&lt;strong&gt;某个属性&lt;/strong&gt;，注意点如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若该属性值&lt;strong&gt;不是&lt;/strong&gt;【对象类型】，需要写成函数形式。&lt;/li&gt;
&lt;li&gt;若该属性值是&lt;strong&gt;依然&lt;/strong&gt;是【对象类型】，可直接编，也可写成函数，建议写成函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;结论：监视的要是对象里的属性，那么最好写函数式，注意点：若是对象监视的是地址值，需要关注对象内部，需要手动开启深度监视。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.name }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.age }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.car.car1 + &quot;、&quot; + person.car.car2 }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeName&quot;&amp;gt;update Name&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeAge&quot;&amp;gt;update Age&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeCar1&quot;&amp;gt;update Car1&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeCar2&quot;&amp;gt;update Car2&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeCar&quot;&amp;gt;修改整个车&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { reactive, watch } from &quot;vue&quot;;

let person = reactive({
  name: &quot;张三&quot;,
  age: 30,
  car: {
    car1: &quot;宝马&quot;,
    car2: &quot;奔驰&quot;,
  },
});

const changeName = () =&amp;gt; {
  person.name += &quot;~&quot;;
};
const changeAge = () =&amp;gt; {
  person.age++;
};

const changeCar1 = () =&amp;gt; {
  person.car.car1 = &quot;奥迪&quot;;
};
const changeCar2 = () =&amp;gt; {
  person.car.car2 = &quot;大众&quot;;
};
const changeCar = () =&amp;gt; {
  person.car = { car1: &quot;雅迪&quot;, car2: &quot;台铃&quot; };
};

// 监视，情况四:监视响应式对象中的某个属性，且该属性时基本类型的，要写成函数式
watch(
  () =&amp;gt; person.name,
  (newValue, oldValue) =&amp;gt; {
    console.log(&quot;新&quot;, newValue);
    console.log(&quot;旧&quot;, oldValue);
  }
);

// 监视，情况四:监视响应式对象中的某个属性，且该属性是对象类型的，可以直接写，也能写函数，更推荐写函数
watch(
  () =&amp;gt; person.car,
  (newValue, oldValue) =&amp;gt; {
    console.log(&quot;car变化了&quot;, newValue, oldValue);
  },
  { deep: true }
);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;情况五&lt;/h3&gt;
&lt;p&gt;监视上述的多个数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.name }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.age }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;{{ person.car.car1 + &quot;、&quot; + person.car.car2 }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeName&quot;&amp;gt;update Name&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeAge&quot;&amp;gt;update Age&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeCar1&quot;&amp;gt;update Car1&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeCar2&quot;&amp;gt;update Car2&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeCar&quot;&amp;gt;修改整个车&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { reactive, watch } from &quot;vue&quot;;

let person = reactive({
  name: &quot;张三&quot;,
  age: 30,
  car: {
    car1: &quot;宝马&quot;,
    car2: &quot;奔驰&quot;,
  },
});

const changeName = () =&amp;gt; {
  person.name += &quot;~&quot;;
};
const changeAge = () =&amp;gt; {
  person.age++;
};

const changeCar1 = () =&amp;gt; {
  person.car.car1 = &quot;奥迪&quot;;
};
const changeCar2 = () =&amp;gt; {
  person.car.car2 = &quot;大众&quot;;
};
const changeCar = () =&amp;gt; {
  person.car = { car1: &quot;雅迪&quot;, car2: &quot;台铃&quot; };
};

// 监视，情况五:监视上述的多个数据
watch(
  [() =&amp;gt; person.name, () =&amp;gt; person.car.car1],
  (newValue, oldValue) =&amp;gt; {
    console.log(newValue, oldValue);
  },
  { deep: true }
);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;watchEffect&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;官网：立即运行一个函数，同时响应式地追踪其依赖，并在依赖更改时重新执行该函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;watch&lt;/code&gt;对比&lt;code&gt;watchEffect&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;都能监听响应式数据的变化，不同的是监听数据变化的方式不同&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;watch&lt;/code&gt;：要明确指出监视的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;watchEffect&lt;/code&gt;：不用明确指出监视的数据（函数中用到哪些属性，那就监视哪些属性）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;需求:当水温达到60度，或水位达到80cm时，给服务器发请求&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;水温{{ temp }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;水位{{ height }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeTemp&quot;&amp;gt;改水温&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;changeHeight&quot;&amp;gt;改高度&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Person&quot;&amp;gt;
import { ref, watch, watchEffect } from &quot;vue&quot;;

let temp = ref(10);
let height = ref(0);

const changeTemp = () =&amp;gt; {
  temp.value += 10;
};
const changeHeight = () =&amp;gt; {
  height.value += 10;
};

// watch([temp, height], (newValue) =&amp;gt; {
//   // console.log(newValue, oldValue);
//   const [newTemp, newHeight] = newValue;
//   if (newTemp &amp;gt;= 60 || newHeight &amp;gt;= 80) {
//     console.log(&quot;call server&quot;);
//   }
// });

const stopWatch = watchEffect(() =&amp;gt; {
  if (temp.value &amp;gt;= 60 || height.value &amp;gt;= 80) {
    console.log(&quot;call server&quot;);
  }
  if (temp.value === 100) {
    stopWatch(); // 取消监视
  }
});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;标签的 ref 属性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用在普通&lt;code&gt;DOM&lt;/code&gt;标签上，获取的是&lt;code&gt;DOM&lt;/code&gt;节点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用在组件标签上，获取的是组件实例对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ref 放在标签上&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;拿的是&lt;strong&gt;DOM 元素&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;p&gt;这样写有问题，引用组件情况下，与其他页面的 id 重复了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3 id=&quot;title&quot;&amp;gt;亚洲&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;中国&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;showLog&quot;&amp;gt;show log&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
defineOptions({
  name: &quot;Person&quot;,
});
const showLog = () =&amp;gt; {
  console.log(document.getElementById(&quot;title&quot;));
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h2 id=&quot;title&quot;&amp;gt;你好&amp;lt;/h2&amp;gt;
  &amp;lt;Person /&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import Person from &quot;./components/Person.vue&quot;;

defineOptions({
  name: &quot;App&quot;,
});
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-03-18_20-29-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样才是对的 ⬇️&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3 ref=&quot;title&quot;&amp;gt;亚洲&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;中国&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;showLog&quot;&amp;gt;show log&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &quot;vue&quot;;

defineOptions({
  name: &quot;Person&quot;,
});

let title = ref();
const showLog = () =&amp;gt; {
  console.log(title.value);
};
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.person:first-child {
  font-size: 12px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-03-18_20-45-08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::info
&lt;img src=&quot;../../assets/img/Snipaste_2025-03-18_20-40-21.png&quot; alt=&quot;&quot; /&gt;
&lt;code&gt;data-v-xxxx&lt;/code&gt; 这个标记，是因为样式中有了&lt;code&gt;scoped&lt;/code&gt;
:::&lt;/p&gt;
&lt;h3&gt;ref 放在组件身上&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;拿的是组件&lt;strong&gt;实例对象&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;子传父&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3 ref=&quot;title&quot;&amp;gt;亚洲&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;中国&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;showLog&quot;&amp;gt;show log&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, defineExpose } from &quot;vue&quot;;

defineOptions({
  name: &quot;Person&quot;,
});

let title = ref();

let num1 = ref(0);
let num2 = ref(1);
let num3 = ref(2);
const showLog = () =&amp;gt; {
  console.log(title.value);
};

// 使用defineExpose，父组件才能拿到num1, num2, num3
// 使用defineExpose将组件中的数据交给外部
defineExpose({ num1, num2, num3 });
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.person:first-child {
  font-size: 12px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h2 id=&quot;title&quot;&amp;gt;你好&amp;lt;/h2&amp;gt;
  &amp;lt;Person ref=&quot;child&quot; /&amp;gt;
  &amp;lt;h2&amp;gt;&amp;lt;button @click=&quot;showLog&quot;&amp;gt;点击展示Person数据&amp;lt;/button&amp;gt;&amp;lt;/h2&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &quot;vue&quot;;
import Person from &quot;./components/Person.vue&quot;;

defineOptions({
  name: &quot;App&quot;,
});

const child = ref();

const showLog = () =&amp;gt; {
  console.log(child.value);
  console.log(child.value.num1);
  console.log(child.value.num2);
  console.log(child.value.num3);
};
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-03-18_21-04-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;defineExpose&lt;/h3&gt;
&lt;p&gt;:::tip
&lt;code&gt;defineExpose&lt;/code&gt;用于明确地暴露组件的属性或方法给父组件。默认情况下，使用 &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; 定义的组件是完全封闭的，即其内部的状态和方法对外部（父组件）是不可见的。如果你希望某些状态或方法能够被父组件访问，就需要使用 defineExpose。
:::&lt;/p&gt;
&lt;h2&gt;ts 接口、泛型、自定义类型&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个接口，用于限制person对象的具体属性

// 暴露方式： 1.默认暴露 2.分别暴露 3.统一暴露
export interface PersonInter {
  id: string;
  name: string;
  age: number;
}

// 自定义类型
export type Persons = Array&amp;lt;PersonInter&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
// 如果引入的是值，那么就不用type; 但PersonInter是一种约束，就需要标记它是一个类型
import { type PersonInter, type Persons } from &quot;@/types&quot;;

defineOptions({
  name: &quot;Person&quot;,
});

let person: PersonInter = { id: &quot;xxxx&quot;, name: &quot;Jack&quot;, age: 30 };

let personList: Persons = [
  { id: &quot;xxxx1&quot;, name: &quot;Jack&quot;, age: 30 },
  { id: &quot;xxxx2&quot;, name: &quot;Marry&quot;, age: 23 },
  { id: &quot;xxxx3&quot;, name: &quot;Lily&quot;, age: 22 },
];
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.person:first-child {
  font-size: 12px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;props&lt;/h2&gt;
&lt;p&gt;&amp;lt;span class=&quot;marker-evy&quot;&amp;gt;&lt;strong&gt;父传子&lt;/strong&gt; 简单 demo&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;p&gt;可理解为 父传给子一个&lt;code&gt;a&lt;/code&gt;，值是&lt;code&gt;你好&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;Person a=&quot;你好&quot; b=&quot;哈哈&quot; /&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import Person from &quot;./components/Person.vue&quot;;

defineOptions({
  name: &quot;App&quot;,
});
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h2&amp;gt;{{ a }}&amp;lt;/h2&amp;gt;
    &amp;lt;h2&amp;gt;{{ b }}&amp;lt;/h2&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { defineProps } from &quot;vue&quot;;

defineOptions({
  name: &quot;Person&quot;,
});

let x = defineProps([&quot;a&quot;, &quot;b&quot;]);
console.log(x);
console.log(x.a);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-03-18_21-49-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;简单测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-03-18_21-56-17.png&quot; alt=&quot;&quot; /&gt;
它的值是,被解析为：
&lt;img src=&quot;../../assets/img/Snipaste_2025-03-18_21-57-27.png&quot; alt=&quot;&quot; /&gt;
特例就是，ref 不需要加冒号&lt;code&gt;:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;完整示例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;Person :list=&quot;personList&quot; /&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive } from &quot;vue&quot;;
import Person from &quot;./components/Person.vue&quot;;
import type { Persons } from &quot;./types&quot;;

defineOptions({
  name: &quot;App&quot;,
});

let personList = reactive&amp;lt;Persons&amp;gt;([
  { id: &quot;qwerasdf1&quot;, name: &quot;Lily&quot;, age: 20 },
  { id: &quot;qwerasdf2&quot;, name: &quot;Jack&quot;, age: 34 },
  { id: &quot;qwerasdf3&quot;, name: &quot;Mary&quot;, age: 45 },
]);
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;ul&amp;gt;
      &amp;lt;li v-for=&quot;item in list&quot; :key=&quot;item.id&quot;&amp;gt;
        {{ item.name }}-{{ item.age }}
      &amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import type { Persons } from &quot;@/types&quot;;
import { withDefaults } from &quot;vue&quot;;

defineOptions({
  name: &quot;Person&quot;,
});

// 只接收list
let x = defineProps([&quot;list&quot;]);

// 接收list+限制类型
defineProps&amp;lt;{ list: Persons }&amp;gt;();

// 接收list + 限制类型 + 限制必要性(可传，可不传) + 指定默认值
withDefaults(defineProps&amp;lt;{ list?: Persons }&amp;gt;(), {
  list: () =&amp;gt; [{ id: &quot;0001&quot;, name: &quot;康师傅&quot;, age: 34 }],
});
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;defineProps&lt;/h3&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 vue3 中，defineXXX 是宏函数，宏函数不用引入可直接使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;defineProps&lt;/code&gt;是 Vue 3 提供的一个编译时宏（compile-time macro）用于在 &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; 中声明和获取 props。当你需要从父组件向子组件传递数据时，你可以在子组件中使用 &lt;code&gt;defineProps&lt;/code&gt; 来接收这些数据。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;生命周期&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;概念：&lt;code&gt;Vue&lt;/code&gt;组件实例在创建时要经历一系列的初始化步骤，在此过程中&lt;code&gt;Vue&lt;/code&gt;会在合适的时机，调用特定的函数，从而让开发者有机会在特定阶段运行自己的代码，这些特定的函数统称为：生命周期钩子&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;规律：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;生命周期整体分为四个阶段，分别是：&lt;strong&gt;创建、挂载、更新、销毁&lt;/strong&gt;，每个阶段都有两个钩子，一前一后。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Vue2&lt;/code&gt;的生命周期&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;创建阶段：&lt;code&gt;beforeCreate&lt;/code&gt;、&lt;code&gt;created&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;挂载阶段：&lt;code&gt;beforeMount&lt;/code&gt;、&lt;code&gt;mounted&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;更新阶段：&lt;code&gt;beforeUpdate&lt;/code&gt;、&lt;code&gt;updated&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;销毁阶段：&lt;code&gt;beforeDestroy&lt;/code&gt;、&lt;code&gt;destroyed&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Vue3&lt;/code&gt;的生命周期&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;创建阶段：&lt;code&gt;setup&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;挂载阶段：&lt;code&gt;onBeforeMount&lt;/code&gt;、&lt;code&gt;onMounted&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;更新阶段：&lt;code&gt;onBeforeUpdate&lt;/code&gt;、&lt;code&gt;onUpdated&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;卸载阶段：&lt;code&gt;onBeforeUnmount&lt;/code&gt;、&lt;code&gt;onUnmounted&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用的钩子：&lt;code&gt;onMounted&lt;/code&gt;(挂载完毕)、&lt;code&gt;onUpdated&lt;/code&gt;(更新完毕)、&lt;code&gt;onBeforeUnmount&lt;/code&gt;(卸载之前)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;又称生命周期、生命周期函数、生命周期钩子&lt;/p&gt;
&lt;p&gt;组件的生命周期&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;vue2 生命周期：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;创建
挂载
更新
销毁&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-03-19_21-47-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;vue3 生命周期&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-03-19_21-56-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;span class=&quot;marker-evy&quot;&amp;gt;demo如下&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;当前sum：{{ sum }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;sumAdd&quot;&amp;gt;点击++&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from &quot;vue&quot;;

defineOptions({
  name: &quot;Person&quot;,
});

let sum = ref(0);

const sumAdd = () =&amp;gt; {
  sum.value += 1;
};

console.log(&quot;创建&quot;);

onBeforeMount(() =&amp;gt; {
  console.log(&quot;挂载前&quot;);
});

onMounted(() =&amp;gt; {
  console.log(&quot;挂载完毕&quot;);
});

onBeforeUpdate(() =&amp;gt; {
  console.log(&quot;更新前&quot;);
});

onUpdated(() =&amp;gt; {
  console.log(&quot;更新完毕&quot;);
});

onBeforeUnmount(() =&amp;gt; {
  console.log(&quot;卸载前&quot;);
});
onUnmounted(() =&amp;gt; {
  console.log(&quot;卸载完毕&quot;);
});
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;Person v-if=&quot;isShow&quot; /&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &quot;vue&quot;;
import Person from &quot;./components/Person.vue&quot;;

defineOptions({
  name: &quot;App&quot;,
});

let isShow = ref(true);
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自定义 hook&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;什么是&lt;code&gt;hook&lt;/code&gt;？—— 本质是一个函数，把&lt;code&gt;setup&lt;/code&gt;函数中使用的&lt;code&gt;Composition API&lt;/code&gt;进行了封装，类似于&lt;code&gt;vue2.x&lt;/code&gt;中的&lt;code&gt;mixin&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自定义&lt;code&gt;hook&lt;/code&gt;的优势：复用代码, 让&lt;code&gt;setup&lt;/code&gt;中的逻辑更清楚易懂。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如下代码，功能齐全但是，数据与方法比较混乱，可以使用 hook 进行改造&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;当前sum：{{ sum }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;sumAdd&quot;&amp;gt;点击++&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;hr /&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;img v-for=&quot;(dog, index) in dogList&quot; :src=&quot;dog&quot; :key=&quot;index&quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;&amp;lt;button @click=&quot;addDog&quot;&amp;gt;来一只狗&amp;lt;/button&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive, ref } from &quot;vue&quot;;
import axios from &quot;axios&quot;;

defineOptions({
  name: &quot;Person&quot;,
});

let sum = ref(0);

const sumAdd = () =&amp;gt; {
  sum.value += 1;
};

let dogList = reactive([
  &quot;https://images.dog.ceo/breeds/pembroke/n02113023_7316.jpg&quot;,
]);

const addDog = async () =&amp;gt; {
  try {
    let result = await axios.get(
      &quot;https://dog.ceo/api/breed/pembroke/images/random&quot;
    );
    dogList.push(result.data.message);
    console.log(result.data.message);
  } catch (error) {
    alert(error);
  }
};
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.person {
  width: 500px;
  height: 300px;
}
img {
  height: 100px;
  margin-right: 10px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 hooks 文件夹，将每个不同模块进行拆分，命名必须使用&lt;code&gt;useXXX&lt;/code&gt;这种形式&lt;/p&gt;
&lt;p&gt;改进过后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;person&quot;&amp;gt;
    &amp;lt;h3&amp;gt;当前sum：{{ sum }}&amp;lt;/h3&amp;gt;
    &amp;lt;h3&amp;gt;&amp;lt;button @click=&quot;sumAdd&quot;&amp;gt;点击++&amp;lt;/button&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;hr /&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;img v-for=&quot;(dog, index) in dogList&quot; :src=&quot;dog&quot; :key=&quot;index&quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;&amp;lt;button @click=&quot;addDog&quot;&amp;gt;来一只狗&amp;lt;/button&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
defineOptions({
  name: &quot;Person&quot;,
});
import useDog from &quot;@/hooks/useDog&quot;;
import useSum from &quot;@/hooks/useSum&quot;;

const { dogList, addDog } = useDog();
const { sum, sumAdd } = useSum();
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.person {
  width: 500px;
  height: 300px;
}
img {
  height: 100px;
  margin-right: 10px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { reactive, onMounted } from &quot;vue&quot;;
import axios from &quot;axios&quot;;
export default () =&amp;gt; {
  // 钩子函数也是不影响使用的
  onMounted(() =&amp;gt; {
    addDog();
  });

  let dogList = reactive([
    &quot;https://images.dog.ceo/breeds/pembroke/n02113023_7316.jpg&quot;,
  ]);

  const addDog = async () =&amp;gt; {
    try {
      let result = await axios.get(
        &quot;https://dog.ceo/api/breed/pembroke/images/random&quot;
      );
      dogList.push(result.data.message);
      console.log(result.data.message);
    } catch (error) {
      alert(error);
    }
  };
  return { dogList, addDog };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { ref } from &quot;vue&quot;;
export default () =&amp;gt; {
  let sum = ref(0);

  const sumAdd = () =&amp;gt; {
    sum.value += 1;
  };
  return { sum, sumAdd };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/189983606304eryteryt.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;路由&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-03-20_22-52-29.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;路由安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i vue-router
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基本切换&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;src&lt;/code&gt;下创建&lt;code&gt;router&lt;/code&gt;文件夹，创建&lt;code&gt;index.ts&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建路由器，暴露出去

// 1.引入crateRouter
import { createRouter, createWebHashHistory } from &quot;vue-router&quot;;

// 2.创建路由器
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: &quot;/home&quot;,
      name: &quot;home&quot;,
      component: () =&amp;gt; import(&quot;@/views/Home.vue&quot;),
    },
    {
      path: &quot;/about&quot;,
      name: &quot;about&quot;,
      component: () =&amp;gt; import(&quot;../views/About.vue&quot;),
    },
    {
      path: &quot;/news&quot;,
      name: &quot;news&quot;,
      component: () =&amp;gt; import(&quot;../views/News.vue&quot;),
    },
  ],
});

export default router;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import &quot;./assets/main.css&quot;;

import { createApp } from &quot;vue&quot;;
import App from &quot;./App.vue&quot;;
// 引入路由器
import router from &quot;./router&quot;;

// 创建一个应用
const app = createApp(App);
// 使用路由器
app.use(router);
// 挂载整个应用到app容器中
app.mount(&quot;#app&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;路由 示例&amp;lt;/h1&amp;gt;
    &amp;lt;div class=&quot;navigate&quot;&amp;gt;
      &amp;lt;RouterLink to=&quot;/home&quot; active-class=&quot;highlight&quot;&amp;gt;首页&amp;lt;/RouterLink&amp;gt;
      &amp;lt;RouterLink to=&quot;/news&quot; active-class=&quot;highlight&quot;&amp;gt;新闻&amp;lt;/RouterLink&amp;gt;
      &amp;lt;RouterLink to=&quot;/about&quot; active-class=&quot;highlight&quot;&amp;gt;关于&amp;lt;/RouterLink&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;main-content&quot;&amp;gt;
      &amp;lt;RouterView&amp;gt;&amp;lt;/RouterView&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { RouterView, RouterLink } from &quot;vue-router&quot;;

defineOptions({
  name: &quot;App&quot;,
});
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;
.highlight {
  color: aqua;
  background-color: rgb(253, 238, 219);
}
.navigate {
  width: 100%;
  height: 100px;
}
.navigate a {
  margin-left: 100px;
  display: block;
  float: left;
  border: 1px solid black;
  padding: 10px;
  border-radius: 10px;
  text-decoration: none;
}

.main-content {
  border: 1px solid rgb(177, 125, 211);
  overflow: hidden;
  height: 350px;
  border-radius: 20px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/189983604509820394.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;注意点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;路由组件通常存放在&lt;code&gt;pages&lt;/code&gt; 或 &lt;code&gt;views&lt;/code&gt;文件夹，一般组件通常存放在&lt;code&gt;components&lt;/code&gt;文件夹。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过点击导航，视觉效果上“消失” 了的路由组件，默认是被&lt;strong&gt;卸载&lt;/strong&gt;掉的，需要的时候再去&lt;strong&gt;挂载&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;路由组件
靠路由规则渲染出来的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一般组件
亲手写标签出来的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-03-24_20-57-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;路由器工作模式&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;history&lt;/code&gt;模式&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;优点：&lt;code&gt;URL&lt;/code&gt;更加美观，不带有&lt;code&gt;#&lt;/code&gt;，更接近传统的网站&lt;code&gt;URL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;缺点：后期项目上线，需要服务端配合处理路径问题，否则刷新会有&lt;code&gt;404&lt;/code&gt;错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const router = createRouter({
  history: createWebHistory(), //history模式
  /******/
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;hash&lt;/code&gt;模式&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;优点：兼容性更好，因为不需要服务器端处理路径。&lt;/p&gt;
&lt;p&gt;缺点：&lt;code&gt;URL&lt;/code&gt;带有&lt;code&gt;#&lt;/code&gt;不太美观，且在&lt;code&gt;SEO&lt;/code&gt;优化方面相对较差。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const router = createRouter({
  history: createWebHashHistory(), //hash模式
  /******/
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;to 的两种写法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 第一种：to的字符串写法 --&amp;gt;
&amp;lt;router-link active-class=&quot;active&quot; to=&quot;/home&quot;&amp;gt;主页&amp;lt;/router-link&amp;gt;

&amp;lt;!-- 第二种：to的对象写法 --&amp;gt;
&amp;lt;router-link active-class=&quot;active&quot; :to=&quot;{ path: &apos;/home&apos; }&quot;&amp;gt;Home&amp;lt;/router-link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;命名路由&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 创建路由器，暴露出去

// 1.引入crateRouter
import { createRouter, createWebHashHistory } from &quot;vue-router&quot;;
import About from &quot;@/views/About.vue&quot;;
import Home from &quot;@/views/Home.vue&quot;;
import News from &quot;@/views/News.vue&quot;;

// 2.创建路由器
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: &quot;/home&quot;,
      component: () =&amp;gt; Home,
    },
    {
      path: &quot;/about&quot;,
      component: () =&amp;gt; About,
    },
    {
      name: &quot;xinwen&quot;,
      path: &quot;/news&quot;,
      component: () =&amp;gt; News,
    },
  ],
});

export default router;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;RouterLink :to=&quot;{ name: &apos;xinwen&apos; }&quot; active-class=&quot;highlight&quot;&amp;gt;新闻&amp;lt;/RouterLink&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;嵌套路由&lt;/h3&gt;
&lt;p&gt;编写子组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h3&amp;gt;编号：xxx&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;标题：xxx&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;详情：xxx&amp;lt;/h3&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置路由规则，使用&lt;code&gt;children&lt;/code&gt;配置项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建路由器，暴露出去

// 1.引入crateRouter
import { createRouter, createWebHashHistory } from &quot;vue-router&quot;;

// 2.创建路由器
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: &quot;/home&quot;,
      name: &quot;home&quot;,
      component: () =&amp;gt; import(&quot;@/views/Home.vue&quot;),
    },
    {
      path: &quot;/about&quot;,
      name: &quot;about&quot;,
      component: () =&amp;gt; import(&quot;@/views/About.vue&quot;),
    },
    {
      path: &quot;/news&quot;,
      name: &quot;news&quot;,
      component: () =&amp;gt; import(&quot;@/views/News.vue&quot;),
      children: [
        {
          path: &quot;detail&quot;, // 子路由不用写 ‘/’
          name: &quot;detail&quot;,
          component: () =&amp;gt; import(&quot;@/components/Detail.vue&quot;),
        },
      ],
    },
  ],
});

export default router;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跳转路由（记得要加完整路径）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;newsList&quot;&amp;gt;
    &amp;lt;RouterLink :to=&quot;{ path: &apos;/news/detail&apos; }&quot; v-for=&quot;news in newsList&quot;&amp;gt;{{
      news.title
    }}&amp;lt;/RouterLink&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;news-detail&quot;&amp;gt;
    &amp;lt;RouterView&amp;gt;&amp;lt;/RouterView&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive } from &quot;vue&quot;;

const newsList = reactive([
  {
    id: &quot;1904165055277039616&quot;,
    title: &quot;海关总署就芬太尼相关问题答记者问&quot;,
    content:
      &quot;海关总署有关负责人：《麻醉药品品种目录》（2013年版）里面列管的阿芬太尼、芬太尼、瑞芬太尼、舒芬太尼等具有药品属性的芬太尼按照麻醉药品管理。&quot;,
  },
  {
    id: &quot;1904165055281233920&quot;,
    title: &quot;NASA“撤回”登月宇航员“多元化”承诺&quot;,
    content:
      &quot;今日俄罗斯电视台网站3月23日报道，美国国家航空航天局（NASA）收回其公开承诺，即在阿耳忒弥斯登月计划下，将第一位女性和第一位有色人种送上月球。&quot;,
  },
  {
    id: &quot;1904165055281233921&quot;,
    title: &quot;春天是最懂氛围感的&quot;,
    content:
      &quot;春暖花开，春意融融。近日，北京各大公园里的春天已藏不住了，当古建与春天相遇，一场跨越时空的浪漫邂逅开启。人勤春早，收藏这组壁纸，新的一周，一起与美好同行&quot;,
  },
]);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.newsList {
  margin-left: 30px;
  float: left;
  width: 400px;
}
.newsList a {
  margin-right: 20px;
  display: block;
  margin-bottom: 20px;
}
.news-detail {
  width: 500px;
  height: 300px;
  border: 2px solid rgb(232, 193, 193);
  float: left;
  border-radius: 20px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记得去&lt;code&gt;该&lt;/code&gt;组件中预留一个&lt;code&gt;&amp;lt;router-view&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;路由传参-query 参数&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;第一种方式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;newsList&quot;&amp;gt;
    &amp;lt;RouterLink
      :to=&quot;`/news/detail?id=${news.id}&amp;amp;title=${news.title}&amp;amp;content=${news.content}`&quot;
      v-for=&quot;news in newsList&quot;
      &amp;gt;{{ news.title }}&amp;lt;/RouterLink
    &amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;news-detail&quot;&amp;gt;
    &amp;lt;RouterView&amp;gt;&amp;lt;/RouterView&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive } from &quot;vue&quot;;

const newsList = reactive([
  {
    id: &quot;1904165055277039616&quot;,
    title: &quot;海关总署就芬太尼相关问题答记者问&quot;,
    content:
      &quot;海关总署有关负责人：《麻醉药品品种目录》（2013年版）里面列管的阿芬太尼、芬太尼、瑞芬太尼、舒芬太尼等具有药品属性的芬太尼按照麻醉药品管理。&quot;,
  },
  {
    id: &quot;1904165055281233920&quot;,
    title: &quot;NASA“撤回”登月宇航员“多元化”承诺&quot;,
    content:
      &quot;今日俄罗斯电视台网站3月23日报道，美国国家航空航天局（NASA）收回其公开承诺，即在阿耳忒弥斯登月计划下，将第一位女性和第一位有色人种送上月球。&quot;,
  },
  {
    id: &quot;1904165055281233921&quot;,
    title: &quot;春天是最懂氛围感的&quot;,
    content:
      &quot;春暖花开，春意融融。近日，北京各大公园里的春天已藏不住了，当古建与春天相遇，一场跨越时空的浪漫邂逅开启。人勤春早，收藏这组壁纸，新的一周，一起与美好同行&quot;,
  },
]);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.newsList {
  margin-left: 30px;
  float: left;
  width: 400px;
}
.newsList a {
  margin-right: 20px;
  display: block;
  margin-bottom: 20px;
}
.news-detail {
  width: 500px;
  height: 300px;
  border: 2px solid rgb(232, 193, 193);
  float: left;
  border-radius: 20px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h3&amp;gt;编号：{{ query.id }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;标题：{{ query.title }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;详情：{{ query.content }}&amp;lt;/h3&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { toRefs } from &quot;vue&quot;;
import { useRoute } from &quot;vue-router&quot;;
defineOptions({
  name: &quot;Detail&quot;,
});

let route = useRoute();
let { query } = toRefs(route);
console.log(&quot;route&quot;, route);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二种方式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;newsList&quot;&amp;gt;
    &amp;lt;RouterLink
      :to=&quot;{
        path: &apos;/news/detail&apos;,
        query: {
          id: news.id,
          content: news.content,
          title: news.title,
        },
      }&quot;
      v-for=&quot;news in newsList&quot;
      &amp;gt;{{ news.title }}&amp;lt;/RouterLink
    &amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;news-detail&quot;&amp;gt;
    &amp;lt;RouterView&amp;gt;&amp;lt;/RouterView&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive } from &quot;vue&quot;;

const newsList = reactive([
  {
    id: &quot;1904165055277039616&quot;,
    title: &quot;海关总署就芬太尼相关问题答记者问&quot;,
    content:
      &quot;海关总署有关负责人：《麻醉药品品种目录》（2013年版）里面列管的阿芬太尼、芬太尼、瑞芬太尼、舒芬太尼等具有药品属性的芬太尼按照麻醉药品管理。&quot;,
  },
  {
    id: &quot;1904165055281233920&quot;,
    title: &quot;NASA“撤回”登月宇航员“多元化”承诺&quot;,
    content:
      &quot;今日俄罗斯电视台网站3月23日报道，美国国家航空航天局（NASA）收回其公开承诺，即在阿耳忒弥斯登月计划下，将第一位女性和第一位有色人种送上月球。&quot;,
  },
  {
    id: &quot;1904165055281233921&quot;,
    title: &quot;春天是最懂氛围感的&quot;,
    content:
      &quot;春暖花开，春意融融。近日，北京各大公园里的春天已藏不住了，当古建与春天相遇，一场跨越时空的浪漫邂逅开启。人勤春早，收藏这组壁纸，新的一周，一起与美好同行&quot;,
  },
]);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.newsList {
  margin-left: 30px;
  float: left;
  width: 400px;
}
.newsList a {
  margin-right: 20px;
  display: block;
  margin-bottom: 20px;
}
.news-detail {
  width: 500px;
  height: 300px;
  border: 2px solid rgb(232, 193, 193);
  float: left;
  border-radius: 20px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h3&amp;gt;编号：{{ query.id }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;标题：{{ query.title }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;详情：{{ query.content }}&amp;lt;/h3&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { toRefs } from &quot;vue&quot;;
import { useRoute } from &quot;vue-router&quot;;
defineOptions({
  name: &quot;Detail&quot;,
});

let route = useRoute();
let { query } = toRefs(route);
console.log(&quot;route&quot;, route);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/189983604512356334fdd.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;params 参数&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;第一种方式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;newsList&quot;&amp;gt;
    &amp;lt;RouterLink
      v-for=&quot;news in newsList&quot;
      :to=&quot;`/news/detail/${news.id}/${news.title}/${news.content}`&quot;
      &amp;gt;{{ news.title }}&amp;lt;/RouterLink
    &amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;news-detail&quot;&amp;gt;
    &amp;lt;RouterView&amp;gt;&amp;lt;/RouterView&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive } from &quot;vue&quot;;

const newsList = reactive([
  {
    id: &quot;1904165055277039616&quot;,
    title: &quot;海关总署就芬太尼相关问题答记者问&quot;,
    content:
      &quot;海关总署有关负责人：《麻醉药品品种目录》（2013年版）里面列管的阿芬太尼、芬太尼、瑞芬太尼、舒芬太尼等具有药品属性的芬太尼按照麻醉药品管理。&quot;,
  },
  {
    id: &quot;1904165055281233920&quot;,
    title: &quot;NASA“撤回”登月宇航员“多元化”承诺&quot;,
    content:
      &quot;今日俄罗斯电视台网站3月23日报道，美国国家航空航天局（NASA）收回其公开承诺，即在阿耳忒弥斯登月计划下，将第一位女性和第一位有色人种送上月球。&quot;,
  },
  {
    id: &quot;1904165055281233921&quot;,
    title: &quot;春天是最懂氛围感的&quot;,
    content:
      &quot;春暖花开，春意融融。近日，北京各大公园里的春天已藏不住了，当古建与春天相遇，一场跨越时空的浪漫邂逅开启。人勤春早，收藏这组壁纸，新的一周，一起与美好同行&quot;,
  },
]);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.newsList {
  margin-left: 30px;
  float: left;
  width: 400px;
}
.newsList a {
  margin-right: 20px;
  display: block;
  margin-bottom: 20px;
}
.news-detail {
  width: 500px;
  height: 300px;
  border: 2px solid rgb(232, 193, 193);
  float: left;
  border-radius: 20px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/Snipaste_2025-03-25_22-22-53.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建路由器，暴露出去

// 1.引入crateRouter
import { createRouter, createWebHashHistory } from &quot;vue-router&quot;;

// 2.创建路由器
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: &quot;/home&quot;,
      name: &quot;home&quot;,
      component: () =&amp;gt; import(&quot;@/views/Home.vue&quot;),
    },
    {
      path: &quot;/about&quot;,
      name: &quot;about&quot;,
      component: () =&amp;gt; import(&quot;@/views/About.vue&quot;),
    },
    {
      path: &quot;/news&quot;,
      name: &quot;news&quot;,
      component: () =&amp;gt; import(&quot;@/views/News.vue&quot;),
      children: [
        {
          path: &quot;detail/:id/:title/:content?&quot;, // 加? 表示可传可不传
          name: &quot;detail&quot;,
          component: () =&amp;gt; import(&quot;@/components/Detail.vue&quot;),
        },
      ],
    },
  ],
});

export default router;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h3&amp;gt;编号：{{ params.id }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;标题：{{ params.title }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;详情：{{ params.content }}&amp;lt;/h3&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { toRefs } from &quot;vue&quot;;
import { useRoute } from &quot;vue-router&quot;;
defineOptions({
  name: &quot;Detail&quot;,
});

let route = useRoute();
let { params } = toRefs(route);
console.log(&quot;route&quot;, route);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二种方式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;newsList&quot;&amp;gt;
    &amp;lt;RouterLink
      v-for=&quot;news in newsList&quot;
      :to=&quot;{
        name: &apos;detail&apos;,
        params: {
          id: news.id,
          title: news.title,
          content: news.content,
        },
      }&quot;
      &amp;gt;{{ news.title }}&amp;lt;/RouterLink
    &amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;news-detail&quot;&amp;gt;
    &amp;lt;RouterView&amp;gt;&amp;lt;/RouterView&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive } from &quot;vue&quot;;

const newsList = reactive([
  {
    id: &quot;1904165055277039616&quot;,
    title: &quot;海关总署就芬太尼相关问题答记者问&quot;,
    content:
      &quot;海关总署有关负责人：《麻醉药品品种目录》（2013年版）里面列管的阿芬太尼、芬太尼、瑞芬太尼、舒芬太尼等具有药品属性的芬太尼按照麻醉药品管理。&quot;,
  },
  {
    id: &quot;1904165055281233920&quot;,
    title: &quot;NASA“撤回”登月宇航员“多元化”承诺&quot;,
    content:
      &quot;今日俄罗斯电视台网站3月23日报道，美国国家航空航天局（NASA）收回其公开承诺，即在阿耳忒弥斯登月计划下，将第一位女性和第一位有色人种送上月球。&quot;,
  },
  {
    id: &quot;1904165055281233921&quot;,
    title: &quot;春天是最懂氛围感的&quot;,
    content:
      &quot;春暖花开，春意融融。近日，北京各大公园里的春天已藏不住了，当古建与春天相遇，一场跨越时空的浪漫邂逅开启。人勤春早，收藏这组壁纸，新的一周，一起与美好同行&quot;,
  },
]);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.newsList {
  margin-left: 30px;
  float: left;
  width: 400px;
}
.newsList a {
  margin-right: 20px;
  display: block;
  margin-bottom: 20px;
}
.news-detail {
  width: 500px;
  height: 300px;
  border: 2px solid rgb(232, 193, 193);
  float: left;
  border-radius: 20px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果路由中有占位，但传参时没有传会报错，可以通过在路由中这样写：&lt;code&gt;path:&apos;detail/:id/:title/:content?&apos;&lt;/code&gt; 添加&lt;code&gt;?&lt;/code&gt;即可,配置参数必要性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传递&lt;code&gt;params&lt;/code&gt;参数时，若使用&lt;code&gt;to&lt;/code&gt;的对象写法，必须使用&lt;code&gt;name&lt;/code&gt;配置项，不能用&lt;code&gt;path&lt;/code&gt;，并且该写法不能传递数组和对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传递&lt;code&gt;params&lt;/code&gt;参数时，需要提前在规则中占位。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;路由 props 配置&lt;/h3&gt;
&lt;p&gt;第一种写法:将路由收到的所有 params 参数作为 props 传给路由组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建路由器，暴露出去

// 1.引入crateRouter
import { createRouter, createWebHashHistory } from &quot;vue-router&quot;;

// 2.创建路由器
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: &quot;/home&quot;,
      name: &quot;home&quot;,
      component: () =&amp;gt; import(&quot;@/views/Home.vue&quot;),
    },
    {
      path: &quot;/about&quot;,
      name: &quot;about&quot;,
      component: () =&amp;gt; import(&quot;@/views/About.vue&quot;),
    },
    {
      path: &quot;/news&quot;,
      name: &quot;news&quot;,
      component: () =&amp;gt; import(&quot;@/views/News.vue&quot;),
      children: [
        {
          path: &quot;detail/:id/:title/:content?&quot;,
          name: &quot;detail&quot;,
          component: () =&amp;gt; import(&quot;@/components/Detail.vue&quot;),
          props: true,
        },
      ],
    },
  ],
});

export default router;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h3&amp;gt;编号：{{ id }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;标题：{{ title }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;详情：{{ content }}&amp;lt;/h3&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { toRefs } from &quot;vue&quot;;
import { useRoute } from &quot;vue-router&quot;;
defineOptions({
  name: &quot;Detail&quot;,
});

defineProps([&quot;id&quot;, &quot;title&quot;, &quot;content&quot;]);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二种写法&lt;/strong&gt;:函数写法，可以自己决定将什么作为 props 给路由组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建路由器，暴露出去

// 1.引入crateRouter
import { createRouter, createWebHashHistory } from &quot;vue-router&quot;;

// 2.创建路由器
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: &quot;/home&quot;,
      name: &quot;home&quot;,
      component: () =&amp;gt; import(&quot;@/views/Home.vue&quot;),
    },
    {
      path: &quot;/about&quot;,
      name: &quot;about&quot;,
      component: () =&amp;gt; import(&quot;@/views/About.vue&quot;),
    },
    {
      path: &quot;/news&quot;,
      name: &quot;news&quot;,
      component: () =&amp;gt; import(&quot;@/views/News.vue&quot;),
      children: [
        {
          path: &quot;detail&quot;,
          name: &quot;detail&quot;,
          component: () =&amp;gt; import(&quot;@/components/Detail.vue&quot;),
          // props: true,
          props(route) {
            return route.query;
          },
        },
      ],
    },
  ],
});

export default router;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;newsList&quot;&amp;gt;
    &amp;lt;RouterLink
      v-for=&quot;news in newsList&quot;
      :to=&quot;{
        name: &apos;detail&apos;,
        query: {
          id: news.id,
          title: news.title,
          content: news.content,
        },
      }&quot;
      &amp;gt;{{ news.title }}&amp;lt;/RouterLink
    &amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;news-detail&quot;&amp;gt;
    &amp;lt;RouterView&amp;gt;&amp;lt;/RouterView&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive } from &quot;vue&quot;;

const newsList = reactive([
  {
    id: &quot;1904165055277039616&quot;,
    title: &quot;海关总署就芬太尼相关问题答记者问&quot;,
    content:
      &quot;海关总署有关负责人：《麻醉药品品种目录》（2013年版）里面列管的阿芬太尼、芬太尼、瑞芬太尼、舒芬太尼等具有药品属性的芬太尼按照麻醉药品管理。&quot;,
  },
  {
    id: &quot;1904165055281233920&quot;,
    title: &quot;NASA“撤回”登月宇航员“多元化”承诺&quot;,
    content:
      &quot;今日俄罗斯电视台网站3月23日报道，美国国家航空航天局（NASA）收回其公开承诺，即在阿耳忒弥斯登月计划下，将第一位女性和第一位有色人种送上月球。&quot;,
  },
  {
    id: &quot;1904165055281233921&quot;,
    title: &quot;春天是最懂氛围感的&quot;,
    content:
      &quot;春暖花开，春意融融。近日，北京各大公园里的春天已藏不住了，当古建与春天相遇，一场跨越时空的浪漫邂逅开启。人勤春早，收藏这组壁纸，新的一周，一起与美好同行&quot;,
  },
]);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.newsList {
  margin-left: 30px;
  float: left;
  width: 400px;
}
.newsList a {
  margin-right: 20px;
  display: block;
  margin-bottom: 20px;
}
.news-detail {
  width: 500px;
  height: 300px;
  border: 2px solid rgb(232, 193, 193);
  float: left;
  border-radius: 20px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h3&amp;gt;编号：{{ id }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;标题：{{ title }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;详情：{{ content }}&amp;lt;/h3&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
defineOptions({
  name: &quot;Detail&quot;,
});

defineProps([&quot;id&quot;, &quot;title&quot;, &quot;content&quot;]);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第三种写法&lt;/strong&gt;:对象写法，可以自己决定将什么作为 props 给路由组件&lt;/p&gt;
&lt;p&gt;这种写法只能写死，没有什么意义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;children: [
  {
    path: &quot;detail/:id/:title/:content&quot;,
    component: () =&amp;gt; import(&quot;@/components/Detail.vue&quot;),
    // props: true
    // props(route) {
    //     return route.query
    // }
    props: {
      x: 1,
      y: 2,
      z: 3,
    },
  },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;replace属性&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;作用：控制路由跳转时操作浏览器历史记录的模式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览器的历史记录有两种写入方式：分别为&lt;code&gt;push&lt;/code&gt;和&lt;code&gt;replace&lt;/code&gt;：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push&lt;/code&gt;是追加历史记录（默认值）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replace&lt;/code&gt;是替换当前记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;开启&lt;code&gt;replace&lt;/code&gt;模式：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;RouterLink replace .......&amp;gt;News&amp;lt;/RouterLink&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编程式路由导航&lt;/h3&gt;
&lt;p&gt;可以理解为，脱离&lt;code&gt;&amp;lt;RouterLink/&amp;gt;&lt;/code&gt;实现路由跳转&lt;/p&gt;
&lt;p&gt;简单demo首页停留三秒后，跳转&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h1&amp;gt;Home&amp;lt;/h1&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { onMounted } from &quot;vue&quot;;
import { useRouter } from &quot;vue-router&quot;;

let router = useRouter();

onMounted(() =&amp;gt; {
  setTimeout(() =&amp;gt; {
    router.push(&quot;news&quot;);
  }, 3000);
});
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现点击按钮也可查看新闻详情⬇️&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;newsList&quot; v-for=&quot;news in newsList&quot;&amp;gt;
    &amp;lt;button @click=&quot;showDetail(news)&quot;&amp;gt;查看&amp;lt;/button&amp;gt;
    &amp;lt;RouterLink
      :to=&quot;{
        name: &apos;detail&apos;,
        params: {
          id: news.id,
          title: news.title,
          content: news.content,
        },
      }&quot;
      &amp;gt;{{ news.title }}&amp;lt;/RouterLink
    &amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;news-detail&quot;&amp;gt;
    &amp;lt;RouterView&amp;gt;&amp;lt;/RouterView&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive } from &quot;vue&quot;;
import { useRouter } from &quot;vue-router&quot;;

const newsList = reactive([
  {
    id: &quot;1904165055277039616&quot;,
    title: &quot;海关总署就芬太尼相关问题答记者问&quot;,
    content:
      &quot;海关总署有关负责人：《麻醉药品品种目录》（2013年版）里面列管的阿芬太尼、芬太尼、瑞芬太尼、舒芬太尼等具有药品属性的芬太尼按照麻醉药品管理。&quot;,
  },
  {
    id: &quot;1904165055281233920&quot;,
    title: &quot;NASA“撤回”登月宇航员“多元化”承诺&quot;,
    content:
      &quot;今日俄罗斯电视台网站3月23日报道，美国国家航空航天局（NASA）收回其公开承诺，即在阿耳忒弥斯登月计划下，将第一位女性和第一位有色人种送上月球。&quot;,
  },
  {
    id: &quot;1904165055281233921&quot;,
    title: &quot;春天是最懂氛围感的&quot;,
    content:
      &quot;春暖花开，春意融融。近日，北京各大公园里的春天已藏不住了，当古建与春天相遇，一场跨越时空的浪漫邂逅开启。人勤春早，收藏这组壁纸，新的一周，一起与美好同行&quot;,
  },
]);
let router = useRouter();

interface NewsInter {
  id: string;
  title: string;
  content: string;
}
const showDetail = (news: NewsInter) =&amp;gt; {
  // 或者router.replace
  router.push({
    name: &quot;detail&quot;,
    params: {
      id: news.id,
      title: news.title,
      content: news.content,
    },
  });
};
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.newsList {
  margin-left: 30px;
  float: left;
  width: 400px;
}
.newsList a {
  margin-right: 20px;
  display: block;
  margin-bottom: 20px;
}
.news-detail {
  width: 500px;
  height: 300px;
  border: 2px solid rgb(232, 193, 193);
  float: left;
  border-radius: 20px;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 创建路由器，暴露出去

// 1.引入crateRouter
import { createRouter, createWebHashHistory } from &quot;vue-router&quot;;


// 2.创建路由器
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            name:&apos;home&apos;,
            path: &quot;/home&quot;,
            component: () =&amp;gt; import(&apos;@/views/Home.vue&apos;)
        },
        {
            name:&apos;about&apos;,
            path: &quot;/about&quot;,
            component: () =&amp;gt; import(&apos;@/views/About.vue&apos;)
        },
        {
            name: &apos;news&apos;,
            path: &quot;/news&quot;,
            component: () =&amp;gt; import(&apos;@/views/News.vue&apos;),
            children: [
                {
                    name: &apos;detail&apos;,
                    path: &quot;detail/:id/:title/:content&quot;,
                    component: () =&amp;gt; import(&apos;@/components/Detail.vue&apos;),
                    props: true
                    // props(route) {
                    //     return route.query
                    // }
                    // props: {
                    //     x: 1,
                    //     y: 2,
                    //     z: 3
                    // }
                }
            ]
        },
    ]
});

export default router;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h3&amp;gt;编号：{{ id }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;标题：{{ title }}&amp;lt;/h3&amp;gt;
  &amp;lt;h3&amp;gt;详情：{{ content }}&amp;lt;/h3&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
defineOptions({
  name: &quot;Detail&quot;,
});

defineProps([&quot;id&quot;, &quot;title&quot;, &quot;content&quot;]);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;重定向&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;作用：将特定的路径，重新定向到已有路由。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;具体编码：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;routes: [
        {
            path: &quot;/&quot;,
            redirect: &apos;/home&apos;
        },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pinia&lt;/h2&gt;
&lt;h3&gt;简单搭建&lt;/h3&gt;
&lt;p&gt;安装个&lt;code&gt;nanoid&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i nanoid
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;outer&quot;&amp;gt;
    &amp;lt;div&amp;gt;当前求和：{{ sum }}&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;select v-model.number=&quot;n&quot;&amp;gt;
        &amp;lt;option value=&quot;1&quot;&amp;gt;1&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;2&quot;&amp;gt;2&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;3&quot;&amp;gt;3&amp;lt;/option&amp;gt;
      &amp;lt;/select&amp;gt;
      &amp;lt;button @click=&quot;add&quot;&amp;gt;加&amp;lt;/button&amp;gt;
      &amp;lt;button @click=&quot;subtraction&quot;&amp;gt;减&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { ref } from &quot;vue&quot;;

defineOptions({
  name: &quot;Count&quot;,
});

let sum = ref(0);
let n = ref(1);

const add = () =&amp;gt; {
  sum.value += n.value;
};

const subtraction = () =&amp;gt; {
  sum.value -= n.value;
};
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.outer {
  height: 250px;
  width: 700px;
  border: 1px solid rebeccapurple;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;div class=&quot;talk&quot;&amp;gt;
        &amp;lt;button @click=&quot;getTalk&quot;&amp;gt;获取一句话&amp;lt;/button&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;ul&amp;gt;
                &amp;lt;li v-for=&quot;word in talkList&quot;&amp;gt;{{ word.title }}&amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import axios from &quot;axios&quot;;
import { reactive, ref } from &quot;vue&quot;;
import { nanoid } from &apos;nanoid&apos;

let talkList = reactive([{ id: &quot;001&quot;, title: &quot;广厦千间，夜眠仅需六尺；家财万贯，日食不过三餐。&quot; }])

const getTalk = async () =&amp;gt; {
    let { data } = await axios.get(&quot;https://api.vvhan.com/api/ian/rand&quot;);
    console.log(data);
    let obj = { id: nanoid(), title: data };
    talkList.unshift(obj);
};
&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;Count&amp;gt;&amp;lt;/Count&amp;gt;
  &amp;lt;LoveTalk&amp;gt;&amp;lt;/LoveTalk&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import Count from &quot;./components/Count.vue&quot;;
import LoveTalk from &quot;./components/LoveTalk.vue&quot;;

defineOptions({
  name: &quot;App&quot;,
});
&amp;lt;/script&amp;gt;


&amp;lt;style scoped&amp;gt;

&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;搭建Pinia&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm i pinia
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;main.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createPinia } from &apos;pinia&apos;

const pinia = createPinia();
app.use(pinia)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;存储+读取数据&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;命名注意&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;命名最好相相呼应
&lt;img src=&quot;../../assets/img/1906693801527738368.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;store中定义的数据不用&lt;code&gt;.value&lt;/code&gt;，因为它是包在&lt;code&gt;reactive&lt;/code&gt;中的
&lt;img src=&quot;../../assets/img/1906696710827016192.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineStore } from &quot;pinia&quot;;

export const useCountStore = defineStore(&apos;count&apos;, {
    state() {
        return {
            sum: 666
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { defineStore } from &quot;pinia&quot;;

export const useLoveTalkStore = defineStore(&apos;loveTalk&apos;, {
    state() {
        return {
            talkList: [{ id: &quot;001&quot;, title: &quot;广厦千间，夜眠仅需六尺；家财万贯，日食不过三餐。&quot; }]
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;outer&quot;&amp;gt;
    &amp;lt;div&amp;gt;当前求和：{{ countStore.sum }}&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;select v-model.number=&quot;n&quot;&amp;gt;
        &amp;lt;option value=&quot;1&quot;&amp;gt;1&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;2&quot;&amp;gt;2&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;3&quot;&amp;gt;3&amp;lt;/option&amp;gt;
      &amp;lt;/select&amp;gt;
      &amp;lt;button @click=&quot;add&quot;&amp;gt;加&amp;lt;/button&amp;gt;
      &amp;lt;button @click=&quot;subtraction&quot;&amp;gt;减&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { ref } from &quot;vue&quot;;

import { useCountStore } from &apos;@/store/count&apos;

defineOptions({
  name: &quot;Count&quot;,
});

let countStore = useCountStore();
let n = ref(1);

const add = () =&amp;gt; {
  // sum.value += n.value;
};

const subtraction = () =&amp;gt; {
  // sum.value -= n.value;
};
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.outer {
  height: 250px;
  width: 700px;
  border: 1px solid rebeccapurple;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;div class=&quot;talk&quot;&amp;gt;
        &amp;lt;button @click=&quot;getTalk&quot;&amp;gt;获取一句话&amp;lt;/button&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;ul&amp;gt;
                &amp;lt;li v-for=&quot;word in talkList.talkList&quot;&amp;gt;{{ word.title }}&amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import axios from &quot;axios&quot;;
import { reactive, ref } from &quot;vue&quot;;
import { nanoid } from &apos;nanoid&apos;
import { useLoveTalkStore } from &apos;@/store/loveTalk&apos;


let talkList = useLoveTalkStore();
console.log(talkList);

const getTalk = async () =&amp;gt; {
    // let { data } = await axios.get(&quot;https://api.vvhan.com/api/ian/rand&quot;);
    // console.log(data);
    // let obj = { id: nanoid(), title: data };
    // talkList.unshift(obj);
};
&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;修改数据三种方式&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方式1&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const add = () =&amp;gt; {
  // 第一种修改方式
   countStore.sum += n.value;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式2&lt;/strong&gt;
批量修改数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const add = () =&amp;gt; {
   第二种
   countStore.$patch({
     sum: 2,
     position: &apos;河北&apos;
   })

};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式3&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const add = () =&amp;gt; {
    // 第三种
  countStore.increase(n.value);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { defineStore } from &quot;pinia&quot;;

export const useCountStore = defineStore(&apos;count&apos;, {
    // 真正存储数据的地方
    state() {
        return {
            sum: 666,
            position: &apos;北京&apos;
        }
    },
    // actions里面放置的是一个一个的方法，用于响应组件中的“动作”
    actions: {
        increase(value: number) {
            // 修改数据（this是当前的store）
            this.sum += value;
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;storeToRefs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;借助&lt;code&gt;storeToRefs&lt;/code&gt;将&lt;code&gt;store&lt;/code&gt;中的数据转为&lt;code&gt;ref&lt;/code&gt;对象，方便在模板中使用。&lt;/li&gt;
&lt;li&gt;注意：&lt;code&gt;pinia&lt;/code&gt;提供的&lt;code&gt;storeToRefs&lt;/code&gt;只会将数据做转换，而&lt;code&gt;Vue&lt;/code&gt;的&lt;code&gt;toRefs&lt;/code&gt;会转换&lt;code&gt;store&lt;/code&gt;中所有数据（包括方法）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;outer&quot;&amp;gt;
    &amp;lt;div&amp;gt;当前求和：{{ sum }}&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;位置：{{ position }}&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;select v-model.number=&quot;n&quot;&amp;gt;
        &amp;lt;option value=&quot;1&quot;&amp;gt;1&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;2&quot;&amp;gt;2&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;3&quot;&amp;gt;3&amp;lt;/option&amp;gt;
      &amp;lt;/select&amp;gt;
      &amp;lt;button @click=&quot;add&quot;&amp;gt;加&amp;lt;/button&amp;gt;
      &amp;lt;button @click=&quot;subtraction&quot;&amp;gt;减&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { ref } from &quot;vue&quot;;
import { useCountStore } from &apos;@/store/count&apos;
import { storeToRefs } from &quot;pinia&quot;;

defineOptions({
  name: &quot;Count&quot;,
});

let countStore = useCountStore();
let { sum, position } = storeToRefs(countStore);
let n = ref(1);

const add = () =&amp;gt; {
  countStore.increase(n.value);
};

const subtraction = () =&amp;gt; {
  sum.value -= n.value;
};
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.outer {
  height: 250px;
  width: 700px;
  border: 1px solid rebeccapurple;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { defineStore } from &quot;pinia&quot;;

export const useCountStore = defineStore(&apos;count&apos;, {
    // 真正存储数据的地方
    state() {
        return {
            sum: 666,
            position: &apos;北京&apos;
        }
    },
    // actions里面放置的是一个一个的方法，用于响应组件中的“动作”
    actions: {
        increase(value: number) {
            // 修改数据（this是当前的store）
            this.sum += value;
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;div class=&quot;talk&quot;&amp;gt;
        &amp;lt;button @click=&quot;getTalk&quot;&amp;gt;获取一句话&amp;lt;/button&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;ul&amp;gt;
                &amp;lt;li v-for=&quot;word in talkList&quot;&amp;gt;{{ word.title }}&amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { storeToRefs } from &quot;pinia&quot;;
import { useLoveTalkStore } from &apos;@/store/loveTalk&apos;


let talkListStore = useLoveTalkStore();
let { talkList } = storeToRefs(talkListStore);

function getTalk() {
    talkListStore.getTalk();
}
&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import axios from &quot;axios&quot;;
import { nanoid } from &quot;nanoid&quot;;
import { defineStore } from &quot;pinia&quot;;

export const useLoveTalkStore = defineStore(&apos;loveTalk&apos;, {
    state() {
        return {
            talkList: [{ id: &quot;001&quot;, title: &quot;广厦千间，夜眠仅需六尺；家财万贯，日食不过三餐。&quot; }]
        }
    },
    actions: {
        async getTalk() {
            let { data } = await axios.get(&quot;https://api.vvhan.com/api/ian/rand&quot;);
            console.log(data);
            let obj = { id: nanoid(), title: data };
            this.talkList.unshift(obj);
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;getters&lt;/h3&gt;
&lt;p&gt;概念：当&lt;code&gt;state&lt;/code&gt;中的数据，需要经过处理后再使用时，可以使用&lt;code&gt;getters&lt;/code&gt;配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineStore } from &quot;pinia&quot;;

export const useCountStore = defineStore(&apos;count&apos;, {
    // 真正存储数据的地方
    state() {
        return {
            sum: 10,
            position: &apos;beijing&apos;
        }
    },
    // actions里面放置的是一个一个的方法，用于响应组件中的“动作”
    actions: {
        increase(value: number) {
            // 修改数据（this是当前的store）
            this.sum += value;
        }
    },
    getters: {
        zoomInTenTimes(state) {
            return state.sum * 10;
        },
        positionCapitalized(): string {
            return this.position.toUpperCase();
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;outer&quot;&amp;gt;
    &amp;lt;div&amp;gt;当前求和：{{ sum }},放大十倍后：{{ zoomInTenTimes }}&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;位置：{{ position }},Upper：{{ positionCapitalized }}&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;select v-model.number=&quot;n&quot;&amp;gt;
        &amp;lt;option value=&quot;1&quot;&amp;gt;1&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;2&quot;&amp;gt;2&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;3&quot;&amp;gt;3&amp;lt;/option&amp;gt;
      &amp;lt;/select&amp;gt;
      &amp;lt;button @click=&quot;add&quot;&amp;gt;加&amp;lt;/button&amp;gt;
      &amp;lt;button @click=&quot;subtraction&quot;&amp;gt;减&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { ref } from &quot;vue&quot;;
import { useCountStore } from &apos;@/store/count&apos;
import { storeToRefs } from &quot;pinia&quot;;

defineOptions({
  name: &quot;Count&quot;,
});

let countStore = useCountStore();
let { sum, position, zoomInTenTimes, positionCapitalized } = storeToRefs(countStore);
let n = ref(1);

const add = () =&amp;gt; {
  countStore.increase(n.value);
};

const subtraction = () =&amp;gt; {
  sum.value -= n.value;
};


&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.outer {
  height: 250px;
  width: 700px;
  border: 1px solid rebeccapurple;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/18998360451235633123.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;$subscribe&lt;/h3&gt;
&lt;p&gt;通过 store 的 &lt;code&gt;$subscribe()&lt;/code&gt; 方法侦听 &lt;code&gt;state&lt;/code&gt; 及其变化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;div class=&quot;talk&quot;&amp;gt;
        &amp;lt;button @click=&quot;getTalk&quot;&amp;gt;获取一句话&amp;lt;/button&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;ul&amp;gt;
                &amp;lt;li v-for=&quot;word in talkList&quot;&amp;gt;{{ word.title }}&amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
import { storeToRefs } from &quot;pinia&quot;;
import { useLoveTalkStore } from &apos;@/store/loveTalk&apos;


let talkListStore = useLoveTalkStore();
let { talkList } = storeToRefs(talkListStore);

talkListStore.$subscribe((mutate, state) =&amp;gt; {
    console.log(&apos;数据改变了&apos;);
    localStorage.setItem(&apos;talkList&apos;, JSON.stringify(state.talkList));
})

function getTalk() {
    talkListStore.getTalk();
}
&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import axios from &quot;axios&quot;;
import { nanoid } from &quot;nanoid&quot;;
import { defineStore } from &quot;pinia&quot;;

export const useLoveTalkStore = defineStore(&apos;loveTalk&apos;, {
    state() {
        return {
            // talkList: [{ id: &quot;001&quot;, title: &quot;广厦千间，夜眠仅需六尺；家财万贯，日食不过三餐。&quot; }]
            talkList: JSON.parse(localStorage.getItem(&apos;talkList&apos;) as string) || []
        }
    },
    actions: {
        async getTalk() {
            let { data } = await axios.get(&quot;https://api.vvhan.com/api/ian/rand&quot;);
            console.log(data);
            let obj = { id: nanoid(), title: data };
            this.talkList.unshift(obj);
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;store组合式写法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import axios from &quot;axios&quot;;
import { nanoid } from &quot;nanoid&quot;;
import { defineStore } from &quot;pinia&quot;;

// export const useLoveTalkStore = defineStore(&apos;loveTalk&apos;, {
//     state() {
//         return {
//             // talkList: [{ id: &quot;001&quot;, title: &quot;广厦千间，夜眠仅需六尺；家财万贯，日食不过三餐。&quot; }]
//             talkList: JSON.parse(localStorage.getItem(&apos;talkList&apos;) as string)||[]
//         }
//     },
//     actions: {
//         async getTalk() {
//             let { data } = await axios.get(&quot;https://api.vvhan.com/api/ian/rand&quot;);
//             console.log(data);
//             let obj = { id: nanoid(), title: data };
//             this.talkList.unshift(obj);
//         }
//     }
// });

import { reactive } from &apos;vue&apos;
export const useLoveTalkStore = defineStore(&apos;loveTalk&apos;, () =&amp;gt; {
    let talkList = reactive(JSON.parse(localStorage.getItem(&apos;talkList&apos;) as string) || []);

    const getTalk = async () =&amp;gt; {
        let { data } = await axios.get(&quot;https://api.vvhan.com/api/ian/rand&quot;);
        console.log(data);
        let obj = { id: nanoid(), title: data };
        talkList.unshift(obj);
    }

    return { talkList, getTalk }
});

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;组件通信&lt;/h2&gt;
&lt;h3&gt;方式1 props&lt;/h3&gt;
&lt;p&gt;概述：&lt;code&gt;props&lt;/code&gt;是使用频率最高的一种通信方式，常用与 ：&lt;strong&gt;父 ↔ 子&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若 &lt;strong&gt;父传子&lt;/strong&gt;：属性值是&lt;strong&gt;非函数&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;若 &lt;strong&gt;子传父&lt;/strong&gt;：属性值是&lt;strong&gt;函数&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
	&amp;lt;div&amp;gt;{{ car }}&amp;lt;/div&amp;gt;
	&amp;lt;Child :car=&quot;car&quot; :sendToy=&quot;getToy&quot;/&amp;gt;
	&amp;lt;h6&amp;gt;父接收到：{{ toy }}&amp;lt;/h6&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
	import { ref } from &apos;vue&apos;;
	import Child from &apos;./Child.vue&apos;;

	let car = ref(&apos;奔驰&apos;)
	let toy = ref(&apos;&apos;)

	const getToy = (value:string)=&amp;gt;{
		console.log(value);
		toy.value = value;
	}

&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
	.father{
		background-color:rgb(165, 164, 164);
		padding: 20px;
		border-radius: 10px;
	}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;child&quot;&amp;gt;
    &amp;lt;h3&amp;gt;子组件&amp;lt;/h3&amp;gt;
		&amp;lt;div&amp;gt;{{ toy }}&amp;lt;/div&amp;gt;
		&amp;lt;h6&amp;gt;父给子的：{{ car }}&amp;lt;/h6&amp;gt;
		&amp;lt;button @click=&quot;sendToy(toy)&quot;&amp;gt;给父传&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Child&quot;&amp;gt;
	import { ref } from &apos;vue&apos;;

	let toy = ref(&apos;奥特曼&apos;)
	defineProps([&apos;car&apos;,&apos;sendToy&apos;])
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
	.child{
		background-color: skyblue;
		padding: 10px;
		box-shadow: 0 0 10px black;
		border-radius: 10px;
	}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方式2 自定义事件&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;概述：自定义事件常用于：&lt;strong&gt;子 =&amp;gt; 父。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;注意区分好：原生事件、自定义事件。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;原生事件：
&lt;ul&gt;
&lt;li&gt;事件名是特定的（&lt;code&gt;click&lt;/code&gt;、&lt;code&gt;mosueenter&lt;/code&gt;等等）&lt;/li&gt;
&lt;li&gt;事件对象&lt;code&gt;$event&lt;/code&gt;: 是包含事件相关信息的对象（&lt;code&gt;pageX&lt;/code&gt;、&lt;code&gt;pageY&lt;/code&gt;、&lt;code&gt;target&lt;/code&gt;、&lt;code&gt;keyCode&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自定义事件：
&lt;ul&gt;
&lt;li&gt;事件名是任意名称&lt;/li&gt;
&lt;li&gt;&amp;lt;strong style=&quot;color:red&quot;&amp;gt;事件对象&lt;code&gt;$event&lt;/code&gt;: 是调用&lt;code&gt;emit&lt;/code&gt;时所提供的数据，可以是任意类型！！！&amp;lt;/strong &amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单demo，组件挂载3秒后触发事件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;father&quot;&amp;gt;
		&amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
		&amp;lt;!-- 给子组件Child绑定haha事件 --&amp;gt;
		&amp;lt;Child @haha=&quot;xyz&quot; /&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
import Child from &apos;./Child.vue&apos;
import { ref } from &quot;vue&quot;;

function xyz() {
	console.log(&apos;xyz&apos;);

}

&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.father {
	background-color: rgb(165, 164, 164);
	padding: 20px;
	border-radius: 10px;
}

.father button {
	margin-right: 5px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;child&quot;&amp;gt;
		&amp;lt;h3&amp;gt;子组件&amp;lt;/h3&amp;gt;
		&amp;lt;h5&amp;gt;玩具：{{ toy }}&amp;lt;/h5&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Child&quot;&amp;gt;
import { ref, onMounted } from &quot;vue&quot;;
let toy = ref(&apos;奥特曼&apos;)

onMounted(() =&amp;gt; {
	setTimeout(() =&amp;gt; {
		emit(&apos;haha&apos;);
	}, 3000);
})

// 声明事件
let emit = defineEmits([&apos;haha&apos;])
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.child {
	margin-top: 10px;
	background-color: rgb(76, 209, 76);
	padding: 10px;
	box-shadow: 0 0 10px black;
	border-radius: 10px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;点击触发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;father&quot;&amp;gt;
		&amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
		&amp;lt;h5 v-show=&quot;toy&quot;&amp;gt;父收到子：{{ toy }}&amp;lt;/h5&amp;gt;
		&amp;lt;!-- 给子组件Child绑定haha事件 --&amp;gt;
		&amp;lt;Child @get-toy=&quot;saveToy&quot; /&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
import Child from &apos;./Child.vue&apos;
import { ref } from &quot;vue&quot;;

let toy = ref(&apos;&apos;)

function saveToy(value: string) {
	toy.value = value;
}

&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.father {
	background-color: rgb(165, 164, 164);
	padding: 20px;
	border-radius: 10px;
}

.father button {
	margin-right: 5px;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;child&quot;&amp;gt;
		&amp;lt;h3&amp;gt;子组件&amp;lt;/h3&amp;gt;
		&amp;lt;h5&amp;gt;玩具：{{ toy }}&amp;lt;/h5&amp;gt;
		&amp;lt;button @click=&quot;emit(&apos;get-toy&apos;, toy)&quot;&amp;gt;测试&amp;lt;/button&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Child&quot;&amp;gt;
import { ref, onMounted } from &quot;vue&quot;;
let toy = ref(&apos;奥特曼&apos;)

// 声明事件
let emit = defineEmits([&apos;get-toy&apos;])
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.child {
	margin-top: 10px;
	background-color: rgb(76, 209, 76);
	padding: 10px;
	box-shadow: 0 0 10px black;
	border-radius: 10px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
在自定义事件中，vue官方推荐&lt;strong&gt;get-toy&lt;/strong&gt;这种命名方式
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;方式3 mitt&lt;/h3&gt;
&lt;p&gt;概述：与消息订阅与发布（&lt;code&gt;pubsub&lt;/code&gt;）功能类似，可以实现任意组件间通信。&lt;/p&gt;
&lt;p&gt;安装&lt;code&gt;mitt&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i mitt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单demo,在utils下创建&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 引入mitt
import mitt from &apos;mitt&apos;

// 调用mitt得到emitter，emitter能：绑定事件、触发事件
const emitter = mitt()

emitter.on(&apos;test1&apos;, () =&amp;gt; {
  console.log(&apos;test1被触发了&apos;);
})

emitter.on(&apos;test2&apos;, () =&amp;gt; {
  console.log(&apos;test2被触发了&apos;);
})

setInterval(() =&amp;gt; {
  // 触发事件
  emitter.emit(&apos;test1&apos;);
  emitter.emit(&apos;test2&apos;);
}, 1000);


setInterval(() =&amp;gt; {
  // emitter.off(&apos;test1&apos;)
  // emitter.off(&apos;test2&apos;)
  emitter.all.clear();  // 全部解绑 清理事件
}, 3000);

// 暴露emitter
export default emitter
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
    &amp;lt;Child1/&amp;gt;
    &amp;lt;Child2/&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
  import Child1 from &apos;./Child1.vue&apos;
  import Child2 from &apos;./Child2.vue&apos;
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
	.father{
		background-color:rgb(165, 164, 164);
		padding: 20px;
    border-radius: 10px;
	}
  .father button{
    margin-left: 5px;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;child1&quot;&amp;gt;
		&amp;lt;h3&amp;gt;子组件1&amp;lt;/h3&amp;gt;
		&amp;lt;h5&amp;gt;我的玩具:{{ toy }}&amp;lt;/h5&amp;gt;
		&amp;lt;button @click=&quot;emitter.emit(&apos;getToy&apos;, toy)&quot;&amp;gt;传给他&amp;lt;/button&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Child1&quot;&amp;gt;
import { ref } from &apos;vue&apos;
import emitter from &apos;@/utils/emitter&apos;;

let toy = ref(&apos;奥特曼&apos;)

&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.child1 {
	margin-top: 50px;
	background-color: skyblue;
	padding: 10px;
	box-shadow: 0 0 10px black;
	border-radius: 10px;
}

.child1 button {
	margin-right: 10px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;child2&quot;&amp;gt;
		&amp;lt;h3&amp;gt;子组件2&amp;lt;/h3&amp;gt;
		&amp;lt;h5&amp;gt;我的电脑:{{ computer }}&amp;lt;/h5&amp;gt;
		&amp;lt;h5 v-show=&quot;toy&quot;&amp;gt;接收到：{{ toy }}&amp;lt;/h5&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Child2&quot;&amp;gt;
import { ref, onUnmounted } from &apos;vue&apos;
import emitter from &apos;@/utils/emitter&apos;;
let toy = ref(&apos;&apos;)

emitter.on(&quot;getToy&quot;, (value: any) =&amp;gt; {
	console.log(value);
	toy.value = value
})

let computer = ref(&apos;联想&apos;);


// 在组件卸载时解绑getToy事件
onUnmounted(() =&amp;gt; {
	emitter.off(&apos;getToy&apos;)
})
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.child2 {
	margin-top: 50px;
	background-color: orange;
	padding: 10px;
	box-shadow: 0 0 10px black;
	border-radius: 10px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方式4 $attrs&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;概述：&lt;code&gt;$attrs&lt;/code&gt;用于实现&lt;strong&gt;当前组件的父组件&lt;/strong&gt;，向&lt;strong&gt;当前组件的子组件&lt;/strong&gt;通信（&lt;strong&gt;祖→孙&lt;/strong&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;具体说明：&lt;code&gt;$attrs&lt;/code&gt;是一个对象，包含所有父组件传入的标签属性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;code&gt;$attrs&lt;/code&gt;会自动排除&lt;code&gt;props&lt;/code&gt;中声明的属性(可以认为声明过的 &lt;code&gt;props&lt;/code&gt; 被子组件自己“消费”了)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;father&quot;&amp;gt;
		&amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
		&amp;lt;h5&amp;gt;{{ a }}&amp;lt;/h5&amp;gt;
		&amp;lt;h5&amp;gt;{{ b }}&amp;lt;/h5&amp;gt;
		&amp;lt;h5&amp;gt;{{ c }}&amp;lt;/h5&amp;gt;
		&amp;lt;Child :a=&quot;a&quot; :b=&quot;b&quot; :c=&quot;c&quot; :updateA=&quot;updateA&quot;&amp;gt;&amp;lt;/Child&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
import Child from &apos;./Child.vue&apos;
import { ref } from &apos;vue&apos;

let a = ref(1);
let b = ref(2);
let c = ref(3);

const updateA = (value: number) =&amp;gt; {
	a.value += value;
}

&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.father {
	background-color: rgb(165, 164, 164);
	padding: 20px;
	border-radius: 10px;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;child&quot;&amp;gt;
		&amp;lt;h3&amp;gt;子组件&amp;lt;/h3&amp;gt;
		&amp;lt;GrandChild v-bind=&quot;$attrs&quot;/&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Child&quot;&amp;gt;
	import GrandChild from &apos;./GrandChild.vue&apos;
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
	.child{
		margin-top: 20px;
		background-color: skyblue;
		padding: 20px;
		border-radius: 10px;
		box-shadow: 0 0 10px black;
	}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;grand-child&quot;&amp;gt;
		&amp;lt;h3&amp;gt;孙组件&amp;lt;/h3&amp;gt;
		&amp;lt;h5&amp;gt;{{ a }}&amp;lt;/h5&amp;gt;
		&amp;lt;h5&amp;gt;{{ b }}&amp;lt;/h5&amp;gt;
		&amp;lt;h5&amp;gt;{{ c }}&amp;lt;/h5&amp;gt;
		&amp;lt;button @click=&quot;updateA(6)&quot;&amp;gt;修改祖 A 值&amp;lt;/button&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;GrandChild&quot;&amp;gt;

defineProps([&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;updateA&apos;])
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.grand-child {
	margin-top: 20px;
	background-color: orange;
	padding: 20px;
	border-radius: 10px;
	box-shadow: 0 0 10px black;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/18998360451235986412.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;方式5 $refs与$parent&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;概述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$refs&lt;/code&gt;用于 ：&lt;strong&gt;父→子。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$parent&lt;/code&gt;用于：&lt;strong&gt;子→父。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原理如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$refs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;值为对象，包含所有被&lt;code&gt;ref&lt;/code&gt;属性标识的&lt;code&gt;DOM&lt;/code&gt;元素或组件实例。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$parent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;值为对象，当前组件的父组件实例对象。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;father&quot;&amp;gt;
		&amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
		&amp;lt;h5&amp;gt;房产：{{ house }}&amp;lt;/h5&amp;gt;
		&amp;lt;button @click=&quot;addBook($refs)&quot;&amp;gt;增加书籍&amp;lt;/button&amp;gt;
		&amp;lt;Child1 ref=&quot;c1&quot; /&amp;gt;
		&amp;lt;Child2 ref=&quot;c2&quot; /&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
import Child1 from &apos;./Child1.vue&apos;
import Child2 from &apos;./Child2.vue&apos;
import { ref, reactive } from &quot;vue&quot;

let house = ref(3)

const addBook = (refs: any) =&amp;gt; {
	for (let key in refs) {
		refs[key].book += 3;
	}
}

defineExpose({ house })
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.father {
	background-color: rgb(165, 164, 164);
	padding: 20px;
	border-radius: 10px;
}

.father button {
	margin-bottom: 10px;
	margin-left: 10px;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;child1&quot;&amp;gt;
		&amp;lt;h3&amp;gt;子组件1&amp;lt;/h3&amp;gt;
		&amp;lt;h5&amp;gt;书籍：{{ book }}&amp;lt;/h5&amp;gt;
		&amp;lt;h5&amp;gt;玩具：{{ toy }}&amp;lt;/h5&amp;gt;
		&amp;lt;button @click=&quot;reduceProperty($parent)&quot;&amp;gt;房产干掉&amp;lt;/button&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Child1&quot;&amp;gt;
import { ref } from &quot;vue&quot;;
let book = ref(3)
let toy = ref(&apos;奥特曼&apos;)

const reduceProperty = (parent: any) =&amp;gt; {
	parent.house -= 1;
}


defineExpose({ book, toy })
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.child1 {
	margin-top: 20px;
	background-color: skyblue;
	padding: 20px;
	border-radius: 10px;
	box-shadow: 0 0 10px black;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;child2&quot;&amp;gt;
		&amp;lt;h3&amp;gt;子组件2&amp;lt;/h3&amp;gt;
		&amp;lt;h5&amp;gt;书籍：{{ book }}&amp;lt;/h5&amp;gt;
		&amp;lt;h5&amp;gt;电脑：{{ computer }}&amp;lt;/h5&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Child2&quot;&amp;gt;
import { ref } from &apos;vue&apos;;

let book = ref(6)
let computer = ref(&apos;联想&apos;)

defineExpose({ book, computer })
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.child2 {
	margin-top: 20px;
	background-color: orange;
	padding: 20px;
	border-radius: 10px;
	box-shadow: 0 0 10px black;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/1910344507451244544.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;方式6 provide、inject&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;概述：实现&lt;strong&gt;祖孙组件&lt;/strong&gt;直接通信&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;具体使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在祖先组件中通过&lt;code&gt;provide&lt;/code&gt;配置向后代组件提供数据&lt;/li&gt;
&lt;li&gt;在后代组件中通过&lt;code&gt;inject&lt;/code&gt;配置来声明接收数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
    &amp;lt;h5&amp;gt;money:{{ money }}&amp;lt;/h5&amp;gt;
    &amp;lt;h5&amp;gt;car:{{ car.brand }}--价格：{{ car.price }}&amp;lt;/h5&amp;gt;
    &amp;lt;Child /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
import { reactive, ref, provide } from &apos;vue&apos;;
import Child from &apos;./Child.vue&apos;

let money = ref(100);

let car = reactive({
  brand: &apos;BMW&apos;,
  price: 200
})

function reduceMoney(value: number) {
  money.value -= value;
}

// 向后代提供数据
provide(&apos;moneyContext&apos;, { money, reduceMoney })
provide(&apos;car&apos;, car)

&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;grand-child&quot;&amp;gt;
    &amp;lt;h3&amp;gt;我是孙组件&amp;lt;/h3&amp;gt;
    &amp;lt;h5&amp;gt;{{ money }}&amp;lt;/h5&amp;gt;
    &amp;lt;h5&amp;gt;{{ y.brand }}--{{ y.price }}&amp;lt;/h5&amp;gt;
    &amp;lt;button @click=&quot;reduceMoney(6)&quot;&amp;gt;减钱&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;GrandChild&quot;&amp;gt;
import { inject } from &apos;vue&apos;;

let { money, reduceMoney } = inject(&apos;moneyContext&apos;, { money: 0, reduceMoney: (param: number) =&amp;gt; { } })
let y = inject(&apos;car&apos;, { brand: &apos;BMW&apos;, price: 0 })


&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.grand-child {
  background-color: orange;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 0 10px black;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;child&quot;&amp;gt;
    &amp;lt;h3&amp;gt;我是子组件&amp;lt;/h3&amp;gt;
    &amp;lt;GrandChild/&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Child&quot;&amp;gt;
  import GrandChild from &apos;./GrandChild.vue&apos;
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
  .child {
    margin-top: 20px;
    background-color: skyblue;
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0 0 10px black;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/1910693513142468608.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::warning
使用&lt;code&gt;provide&lt;/code&gt;提供数据的时候，不要&lt;code&gt;.value&lt;/code&gt; ,否则数据不是响应式的
:::&lt;/p&gt;
&lt;h2&gt;slot&lt;/h2&gt;
&lt;h3&gt;1.默认插槽&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
    &amp;lt;div class=&quot;content&quot;&amp;gt;
      &amp;lt;Game title=&quot;游戏列表&quot;&amp;gt;
        &amp;lt;ul&amp;gt;
          &amp;lt;li v-for=&quot;g in games&quot; :key=&quot;g.id&quot;&amp;gt;{{ g.name }}&amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
      &amp;lt;/Game&amp;gt;
      &amp;lt;Game title=&quot;美食城市&quot;&amp;gt;
        &amp;lt;img :src=&quot;imgUrl&quot; alt=&quot;&quot;&amp;gt;
      &amp;lt;/Game&amp;gt;
      &amp;lt;Game title=&quot;影视推荐&quot;&amp;gt;
        &amp;lt;video :src=&quot;videoUrl&quot; controls&amp;gt;&amp;lt;/video&amp;gt;
      &amp;lt;/Game&amp;gt;
    &amp;lt;/div&amp;gt;

  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
import { reactive, ref } from &quot;vue&quot;;
import Game from &quot;./Game.vue&quot;;
import { nanoid } from &quot;nanoid&quot;;

let games = reactive([{
  id: nanoid(),
  name: &quot;LOL&quot;
},
{
  id: nanoid(),
  name: &quot;王者农药&quot;
}])

let imgUrl = ref(&quot;https://th.bing.com/th/id/R.6f2c45f0e1970f362d4cb5eab87727c2?rik=y5WZ1SI%2fQsLR%2fA&amp;amp;riu=http%3a%2f%2fimg.daimg.com%2fuploads%2fallimg%2f190325%2f1-1Z325231625.jpg&amp;amp;ehk=O4I2%2bCxYfa8flgaMO4bok8%2fOAc8lDH1fs8%2fhpgKoBZ0%3d&amp;amp;risl=&amp;amp;pid=ImgRaw&amp;amp;r=0&quot;)
let videoUrl = ref(&quot;https://cdn.pixabay.com/video/2018/04/20/15711-266043576_large.mp4&quot;)
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;

}

.content {
  display: flex;
  justify-content: space-evenly;
}

img,
video {
  width: 100%;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;game&quot;&amp;gt;
    &amp;lt;h2&amp;gt;{{ title }}&amp;lt;/h2&amp;gt;
    &amp;lt;slot&amp;gt;默认内容&amp;lt;/slot&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Game&quot;&amp;gt;
import { reactive } from &apos;vue&apos;


defineProps([&apos;title&apos;])
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.game {
  width: 200px;
  height: 300px;
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
}

h2 {
  background-color: orange;
  text-align: center;
  font-size: 20px;
  font-weight: 800;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.具名插槽&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
    &amp;lt;div class=&quot;content&quot;&amp;gt;
      &amp;lt;Game&amp;gt;
        &amp;lt;template v-slot:s2&amp;gt;
          &amp;lt;ul&amp;gt;
            &amp;lt;li v-for=&quot;g in games&quot; :key=&quot;g.id&quot;&amp;gt;{{ g.name }}&amp;lt;/li&amp;gt;
          &amp;lt;/ul&amp;gt;
        &amp;lt;/template&amp;gt;
        &amp;lt;template v-slot:s1&amp;gt;
          &amp;lt;h2&amp;gt;游戏列表&amp;lt;/h2&amp;gt;
        &amp;lt;/template&amp;gt;
      &amp;lt;/Game&amp;gt;

      &amp;lt;Game&amp;gt;
        &amp;lt;template v-slot:s1&amp;gt;
          &amp;lt;h2&amp;gt;美食城市&amp;lt;/h2&amp;gt;
        &amp;lt;/template&amp;gt;
        &amp;lt;template v-slot:s2&amp;gt;
          &amp;lt;img :src=&quot;imgUrl&quot; alt=&quot;&quot;&amp;gt;
        &amp;lt;/template&amp;gt;
      &amp;lt;/Game&amp;gt;

      &amp;lt;Game&amp;gt;
        &amp;lt;template #s2&amp;gt;
          &amp;lt;video :src=&quot;videoUrl&quot; controls&amp;gt;&amp;lt;/video&amp;gt;
        &amp;lt;/template&amp;gt;
        &amp;lt;template #s1&amp;gt;
          &amp;lt;h2&amp;gt;影视推荐&amp;lt;/h2&amp;gt;
        &amp;lt;/template&amp;gt;
      &amp;lt;/Game&amp;gt;
    &amp;lt;/div&amp;gt;

  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
import { reactive, ref } from &quot;vue&quot;;
import Game from &quot;./Game.vue&quot;;
import { nanoid } from &quot;nanoid&quot;;

let games = reactive([{
  id: nanoid(),
  name: &quot;LOL&quot;
},
{
  id: nanoid(),
  name: &quot;王者农药&quot;
}])

let imgUrl = ref(&quot;https://th.bing.com/th/id/R.6f2c45f0e1970f362d4cb5eab87727c2?rik=y5WZ1SI%2fQsLR%2fA&amp;amp;riu=http%3a%2f%2fimg.daimg.com%2fuploads%2fallimg%2f190325%2f1-1Z325231625.jpg&amp;amp;ehk=O4I2%2bCxYfa8flgaMO4bok8%2fOAc8lDH1fs8%2fhpgKoBZ0%3d&amp;amp;risl=&amp;amp;pid=ImgRaw&amp;amp;r=0&quot;)
let videoUrl = ref(&quot;https://cdn.pixabay.com/video/2018/04/20/15711-266043576_large.mp4&quot;)
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;

}

.content {
  display: flex;
  justify-content: space-evenly;
}

img,
video {
  width: 100%;
}

h2 {
  background-color: orange;
  text-align: center;
  font-size: 20px;
  font-weight: 800;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;game&quot;&amp;gt;
    &amp;lt;slot name=&quot;s1&quot;&amp;gt;默认内容&amp;lt;/slot&amp;gt;
    &amp;lt;slot name=&quot;s2&quot;&amp;gt;默认内容&amp;lt;/slot&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Game&quot;&amp;gt;
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.game {
  width: 200px;
  height: 300px;
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
}

h2 {
  background-color: orange;
  text-align: center;
  font-size: 20px;
  font-weight: 800;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.作用域插槽&lt;/h3&gt;
&lt;p&gt;场景：数据在子那边，但根据数据生成的结构，却由父亲决定，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;h3&amp;gt;父组件&amp;lt;/h3&amp;gt;
    &amp;lt;div class=&quot;content&quot;&amp;gt;
      &amp;lt;Game&amp;gt;
        &amp;lt;template v-slot:qwer=&quot;{ games, x }&quot;&amp;gt;
          {{ x }}
          &amp;lt;h2&amp;gt;游戏列表&amp;lt;/h2&amp;gt;
          &amp;lt;ul&amp;gt;
            &amp;lt;li v-for=&quot;g in games&quot; :key=&quot;g.id&quot;&amp;gt;{{ g.name }}&amp;lt;/li&amp;gt;
          &amp;lt;/ul&amp;gt;
        &amp;lt;/template&amp;gt;
      &amp;lt;/Game&amp;gt;

      &amp;lt;Game&amp;gt;
        &amp;lt;template #qwer=&quot;{ games }&quot;&amp;gt;
          &amp;lt;h2&amp;gt;游戏列表&amp;lt;/h2&amp;gt;
          &amp;lt;ol&amp;gt;
            &amp;lt;li v-for=&quot;g in games&quot; :key=&quot;g.id&quot;&amp;gt;{{ g.name }}&amp;lt;/li&amp;gt;
          &amp;lt;/ol&amp;gt;
        &amp;lt;/template&amp;gt;
      &amp;lt;/Game&amp;gt;

      &amp;lt;Game&amp;gt;
        &amp;lt;template v-slot:qwer=&quot;{ games }&quot;&amp;gt;
          &amp;lt;h2&amp;gt;游戏列表&amp;lt;/h2&amp;gt;
          &amp;lt;h4 v-for=&quot;g in games&quot; :key=&quot;g.id&quot;&amp;gt;{{ g.name }}&amp;lt;/h4&amp;gt;
        &amp;lt;/template&amp;gt;
      &amp;lt;/Game&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Father&quot;&amp;gt;
import Game from &quot;./Game.vue&quot;;
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;

}

.content {
  display: flex;
  justify-content: space-evenly;
}

img,
video {
  width: 100%;
}

h2 {
  background-color: orange;
  text-align: center;
  font-size: 20px;
  font-weight: 800;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;game&quot;&amp;gt;
    &amp;lt;slot name=&quot;qwer&quot; :games=&quot;gameList&quot; x=&quot;Hello&quot;&amp;gt;默认内容&amp;lt;/slot&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot; name=&quot;Game&quot;&amp;gt;
import { nanoid } from &apos;nanoid&apos;;
import { reactive } from &apos;vue&apos;;


let gameList = reactive([{
  id: nanoid(),
  name: &quot;LOL&quot;
},
{
  id: nanoid(),
  name: &quot;王者农药&quot;
}])
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.game {
  width: 200px;
  height: 300px;
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
}

h2 {
  background-color: orange;
  text-align: center;
  font-size: 20px;
  font-weight: 800;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其它 API&lt;/h2&gt;
&lt;h3&gt;shallowRef 与 shallowReactive&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;shallowRef&lt;/code&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;作用：创建一个响应式数据，但只对顶层属性进行响应式处理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let myVar = shallowRef(initialValue);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特点：只跟踪引用值的变化，不关心值内部的属性变化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;code&gt;shallowReactive&lt;/code&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;作用：创建一个浅层响应式对象，只会使对象的最顶层属性变成响应式的，对象内部的嵌套属性则不会变成响应式的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const myObj = shallowReactive({ ... });
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特点：对象的顶层属性是响应式的，但嵌套对象的属性不是。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;通过使用 &lt;a href=&quot;https://cn.vuejs.org/api/reactivity-advanced.html#shallowref&quot;&gt;&lt;code&gt;shallowRef()&lt;/code&gt;&lt;/a&gt; 和 &lt;a href=&quot;https://cn.vuejs.org/api/reactivity-advanced.html#shallowreactive&quot;&gt;&lt;code&gt;shallowReactive()&lt;/code&gt;&lt;/a&gt; 来绕开深度响应。浅层式 &lt;code&gt;API&lt;/code&gt; 创建的状态只在其顶层是响应式的，对所有深层的对象不会做任何处理，避免了对每一个内部属性做响应式所带来的性能成本，这使得属性的访问变得更快，可提升性能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;readonly 与 shallowReadonly&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;&lt;code&gt;readonly&lt;/code&gt;&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;作用：用于创建一个对象的深只读副本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// original可以随便改，readOnlyCopy不可修改
const original = reactive({ ... });
const readOnlyCopy = readonly(original);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象的所有嵌套属性都将变为只读。&lt;/li&gt;
&lt;li&gt;任何尝试修改这个对象的操作都会被阻止（在开发模式下，还会在控制台中发出警告）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建不可变的状态快照。&lt;/li&gt;
&lt;li&gt;保护全局状态或配置不被修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;&lt;code&gt;shallowReadonly&lt;/code&gt;&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;作用：与 &lt;code&gt;readonly&lt;/code&gt; 类似，但只作用于对象的顶层属性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const original = reactive({ ... });
const shallowReadOnlyCopy = shallowReadonly(original);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;只将对象的顶层属性设置为只读，对象内部的嵌套属性仍然是可变的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;适用于只需保护对象顶层属性的场景。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>一些杂项</title><link>https://zzyang.top/posts/fragment-a/</link><guid isPermaLink="true">https://zzyang.top/posts/fragment-a/</guid><pubDate>Sat, 07 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;数据加解密&lt;/h2&gt;
&lt;h3&gt;RSA 加解密&lt;/h3&gt;
&lt;p&gt;RSA 加密算法是一种非对称加密算法，即 RSA 拥有一对密钥（公钥 和 私钥），公钥可公开。公钥加密的数据，只能由私钥解密；私钥加密的数据只能由公钥解密。&lt;/p&gt;
&lt;p&gt;用生活例子理解 RSA
想象一下邮箱和钥匙的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;传统方式（对称加密）：
你和朋友共用一个邮箱，你们俩都有同一把钥匙
问题：如何安全地把钥匙给朋友？

RSA方式（非对称加密）：
你有一个特殊的邮箱：
- 投递口（公钥）：任何人都可以往里投信
- 取信钥匙（私钥）：只有你有，只有你能取信

流程：
1. 你把邮箱地址告诉所有人（公钥公开）
2. 朋友用投递口投信（用公钥加密）
3. 只有你能用钥匙取信（用私钥解密）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;RSA 的核心特点&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;公钥（Public Key）：
- 可以公开给任何人
- 用来加密数据
- 就像邮箱的投递口

私钥（Private Key）：
- 绝对保密，只有自己知道
- 用来解密数据
- 就像邮箱的钥匙

神奇之处：
公钥加密的数据，只有对应的私钥才能解密！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;RSA 的优缺点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;✅ 优点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安全性极高：基于数学难题,&quot;大数分解&quot;&lt;/li&gt;
&lt;li&gt;密钥分发简单：公钥可以公开传输&lt;/li&gt;
&lt;li&gt;支持数字签名：可以验证身份&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;❌ 缺点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;速度慢：比 AES 慢 100-1000 倍&lt;/li&gt;
&lt;li&gt;数据大小限制：只能加密小数据（通常&amp;lt;245 字节）&lt;/li&gt;
&lt;li&gt;计算复杂：需要大量 CPU 资源&lt;/li&gt;
&lt;li&gt;密钥长度大：通常需要 2048 位以上&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;登录加解密流程
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-01_14-54-52.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🔐 用户登录密码加密传输&lt;/li&gt;
&lt;li&gt;📧 敏感信息加密存储&lt;/li&gt;
&lt;li&gt;🔑 API 密钥安全传输&lt;/li&gt;
&lt;li&gt;💳 支付信息加密处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么要在服务启动时自动初始化 RSA 密钥？&lt;/p&gt;
&lt;p&gt;RSA 密钥生成耗时较长&lt;/p&gt;
&lt;p&gt;测试时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RSA-1024位: ~50-100ms
RSA-2048位: ~200-500ms  ← 我们用的
RSA-4096位: ~1-3秒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户体验：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;用户A (第一个请求): 等待500ms ❌ 体验差
用户B (后续请求):   立即响应   ✅ 体验好
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;📝 总结&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每人一对 RSA 密钥 ← 这是端到端加密模式&lt;/li&gt;
&lt;li&gt;服务器一对 RSA 密钥 ← 这是传输加密模式&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AES 加解密&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;什么是 AES？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AES = &lt;code&gt;Advanced Encryption Standard&lt;/code&gt;（高级加密标准）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对称加密算法：加密和解密使用同一个密钥&lt;/li&gt;
&lt;li&gt;速度很快：比 RSA 快 100-1000 倍&lt;/li&gt;
&lt;li&gt;安全性高：美国政府标准，军用级别加密&lt;/li&gt;
&lt;li&gt;支持大数据：可以加密任意大小的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;对称加密 vs 非对称加密&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RSA（非对称加密）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;公钥加密 → 私钥解密&lt;/li&gt;
&lt;li&gt;优点：安全性高，不需要共享密钥&lt;/li&gt;
&lt;li&gt;缺点：速度慢，只能加密小数据&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;AES（对称加密）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;密钥加密 → 同一个密钥解密&lt;/li&gt;
&lt;li&gt;优点：速度快，可以加密大数据&lt;/li&gt;
&lt;li&gt;缺点：需要安全地共享密钥&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;AES 工作原理（简化版）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;加密过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;原始数据: &quot;Hello World&quot;
密钥:     &quot;MySecretKey123456&quot;
        ↓
    【AES加密算法】
        ↓
加密结果: &quot;j8vV2K+5x9...&quot;（乱码）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解密过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;加密数据: &quot;j8vV2K+5x9...&quot;
密钥:     &quot;MySecretKey123456&quot;（必须是同一个密钥）
        ↓
    【AES解密算法】
        ↓
原始数据: &quot;Hello World&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ AES 优点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对称加密，实现简单：加密解密用同一个密钥&lt;/li&gt;
&lt;li&gt;速度快：比 RSA 快几百倍&lt;/li&gt;
&lt;li&gt;无大小限制：可以加密任意大小的数据&lt;/li&gt;
&lt;li&gt;资源消耗低,CPU 占用很少,内存占用很少&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;❌ AES 缺点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 🔑 密钥分发困难
   - 如何安全地把密钥给对方？
   - 网络传输密钥有风险
   - 需要提前约定密钥

2. 👥 密钥管理复杂
   - 每个用户都需要不同密钥
   - 密钥泄露影响范围大
   - 密钥更新困难

3. 🚫 无法数字签名
   - 不能验证消息来源
   - 不能防止否认
   - 需要额外的认证机制

4. 🔄 密钥共享问题
   - 同一密钥加密所有数据
   - 一处泄露，全盘皆输
   - 无法区分不同用户
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;RSA vs AES 详细对比解析&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;RSA (非对称加密)&lt;/th&gt;
&lt;th&gt;AES (对称加密)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;加密类型&lt;/td&gt;
&lt;td&gt;非对称 (公钥+私钥)&lt;/td&gt;
&lt;td&gt;对称 (同一密钥)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;密钥数量&lt;/td&gt;
&lt;td&gt;2 个 (公钥+私钥)&lt;/td&gt;
&lt;td&gt;1 个 (共享密钥)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加密速度&lt;/td&gt;
&lt;td&gt;很慢&lt;/td&gt;
&lt;td&gt;很快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据大小限制&lt;/td&gt;
&lt;td&gt;有限制 (~245 字节)&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;密钥分发&lt;/td&gt;
&lt;td&gt;简单 (公钥可公开)&lt;/td&gt;
&lt;td&gt;复杂 (需安全传输)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主要用途&lt;/td&gt;
&lt;td&gt;密钥交换、数字签名&lt;/td&gt;
&lt;td&gt;大量数据加密&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;计算复杂度&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;密钥长度&lt;/td&gt;
&lt;td&gt;2048-4096 位&lt;/td&gt;
&lt;td&gt;128-256 位&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;RSA + AES 组合加密&lt;/h3&gt;
&lt;p&gt;🤔 为什么要组合使用？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 只用RSA的问题
const bigData = &quot;一个1MB的文件内容...&quot;;
const encrypted = RSA.encrypt(bigData, publicKey);
// 💥 报错！RSA只能加密245字节

// ❌ 只用AES的问题
const data = &quot;敏感数据&quot;;
const key = &quot;mySecretKey123&quot;; // 🚨 如何安全地把这个密钥给对方？
const encrypted = AES.encrypt(data, key);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;组合使用的天才之处&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ✅ RSA + AES 完美结合
// RSA负责：安全传输AES密钥（小数据）
// AES负责：快速加密实际数据（大数据）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;前端加密传输&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;流程如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;阶段一：密钥准备
前端 → 请求后端RSA公钥 → 后端返回公钥 → 前端缓存公钥

阶段二：数据加密
前端生成AES密钥 → 用AES加密数据 → 用RSA加密AES密钥 → 打包发送 (用RSA公钥加密的AES密钥、加密的数据包)

阶段三：数据传输
前端发送加密包 → 网络传输 → 后端接收加密包

阶段四：数据解密
后端用RSA私钥解密AES密钥 → 用AES密钥解密数据 → 获得原始数据

阶段五：业务处理
后端处理业务逻辑 → 返回处理结果
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据包结构:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;encryptedData&quot;: &quot;AES加密后的数据&quot;,
  &quot;encryptedKey&quot;: &quot;RSA加密后的AES密钥&quot;,
  &quot;timestamp&quot;: 1703123456789,
  &quot;algorithm&quot;: &quot;RSA-2048+AES-256&quot;,
  &quot;version&quot;: &quot;1.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-01_16-19-17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;密钥管理&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ✅ 正确做法
- RSA私钥只存在服务器端
- RSA公钥可以缓存，但定期更新
- AES密钥每次随机生成，用完即废

// ❌ 错误做法
- 把RSA私钥存在前端
- 重复使用同一个AES密钥
- 把密钥硬编码在代码中
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;后端响应加密&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;逻辑反转：前端生成密钥对
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-01_16-41-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;流程如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;加密通信：

1. 前端生成RSA密钥对（公钥 + 私钥）
2. 前端保存私钥到本地
3. 前端发送公钥到后端注册
4. 后端保存前端公钥到Redis

解密响应：

5. 前端请求敏感数据（携带keyId + requireEncrypted标识）
6. 后端检查请求标识
7. 后端生成随机AES密钥
8. 后端用AES密钥加密响应数据
9. 后端用前端公钥加密AES密钥
10. 后端返回加密包


11. 前端接收加密响应包
12. 前端用私钥解密AES密钥
13. 前端用AES密钥解密响应数据
14. 前端得到原始响应数据

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;防攻击措施&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 前端私钥永远不发送给后端&lt;/li&gt;
&lt;li&gt;✅ 前端公钥有过期时间（24 小时）&lt;/li&gt;
&lt;li&gt;✅ AES 密钥每次随机生成，用完即废&lt;/li&gt;
&lt;li&gt;✅ 使用 Redis 存储临时密钥信息
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm add jsencrypt crypto-js

pnpm add -D @types/crypto-js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;ts 工具类&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/utils/cryptoUtils.ts

import JSEncrypt from &quot;jsencrypt&quot;;
import CryptoJS from &quot;crypto-js&quot;;

/**
 * RSA密钥对接口，定义了密钥对的结构
 */
export interface RsaKeyPair {
  publicKey: string; // Base64编码的公钥
  privateKey: string; // Base64编码的私钥
}

/**
 * 前端RSA和AES加解密工具类
 *
 * JSEncrypt: 用于处理RSA非对称加密，通常用于加密少量敏感数据（如AES密钥）。
 * CryptoJS: 用于处理AES对称加密，通常用于加密大量业务数据。
 *
 * 推荐用法 (混合加密):
 * 1. 从后端获取RSA公钥。
 * 2. 在前端生成一个随机的AES密钥和IV。
 * 3. 使用AES加密业务数据。
 * 4. 使用RSA公钥加密AES密钥。
 * 5. 将RSA加密后的AES密钥、AES加密后的数据以及IV一起发送给后端。
 */
export class CryptoUtils {
  // =================================================================================
  // RSA (非对称加密) 相关方法
  // =================================================================================

  /**
   * 同步生成RSA密钥对
   *
   * @param keySize 密钥大小 (1024, 2048, 4096)，默认为 2048 位。
   * @returns 返回一个包含公钥和私钥的对象 (RsaKeyPair)。
   *
   * @description
   * 这个方法会阻塞主线程直到密钥生成完毕。
   * 对于需要立即获取密钥的简单场景很方便。
   */
  public static generateKeyPairSync(
    keySize: 1024 | 2048 | 4096 = 2048
  ): RsaKeyPair {
    const encrypt = new JSEncrypt({ default_key_size: keySize.toString() });

    // JSEncrypt 在实例化时会自动生成密钥
    // 我们可以直接获取它们
    const publicKey = encrypt.getPublicKey();
    const privateKey = encrypt.getPrivateKey();

    return {
      publicKey,
      privateKey,
    };
  }

  /**
   * 异步生成RSA密钥对
   *
   * @param keySize 密钥大小 (1024, 2048, 4096)，默认为 2048 位。
   * @returns 返回一个Promise，解析后得到包含公钥和私钥的对象 (RsaKeyPair)。
   *
   * @description
   * 推荐在生产环境中使用此方法，因为它不会阻塞UI线程。
   * 特别是生成4096位密钥时，同步方法可能会导致页面卡顿。
   */
  public static generateKeyPairAsync(
    keySize: 1024 | 2048 | 4096 = 2048
  ): Promise&amp;lt;RsaKeyPair&amp;gt; {
    return new Promise((resolve, reject) =&amp;gt; {
      try {
        const encrypt = new JSEncrypt({ default_key_size: keySize.toString() });

        // JSEncrypt的密钥生成是在构造函数中同步执行的，
        // 但为了提供一个标准的异步接口，我们将其包装在Promise中。
        // 这也使得未来如果库更新为真正的异步生成，我们可以平滑过渡。
        const keyPair: RsaKeyPair = {
          publicKey: encrypt.getPublicKey(),
          privateKey: encrypt.getPrivateKey(),
        };

        // 使用 setTimeout(0) 将解析操作推到下一个事件循环，
        // 模拟异步行为，让调用方可以一致地使用 .then()
        setTimeout(() =&amp;gt; {
          resolve(keyPair);
        }, 0);
      } catch (error) {
        reject(error);
      }
    });
  }

  /**
   * RSA公钥加密
   * @param data 待加密的明文数据
   * @param publicKeyBase64 Base64编码的RSA公钥字符串
   * @returns Base64编码的加密后字符串，如果加密失败则返回false
   */
  public static rsaEncrypt(
    data: string,
    publicKeyBase64: string
  ): string | false {
    const encrypt = new JSEncrypt();
    encrypt.setPublicKey(publicKeyBase64);
    return encrypt.encrypt(data);
  }

  /**
   * RSA私钥解密 (注意：私钥通常不应在前端存储或使用)
   * @param encryptedData Base64编码的加密字符串
   * @param privateKeyBase64 Base64编码的RSA私钥字符串
   * @returns 解密后的明文数据，如果解密失败则返回false
   */
  public static rsaDecrypt(
    encryptedData: string,
    privateKeyBase64: string
  ): string | false {
    const decrypt = new JSEncrypt();
    decrypt.setPrivateKey(privateKeyBase64);
    return decrypt.decrypt(encryptedData);
  }

  // =================================================================================
  // AES (对称加密) 相关方法
  // =================================================================================

  /**
   * 生成随机AES密钥
   * @param keySize 密钥大小 (128, 192, 256)，单位：位。返回的密钥长度为 keySize / 8 字节。
   * @returns Base64编码的AES密钥字符串
   */
  public static generateAesKey(keySize: 128 | 192 | 256 = 256): string {
    const key = CryptoJS.lib.WordArray.random(keySize / 8);
    return CryptoJS.enc.Base64.stringify(key);
  }

  /**
   * 生成随机AES初始化向量 (IV)
   * @returns Base64编码的IV字符串 (16字节)
   */
  public static generateIv(): string {
    const iv = CryptoJS.lib.WordArray.random(16);
    return CryptoJS.enc.Base64.stringify(iv);
  }

  /**
   * AES加密 (AES-256-CBC)
   * @param data 待加密的明文数据
   * @param keyBase64 Base64编码的AES密钥
   * @param ivBase64 Base64编码的IV
   * @returns Base64编码的加密后字符串
   */
  public static aesEncrypt(
    data: string,
    keyBase64: string,
    ivBase64: string
  ): string {
    const key = CryptoJS.enc.Base64.parse(keyBase64);
    const iv = CryptoJS.enc.Base64.parse(ivBase64);
    const encrypted = CryptoJS.AES.encrypt(data, key, {
      iv: iv,
      mode: CryptoJS.mode.CBC,
      padding: CryptoJS.pad.Pkcs7,
    });
    return encrypted.toString();
  }

  /**
   * AES解密 (AES-256-CBC)
   * @param encryptedData Base64编码的加密字符串
   * @param keyBase64 Base64编码的AES密钥
   * @param ivBase64 Base64编码的IV
   * @returns 解密后的明文数据
   */
  public static aesDecrypt(
    encryptedData: string,
    keyBase64: string,
    ivBase64: string
  ): string {
    const key = CryptoJS.enc.Base64.parse(keyBase64);
    const iv = CryptoJS.enc.Base64.parse(ivBase64);
    const decrypted = CryptoJS.AES.decrypt(encryptedData, key, {
      iv: iv,
      mode: CryptoJS.mode.CBC,
      padding: CryptoJS.pad.Pkcs7,
    });
    return decrypted.toString(CryptoJS.enc.Utf8);
  }
}

// =================================================================================
// 使用示例 - 你可以在Vue组件的 onMounted 钩子或某个方法中调用
// =================================================================================
export function runCryptoDemo() {
  console.log(&quot;=============== 前端加密工具类示例 ===============&quot;);

  // 模拟从后端获取到的RSA公钥
  // 这是一个示例公钥，实际项目中应通过API从后端获取
  const backendRsaPublicKey =
    &quot;MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyXQ2s/53/7Z+a/gJ&quot; +
    &quot;cEIFGZfz9Jt1OV3a/1D/Shs8s5g8F7/C2sp7a0eC6X7JGjM2Zg6QcZJj/O&quot; +
    &quot;LqJzYh+0Xl/gVcY8Y5T6Z8f6d8a7c6b9e/c8a9f6d7g8e/b7a6f5e8d9c0&quot; +
    &quot;b/f6a8e7d5f4g3h2j1k0l/m5n7o9p1q2r4s6t8v0w+x3y5z7A9B1C3D5E7&quot; +
    &quot;F9G1H3J5K7L9M1N3O5P7R9T1V3W5X7Z9A1B3C5D7E9F1G3H5J7K9L1M3N5&quot; +
    &quot;O7P9R1T3V5W7X9Z1A3B5C7D9E1F3G5H7J9K1L3M5N7O9P1R3V5W7X9Z1A3&quot; +
    &quot;B5C7D9E1F3G5H7J9K1L3M5N7O9P1R3V5W7X9Z1A3B5C7D9E1F3G5H7J9K&quot; +
    &quot;1L3M5N7O9P1R3V5W7X9Z==&quot;;

  // ==================== AES示例 ====================
  console.log(&quot;\n=============== AES示例 ===============&quot;);
  // 1. 生成AES密钥和IV
  const aesKey = CryptoUtils.generateAesKey(256);
  const iv = CryptoUtils.generateIv();
  console.log(&quot;生成的AES密钥 (Base64):&quot;, aesKey);
  console.log(&quot;生成的IV (Base64):&quot;, iv);

  // 2. AES加密
  const aesOriginalText = &quot;这是前端用AES加密的大量业务数据。&quot;;
  const aesEncrypted = CryptoUtils.aesEncrypt(aesOriginalText, aesKey, iv);
  console.log(&quot;AES加密后:&quot;, aesEncrypted);

  // 3. AES解密 (仅用于演示)
  const aesDecrypted = CryptoUtils.aesDecrypt(aesEncrypted, aesKey, iv);
  console.log(&quot;AES解密后:&quot;, aesDecrypted);
  console.log(&quot;AES加解密是否成功:&quot;, aesOriginalText === aesDecrypted);

  // ==================== 混合加密示例（推荐用法） ====================
  console.log(&quot;\n=============== 混合加密示例 ===============&quot;);
  const sensitiveData = JSON.stringify({
    username: &quot;frontend_user&quot;,
    password: &quot;secure_password_123&quot;,
    action: &quot;login&quot;,
  });
  console.log(&quot;原始敏感数据:&quot;, sensitiveData);

  // 1. 生成临时的AES密钥和IV
  const sessionAesKey = CryptoUtils.generateAesKey(256);
  const sessionIv = CryptoUtils.generateIv();

  // 2. 使用AES加密敏感数据
  const encryptedData = CryptoUtils.aesEncrypt(
    sensitiveData,
    sessionAesKey,
    sessionIv
  );
  console.log(&quot;AES加密后的数据:&quot;, encryptedData);

  // 3. 使用从后端获取的RSA公钥加密AES密钥
  const encryptedAesKey = CryptoUtils.rsaEncrypt(
    sessionAesKey,
    backendRsaPublicKey
  );
  if (!encryptedAesKey) {
    console.error(&quot;RSA加密AES密钥失败！&quot;);
    return;
  }
  console.log(&quot;RSA加密后的AES密钥:&quot;, encryptedAesKey);

  // 4. 准备发送到后端的数据包
  const payload = {
    key: encryptedAesKey, // RSA加密的AES密钥
    data: encryptedData, // AES加密的业务数据
    iv: sessionIv, // AES的IV
    timestamp: Date.now(),
  };

  console.log(&quot;\n准备发送到后端的数据包:&quot;, JSON.stringify(payload, null, 2));

  // 5. 接下来可以通过axios等发送payload到后端
  // axios.post(&apos;/api/secure/data&apos;, payload).then(...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;java 工具类&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.zzy.admin.utils;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.security.KeyFactory;

/**
 * RSA和AES加解密工具类
 * &amp;lt;p&amp;gt;
 * RSA (非对称加密): 用于加密小数据（如AES密钥）、数字签名。速度慢。
 * AES (对称加密): 用于加密大量数据。速度快。
 * &amp;lt;p&amp;gt;
 * 推荐用法 (混合加密):
 * 1. 后端生成RSA密钥对（公钥+私钥）。
 * 2. 前端请求后端获取RSA公钥。
 * 3. 前端生成一个随机的AES密钥。
 * 4. 前端用AES密钥加密业务数据。
 * 5. 前端用RSA公钥加密AES密钥。
 * 6. 将RSA加密后的AES密钥和AES加密后的数据一起发送给后端。
 * 7. 后端用RSA私钥解密AES密钥。
 * 8. 后端用解密后的AES密钥解密业务数据。
 */
public class CryptoUtils {
    // RSA算法标识
    private static final String RSA_ALGORITHM = &quot;RSA&quot;;
    // AES算法标识 (CBC模式，PKCS5填充)
    private static final String AES_ALGORITHM = &quot;AES/CBC/PKCS5Padding&quot;;
    // AES密钥算法
    private static final String AES_KEY_ALGORITHM = &quot;AES&quot;;

    /**
     * 私有构造函数，防止实例化
     */
    private CryptoUtils() {
    }

    // =================================================================================
    // RSA (非对称加密) 相关方法
    // =================================================================================

    /**
     * 生成RSA密钥对
     *
     * @param keySize 密钥大小 (推荐 2048)
     * @return KeyPair 密钥对对象
     * @throws NoSuchAlgorithmException 如果请求的加密算法不可用
     */
    public static KeyPair generateRsaKeyPair(int keySize) throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
        keyPairGenerator.initialize(keySize);
        return keyPairGenerator.generateKeyPair();
    }

    /**
     * RSA公钥加密
     *
     * @param data      待加密的明文数据
     * @param publicKey RSA公钥
     * @return Base64编码的加密后字符串
     * @throws Exception 加密过程中可能发生异常
     */
    public static String rsaEncrypt(String data, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * RSA私钥解密
     *
     * @param encryptedData Base64编码的加密字符串
     * @param privateKey    RSA私钥
     * @return 解密后的明文数据
     * @throws Exception 解密过程中可能发生异常
     */
    public static String rsaDecrypt(String encryptedData, PrivateKey privateKey) throws Exception {
        byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    // =================================================================================
    // AES (对称加密) 相关方法
    // =================================================================================

    /**
     * 生成AES密钥
     *
     * @param keySize 密钥大小 (推荐 128, 192, 256)
     * @return SecretKey AES密钥对象
     * @throws NoSuchAlgorithmException 如果请求的加密算法不可用
     */
    public static SecretKey generateAesKey(int keySize) throws NoSuchAlgorithmException {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(AES_KEY_ALGORITHM);
        keyGenerator.init(keySize);
        return keyGenerator.generateKey();
    }

    /**
     * AES加密
     *
     * @param data      待加密的明文数据
     * @param secretKey AES密钥
     * @param iv        初始化向量 (16字节)
     * @return Base64编码的加密后字符串
     * @throws Exception 加密过程中可能发生异常
     */
    public static String aesEncrypt(String data, SecretKey secretKey, IvParameterSpec iv) throws Exception {
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
        byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * AES解密
     *
     * @param encryptedData Base64编码的加密字符串
     * @param secretKey     AES密钥
     * @param iv            初始化向量 (16字节)
     * @return 解密后的明文数据
     * @throws Exception 解密过程中可能发生异常
     */
    public static String aesDecrypt(String encryptedData, SecretKey secretKey, IvParameterSpec iv) throws Exception {
        byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    /**
     * 生成一个随机的16字节初始化向量 (IV)
     *
     * @return IvParameterSpec IV对象
     */
    public static IvParameterSpec generateIv() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return new IvParameterSpec(iv);
    }

    // =================================================================================
    // 密钥转换和存储相关方法
    // =================================================================================

    /**
     * 将公钥对象转换为Base64编码的字符串 (方便传输)
     *
     * @param publicKey 公钥对象
     * @return Base64字符串
     */
    public static String publicKeyToString(PublicKey publicKey) {
        return Base64.getEncoder().encodeToString(publicKey.getEncoded());
    }

    /**
     * 将私钥对象转换为Base64编码的字符串 (方便存储)
     *
     * @param privateKey 私钥对象
     * @return Base64字符串
     */
    public static String privateKeyToString(PrivateKey privateKey) {
        return Base64.getEncoder().encodeToString(privateKey.getEncoded());
    }

    /**
     * 将AES密钥对象转换为Base64编码的字符串 (方便传输和存储)
     *
     * @param secretKey AES密钥对象
     * @return Base64字符串
     */
    public static String aesKeyToString(SecretKey secretKey) {
        return Base64.getEncoder().encodeToString(secretKey.getEncoded());
    }

    /**
     * 将Base64编码的公钥字符串转换回PublicKey对象
     *
     * @param publicKeyString Base64编码的公钥字符串
     * @return PublicKey对象
     * @throws Exception 转换异常
     */
    public static PublicKey stringToPublicKey(String publicKeyString) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(publicKeyString);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        return keyFactory.generatePublic(keySpec);
    }

    /**
     * 将Base64编码的私钥字符串转换回PrivateKey对象
     *
     * @param privateKeyString Base64编码的私钥字符串
     * @return PrivateKey对象
     * @throws Exception 转换异常
     */
    public static PrivateKey stringToPrivateKey(String privateKeyString) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(privateKeyString);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        return keyFactory.generatePrivate(keySpec);
    }

    /**
     * 将Base64编码的AES密钥字符串转换回SecretKey对象
     *
     * @param secretKeyString Base64编码的AES密钥字符串
     * @return SecretKey对象
     */
    public static SecretKey stringToAesKey(String secretKeyString) {
        byte[] keyBytes = Base64.getDecoder().decode(secretKeyString);
        return new SecretKeySpec(keyBytes, AES_KEY_ALGORITHM);
    }

    // =================================================================================
    // Main方法 - 使用示例
    // =================================================================================
    public static void main(String[] args) {
        try {
            // ==================== RSA示例 ====================
            System.out.println(&quot;=============== RSA示例 ===============&quot;);
            // 1. 生成RSA密钥对
            KeyPair rsaKeyPair = generateRsaKeyPair(2048);
            PublicKey rsaPublicKey = rsaKeyPair.getPublic();
            PrivateKey rsaPrivateKey = rsaKeyPair.getPrivate();

            // 2. 将密钥转换为字符串（方便查看和传输）
            String rsaPublicKeyStr = publicKeyToString(rsaPublicKey);
            String rsaPrivateKeyStr = privateKeyToString(rsaPrivateKey);
            System.out.println(&quot;RSA公钥 (Base64): &quot; + rsaPublicKeyStr);
            System.out.println(&quot;RSA私钥 (Base64): &quot; + rsaPrivateKeyStr);

            // 3. RSA加密
            String originalText = &quot;这是一段需要RSA加密的敏感信息&quot;;
            String rsaEncryptedText = rsaEncrypt(originalText, rsaPublicKey);
            System.out.println(&quot;RSA加密后: &quot; + rsaEncryptedText);

            // 4. RSA解密
            String rsaDecryptedText = rsaDecrypt(rsaEncryptedText, rsaPrivateKey);
            System.out.println(&quot;RSA解密后: &quot; + rsaDecryptedText);
            System.out.println(&quot;RSA加解密是否成功: &quot; + originalText.equals(rsaDecryptedText));

            // ==================== AES示例 ====================
            System.out.println(&quot;\n=============== AES示例 ===============&quot;);
            // 1. 生成AES密钥
            SecretKey aesKey = generateAesKey(256);
            String aesKeyStr = aesKeyToString(aesKey);
            System.out.println(&quot;AES密钥 (Base64): &quot; + aesKeyStr);

            // 2. 生成初始化向量IV
            IvParameterSpec iv = generateIv();
            String ivStr = Base64.getEncoder().encodeToString(iv.getIV());
            System.out.println(&quot;AES初始化向量 (Base64): &quot; + ivStr);


            // 3. AES加密
            String largeText = &quot;这是一段比较长的数据，适合使用AES进行加密处理，因为AES速度更快。&quot;;
            String aesEncryptedText = aesEncrypt(largeText, aesKey, iv);
            System.out.println(&quot;AES加密后: &quot; + aesEncryptedText);

            // 4. AES解密
            String aesDecryptedText = aesDecrypt(aesEncryptedText, aesKey, iv);
            System.out.println(&quot;AES解密后: &quot; + aesDecryptedText);
            System.out.println(&quot;AES加解密是否成功: &quot; + largeText.equals(aesDecryptedText));

            // ==================== 混合加密示例 ====================
            System.out.println(&quot;\n=============== 混合加密示例 ===============&quot;);
            String businessData = &quot;{\&quot;userId\&quot;: 123, \&quot;orderId\&quot;: \&quot;SN20240728\&quot;, \&quot;amount\&quot;: 999.99}&quot;;
            System.out.println(&quot;原始业务数据: &quot; + businessData);

            // 1. 生成临时的AES密钥和IV
            SecretKey sessionAesKey = generateAesKey(256);
            IvParameterSpec sessionIv = generateIv();

            // 2. 使用AES加密业务数据
            String encryptedBusinessData = aesEncrypt(businessData, sessionAesKey, sessionIv);
            System.out.println(&quot;AES加密后的业务数据: &quot; + encryptedBusinessData);

            // 3. 使用RSA公钥加密AES密钥
            String aesKeyString = aesKeyToString(sessionAesKey);
            String encryptedAesKey = rsaEncrypt(aesKeyString, rsaPublicKey);
            System.out.println(&quot;RSA加密后的AES密钥: &quot; + encryptedAesKey);

            // 4. [网络传输] 将 encryptedAesKey, encryptedBusinessData, 和 IV 发送给后端...

            // 5. [后端接收] 后端使用RSA私钥解密AES密钥
            String decryptedAesKeyString = rsaDecrypt(encryptedAesKey, rsaPrivateKey);
            SecretKey restoredAesKey = stringToAesKey(decryptedAesKeyString);
            System.out.println(&quot;RSA解密后的AES密钥与原始AES密钥是否一致: &quot; + aesKeyString.equals(decryptedAesKeyString));


            // 6. 后端使用解密后的AES密钥解密业务数据
            String decryptedBusinessData = aesDecrypt(encryptedBusinessData, restoredAesKey, sessionIv);
            System.out.println(&quot;AES解密后的业务数据: &quot; + decryptedBusinessData);
            System.out.println(&quot;混合加密流程是否成功: &quot; + businessData.equals(decryptedBusinessData));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Base64 编码在加密中的作用&lt;/h3&gt;
&lt;p&gt;🤔 为什么要转为 Base64？&lt;/p&gt;
&lt;p&gt;核心原因：二进制数据传输问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 原始密钥是什么样的？
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();

// 获取原始字节数据
byte[] publicKeyBytes = publicKey.getEncoded();
System.out.println(&quot;原始字节数据: &quot; + Arrays.toString(publicKeyBytes));
// 输出：[48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1...]
// 这是二进制数据，包含不可打印字符！

// 如果直接转字符串会怎样？
String badString = new String(publicKeyBytes);
System.out.println(&quot;直接转字符串: &quot; + badString);
// 输出：0☺&quot;0 ♪*☻H☻ù ♪♪☺♪☺☺♪♪○...乱码！❌
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据传输的挑战&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 网络传输场景
const request = {
  publicKey: &apos;0☺&quot;0\r\u0006\t*☻H☻ù\r\u0001\u0001...&apos;, // ❌ 包含控制字符
};

// JSON序列化会出错
JSON.stringify(request); // ❌ 可能报错或丢失数据

// HTTP传输也会有问题
fetch(&quot;/api/keys&quot;, {
  body: JSON.stringify(request), // ❌ 特殊字符可能被破坏
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🎯 Base64 解决了什么问题？&lt;/p&gt;
&lt;p&gt;Base64 的特点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base64字符集：A-Z, a-z, 0-9, +, /（64个字符）
- 所有字符都是可打印的
- 不包含控制字符
- 安全传输通过HTTP、JSON、XML等文本协议
- 不会被各种系统误解或破坏
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编码前后对比&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 编码前：二进制字节
byte[] keyBytes = {48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13};
System.out.println(&quot;二进制: &quot; + Arrays.toString(keyBytes));
// 输出：[48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13]

// 编码后：Base64字符串
String base64String = Base64.encodeBase64String(keyBytes);
System.out.println(&quot;Base64: &quot; + base64String);
// 输出：MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
// 全部是可打印字符！✅
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Java Keystore&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Java Keystore&lt;/code&gt; (密钥库)，顾名思义，是一个用来&lt;strong&gt;存储和管理密钥&lt;/strong&gt;（Keys）和证书（Certificates）的安全容器。&lt;/p&gt;
&lt;p&gt;你可以把它想象成一个加密的保险箱。这个保险箱本身有一个主密码（storepass），用来打开它。保险箱里面可以放很多个带锁的小盒子，每个小盒子都装着敏感的东西（比如私钥或证书），并且每个小盒子也可以有自己独立的小锁密码（keypass）。&lt;/p&gt;
&lt;p&gt;核心特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;是一个文件：它在物理上就是一个文件，常见后缀有.jks, .p12, .pfx。&lt;/li&gt;
&lt;li&gt;内容是加密的：没有正确的密码，无法查看或使用里面的内容。&lt;/li&gt;
&lt;li&gt;结构化存储：内部通过别名（alias）来唯一标识和区分每一个存储的条目（Entry）。&lt;/li&gt;
&lt;li&gt;Java 原生支持：Java 的 java.security 包提供了完整的 API 来创建、加载和操作 Keystore。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;🎯 &lt;strong&gt;为什么要用它？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在软件开发中，尤其是涉及到网络通信和数据安全时，我们不可避免地要处理各种密钥和证书。直接将这些敏感信息硬编码在代码里或明文存放在配置文件中是极其危险的。Keystore 的出现就是为了解决这个问题。&lt;/p&gt;
&lt;p&gt;使用 Keystore 的核心目的：为了安全、统一地管理密钥和证书。&lt;/p&gt;
&lt;p&gt;简单来说，不用 Keystore 就像把家里的钥匙直接挂在门上；用 Keystore 就像把钥匙锁在保险箱里&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;keytool -genkeypair \
  -alias spring_admin \
  -keyalg RSA \
  -keysize 2048 \
  -keystore zzy.jks \
  -validity 3650 \
  -storepass &apos;%U4t#N7k!Bv2^Ec9*Zr0@Hx5*Mp3qJw&apos; \
  -keypass &apos;%U4t#N7k!Bv2^Ec9*Zr0@Hx5*Mp3qJw&apos; \
  -dname &quot;CN=zzyang.top, OU=Tech, O=zzyang Inc, L=NanJing, ST=NanJing, C=CN&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参数说明&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-genkeypair&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：生成密钥对（包含公钥和私钥）&lt;/p&gt;
&lt;p&gt;用途：是创建证书的基本命令&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;-alias spring_admin&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：指定密钥对的别名&lt;/p&gt;
&lt;p&gt;用途：用于在密钥库中标识和访问该密钥对&lt;/p&gt;
&lt;p&gt;实践建议：使用有意义的名称，如项目名或域名&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;-keyalg RSA&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：指定加密算法为 RSA&lt;/p&gt;
&lt;p&gt;用途：RSA 是一种非对称加密算法，广泛用于安全通信&lt;/p&gt;
&lt;p&gt;替代选项：也可以使用 DSA 或 EC 算法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;-keysize 2048&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：指定密钥长度为 2048 位&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;用途：决定加密强度&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;安全建议：2048 位是当前推荐的最小长度 可以使用 4096 位获得更高安全性 不建议使用低于 2048 位的密钥长度&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;-keystore zzy.jks&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：指定生成的密钥库文件名&lt;/p&gt;
&lt;p&gt;用途：存储证书和密钥的容器&lt;/p&gt;
&lt;p&gt;文件格式： .jks：Java 密钥库格式 也可以使用 .p12 (PKCS12 格式)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;-validity 3650&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：设置证书有效期为 3650 天（10 年）&lt;/p&gt;
&lt;p&gt;用途：确定证书的生命周期&lt;/p&gt;
&lt;p&gt;建议： 开发环境可以设置较长时间 生产环境建议 1-2 年，定期更新&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;-storepass &apos;%U4t#N7k!Bv2^Ec9Zr0@Hx5Mp3qJw&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：设置密钥库的访问密码&lt;/p&gt;
&lt;p&gt;用途：保护密钥库的安全&lt;/p&gt;
&lt;p&gt;安全建议： 使用强密码 安全保存密码 生产环境应使用密码管理系统&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;-keypass &apos;%U4t#N7k!Bv2^Ec9Zr0@Hx5Mp3qJw&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：设置密钥对的访问密码&lt;/p&gt;
&lt;p&gt;用途：保护私钥的安全&lt;/p&gt;
&lt;p&gt;注意：通常与 storepass 设置相同值以简化管理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;-dname &quot;CN=zzyang.top, OU=Tech, O=zzyang Inc, L=NanJing, ST=NanJing, C=CN&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：设置证书主题信息&lt;/p&gt;
&lt;p&gt;各字段含义：
CN (Common Name)：域名&lt;/p&gt;
&lt;p&gt;OU (Organizational Unit)：组织单位&lt;/p&gt;
&lt;p&gt;O (Organization)：组织名称&lt;/p&gt;
&lt;p&gt;L (Locality)：城市&lt;/p&gt;
&lt;p&gt;ST (State)：省份/州&lt;/p&gt;
&lt;p&gt;C (Country)：国家代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;使用 Keystore 的注意事项&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keystore 文件本身的保护&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;不要提交到版本控制系统 (Git)：这是最最重要的一条！必须将.jks, .p12 等文件添加到.gitignore 中。&lt;/li&gt;
&lt;li&gt;严格控制文件权限：在服务器上，设置 Keystore 文件的权限，确保只有运行应用程序的用户才能读取它（例如，chmod 400 your_keystore.jks）。&lt;/li&gt;
&lt;li&gt;安全存放：不要将 Keystore 文件放在 Web 服务器的根目录或其他可被公开访问的路径下。应放在配置目录或专门的安全目录下（如/etc/certs/）。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;密码管理 (最关键的环节)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;使用强密码：为 Keystore（storepass）和私钥条目（keypass）设置复杂的、无规律的强密码。&lt;/li&gt;
&lt;li&gt;不要硬编码密码：绝对不要在 application.yml 或 Java 代码中明文写入密码。&lt;/li&gt;
&lt;li&gt;推荐的密码管理方式：
&lt;ul&gt;
&lt;li&gt;环境变量：通过服务器的环境变量传入密码 (${KEYSTORE_PASSWORD})。这是最简单、最常用的方式。&lt;/li&gt;
&lt;li&gt;配置中心：使用如 Nacos, Apollo, Spring Cloud Config 等配置中心来管理密码。&lt;/li&gt;
&lt;li&gt;Docker Secrets / Kubernetes Secrets：在容器化环境中，使用编排工具提供的 Secrets 管理机制。&lt;/li&gt;
&lt;li&gt;云服务 KMS/Vault：使用云厂商提供的密钥管理服务或 HashiCorp Vault 来管理密码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;密钥和证书的生命周期管理&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;定期轮换 (Rotation)：制定策略定期更换 Keystore 中的密钥和证书，以降低因密钥泄露造成的长期风险。&lt;/li&gt;
&lt;li&gt;备份与恢复：建立 Keystore 文件的备份和恢复机制。如果文件损坏或丢失，且没有备份，所有依赖它的加密/签名功能都会瘫痪。&lt;/li&gt;
&lt;li&gt;记录信息：记录好每个 Keystore 中每个别名（alias）对应的密钥用途、有效期等元数据，方便维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;选择合适的 Keystore 类型&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JKS (.jks)&lt;/code&gt;: Java 的传统格式，兼容性好，但功能有限（如默认不支持存储对称密钥）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PKCS12 (.p12, .pfx)&lt;/code&gt;: 推荐使用。这是一个国际标准，具有更好的跨平台兼容性，可以被 Java, .NET, Python, OpenSSL 等大多数工具和语言识别。它也能存储私钥、证书和对称密钥。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JCEKS&lt;/code&gt;: 如果你需要存储对称密钥（如 AES 密钥），这是一个不错的选择，比 JKS 更强大。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是什么：一个加密的文件保险箱，用于安全存储密钥和证书。&lt;/li&gt;
&lt;li&gt;为什么用：为了安全（避免明文密钥）、集中管理（避免密钥散落）、解耦（更换密钥不改代码）和标准化。&lt;/li&gt;
&lt;li&gt;注意事项：保护好文件本身（别上传 Git），保护好密码（别硬编码），做好备份和轮换，并选择合适的格式（推荐 PKCS12）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;jwt 联合&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;application.yml&lt;/code&gt;进行配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring-admin:
  jwt:
    location: classpath:zzy.jks
    alias: spring_admin
    password: &quot;%U4t#N7k!Bv2^Ec9*Zr0@Hx5*Mp3qJw&quot;
    expiration: 2 # 访问令牌过期时间（小时）
    refresh: 168 # 刷新令牌过期时间（小时，7天）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 config 中配置类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
@Component
@ConfigurationProperties(prefix = &quot;spring-admin.jwt&quot;)
public class JwtProperties {

    /**
     * 密钥库 (JKS) 文件的位置, Spring Boot 会自动解析 classpath: 或 file:
     */
    private Resource location;
    /**
     * 密钥库中密钥条目的别名
     */
    private String alias;
    /**
     * 密钥库和私钥的密码
     */
    private String password;

    /**
     * JWT 访问令牌过期时间（小时）
     */
    private Integer expiration = 2;

    /**
     * JWT 刷新令牌过期时间（小时）
     */
    private Integer refresh = 168;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;jwt 工具类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@Component
public class JwtUtil {
    @Autowired
    private JwtProperties jwtProperties; // 注入JKS配置

    private PrivateKey privateKey; // 用于签名的私钥
    private PublicKey publicKey;   // 用于验签的公钥


    /**
     * JWT 签名算法
     */
    private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256;  // 使用RS256算法

    /**
     * JWT 中用户ID的键名
     */
    private static final String USER_ID_KEY = &quot;userId&quot;;

    /**
     * JWT 中用户名的键名
     */
    private static final String USERNAME_KEY = &quot;username&quot;;

    /**
     * JWT 中用户名的键名
     */
    private static final String NICKNAME_KEY = &quot;nickname&quot;;

    /**
     * JWT 中令牌类型的键名
     */
    private static final String TOKEN_TYPE_KEY = &quot;type&quot;;
    /**
     * JWT 中访问令牌类型的值
     */
    private static final String ACCESS_TOKEN_TYPE = &quot;access&quot;;
    /**
     * JWT 中刷新令牌类型的值
     */
    private static final String REFRESH_TOKEN_TYPE = &quot;refresh&quot;;




    /**
     * 初始化方法，在Bean创建后执行
     */
    @PostConstruct
    public void init() {
        try {
            Resource resource = jwtProperties.getLocation();
            String password = jwtProperties.getPassword();
            String alias = jwtProperties.getAlias();

            if (resource == null || !resource.exists()) {
                throw new IllegalStateException(&quot;JWT密钥库文件未找到，请检查配置: &quot; + jwtProperties.getLocation());
            }

            KeyStore keyStore = KeyStore.getInstance(&quot;JKS&quot;);
            try (InputStream is = resource.getInputStream()) {
                keyStore.load(is, password.toCharArray());
            }

            // 从密钥库中获取私钥和公钥
            this.privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
            this.publicKey = keyStore.getCertificate(alias).getPublicKey();

            if (this.privateKey == null || this.publicKey == null) {
                throw new IllegalStateException(&quot;在JKS文件中找不到别名为 &apos;&quot; + alias + &quot;&apos; 的密钥对&quot;);
            }

            // log.info(&quot;JWT工具类初始化成功，使用RS256非对称加密。&quot;);

        } catch (Exception e) {
            log.error(&quot;初始化JWT工具类失败，无法加载JKS密钥库&quot;, e);
            // 抛出运行时异常，使服务启动失败，以便及时发现配置问题
            throw new RuntimeException(&quot;初始化JWT工具类失败，请检查JKS配置&quot;, e);
        }
    }

      /**
     * 生成 JWT 访问令牌
     */
    public String generateAccessToken(Long userId, String username, String nickname) {
        return generateToken(userId, username, nickname, jwtProperties.getExpiration(), ACCESS_TOKEN_TYPE);
    }

    /**
     * 生成 JWT 刷新令牌
     */
    public String generateRefreshToken(Long userId, String username, String nickname) {
        return generateToken(userId, username, nickname, jwtProperties.getRefresh(), REFRESH_TOKEN_TYPE);
    }

    /**
     * 通用令牌生成方法
     */
    private String generateToken(Long userId, String username, String nickname, Integer ttl, String tokenType) {
        if (userId == null || username == null || username.trim().isEmpty()) {
            throw new AuthException(&quot;用户ID和用户名不能为空&quot;);
        }

        try {
            Date now = new Date();
            Date expiration = new Date(now.getTime() + ttl * 60 * 60 * 1000);

            Map&amp;lt;String, Object&amp;gt; claims = new HashMap&amp;lt;&amp;gt;();
            claims.put(USER_ID_KEY, userId);
            claims.put(USERNAME_KEY, username);
            claims.put(NICKNAME_KEY, nickname);
            claims.put(TOKEN_TYPE_KEY, tokenType);

            return Jwts.builder()
                    .setClaims(claims)
                    .setSubject(username)
                    .setIssuedAt(now)
                    .setExpiration(expiration)
                    .signWith(this.privateKey, SIGNATURE_ALGORITHM) //  ⬅️使用私钥和RS256签名
                    .compact();

        } catch (Exception e) {
            log.error(&quot;生成JWT失败，用户ID: {}, 用户名: {}&quot;, userId, username, e);
            throw new AuthException(&quot;生成令牌失败&quot;);
        }
    }


    /**
     * 解析 JWT 令牌
     *
     * @param token JWT令牌
     * @return Claims对象，包含令牌中的所有信息
     * @throws AuthException 当令牌无效、过期或解析失败时抛出
     */
    public Claims parseToken(String token) {
        if (StrUtil.isBlank(token)) {
            throw new AuthException(&quot;令牌不能为空&quot;);
        }

        try {
            return Jwts.parserBuilder()
                    .setSigningKey(this.publicKey)  // ⬅️
                    .build()
                    .parseClaimsJws(token)
                    .getBody();

        } catch (ExpiredJwtException e) {
            // log.warn(&quot;JWT令牌已过期: {}&quot;, e.getMessage());
            throw new AuthException(&quot;访问令牌已过期，请重新登录&quot;);
        } catch (UnsupportedJwtException e) {
            // log.warn(&quot;不支持的JWT令牌: {}&quot;, e.getMessage());
            throw new AuthException(&quot;不支持的令牌格式&quot;);
        } catch (MalformedJwtException e) {
            // log.warn(&quot;JWT令牌格式错误: {}&quot;, e.getMessage());
            throw new AuthException(&quot;令牌格式错误&quot;);
        } catch (SecurityException e) {
            // log.warn(&quot;JWT令牌签名验证失败: {}&quot;, e.getMessage());
            throw new AuthException(&quot;令牌签名验证失败&quot;);
        } catch (IllegalArgumentException e) {
            // log.warn(&quot;JWT令牌参数无效: {}&quot;, e.getMessage());
            throw new AuthException(&quot;令牌参数无效&quot;);
        } catch (Exception e) {
            log.error(&quot;解析JWT令牌异常&quot;, e);
            throw new AuthException(&quot;令牌解析失败&quot;);
        }
    }

    /**
     * 验证 JWT 令牌是否有效
     *
     * @param token JWT令牌
     * @return true-有效，false-无效
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (AuthException e) {
            // log.debug(&quot;JWT令牌验证失败: {}&quot;, e.getMessage());
            return false;
        }
    }

    /**
     * 从令牌中获取用户ID
     *
     * @param token JWT令牌
     * @return 用户ID
     */
    public Long getUserIdFromToken(String token) {
        Claims claims = parseToken(token);
        Object userId = claims.get(USER_ID_KEY);

        try {
            if (userId instanceof Number) {
                return ((Number) userId).longValue();
            } else if (userId instanceof String) {
                return Long.valueOf((String) userId);
            } else {
                throw new AuthException(&quot;令牌中用户ID类型无效: &quot; + userId.getClass().getName());
            }
        } catch (NumberFormatException e) {
            throw new AuthException(&quot;令牌中用户ID格式错误: &quot; + userId);
        }
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token JWT令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get(USERNAME_KEY, String.class);
    }

    /**
     * 从令牌中获取用户昵称
     *
     * @param token JWT令牌
     * @return 用户昵称
     */
    public String getNicknameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get(NICKNAME_KEY, String.class);
    }


    /**
     * 获取令牌的过期时间
     *
     * @param token JWT令牌
     * @return 过期时间
     */
    public Date getExpirationFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getExpiration();
    }

    /**
     * 获取令牌的签发时间
     *
     * @param token JWT令牌
     * @return 签发时间
     */
    public Date getIssuedAtFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getIssuedAt();
    }

    /**
     * 获取令牌中的所有声明信息
     *
     * @param token JWT令牌
     * @return 包含所有声明的Map
     */
    public Map&amp;lt;String, Object&amp;gt; getAllClaimsFromToken(String token) {
        Claims claims = parseToken(token);
        return new HashMap&amp;lt;&amp;gt;(claims);
    }

    /**
     * 检查令牌类型是否为刷新令牌
     *
     * @param token JWT令牌
     * @return true-是刷新令牌，false-是访问令牌
     */
    public boolean isRefreshToken(String token) {
        try {
            Claims claims = parseToken(token);
            return &quot;refresh&quot;.equals(claims.get(&quot;type&quot;));
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获取Base64编码的公钥字符串
     *
     * @return Base64编码的公钥
     */
    public String getPublicKey() {
        return Base64.getEncoder().encodeToString(this.publicKey.getEncoded());
    }

    /**
     * 获取Base64编码的私钥字符串
     *
     * @return Base64编码的私钥
     */
    public String getPrivateKey() {
        return Base64.getEncoder().encodeToString(this.privateKey.getEncoded());
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;Java8 时间API&lt;/h1&gt;
&lt;p&gt;为什么不推荐使用旧的Date类？❌&lt;/p&gt;
&lt;p&gt;旧Date类的问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 旧的Date类 - 有很多问题
Date date = new Date();
System.out.println(date); // 输出格式难看，时区混乱

// 月份从0开始，容易搞错！
Date date2 = new Date(2024, 12, 25); // 实际上是2025年1月25日！😱

// 线程不安全
SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd&quot;);
// 多线程使用会出错
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程不安全 - &lt;code&gt;Date&lt;/code&gt; 和 &lt;code&gt;Calendar&lt;/code&gt; 在多线程环境下使用时，可能会出现数据不一致的情况。多线程环境容易出错&lt;/li&gt;
&lt;li&gt;API设计糟糕 - 月份从0开始计数&lt;/li&gt;
&lt;li&gt;可变对象 - 容易被意外修改&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;flowchart TB
    API[Java 8 时间API] --&amp;gt; LD[LocalDate 日期]
    API --&amp;gt; LT[LocalTime 时间]
    API --&amp;gt; LDT[LocalDateTime 日期时间]
    API --&amp;gt; Fmt[DateTimeFormatter 格式化]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-07-13_11-15-03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;LocalDate - 日期操作 📅&lt;/h2&gt;
&lt;p&gt;创建LocalDate的语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 语法1：获取今天日期
LocalDate today = LocalDate.now();

// 语法2：创建指定日期 - of(年, 月, 日)
LocalDate specificDate = LocalDate.of(2024, 12, 25);
//                                    参数1  参数2  参数3
//                                    年份   月份   日期

// 语法3：从字符串解析 - parse(&quot;字符串&quot;)
LocalDate parsed = LocalDate.parse(&quot;2024-12-25&quot;);
//                                  必须是这种格式：年-月-日

// 打印结果
System.out.println(today);        // 输出：2024-12-12
System.out.println(specificDate); // 输出：2024-12-25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LocalDate的常用方法和参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LocalDate date = LocalDate.now();

// 获取信息的方法（无参数）
int year = date.getYear();          // 获取年份
int month = date.getMonthValue();   // 获取月份（1-12）
int day = date.getDayOfMonth();     // 获取这个月的第几天
int dayOfYear = date.getDayOfYear(); // 获取这一年的第几天

System.out.println(&quot;年份：&quot; + year);   // 比如：2024
System.out.println(&quot;月份：&quot; + month);  // 比如：12
System.out.println(&quot;日期：&quot; + day);    // 比如：12

// 加减操作（参数是数字）
LocalDate tomorrow = date.plusDays(1);      // 加1天
LocalDate nextWeek = date.plusWeeks(1);     // 加1周
LocalDate nextMonth = date.plusMonths(1);   // 加1个月
LocalDate nextYear = date.plusYears(1);     // 加1年

LocalDate yesterday = date.minusDays(1);    // 减1天
LocalDate lastMonth = date.minusMonths(1);  // 减1个月

// 设置操作（参数是新的值）
LocalDate newDate = date.withYear(2025);        // 设置年份为2025
LocalDate newDate2 = date.withMonth(1);         // 设置月份为1月
LocalDate newDate3 = date.withDayOfMonth(1);    // 设置为当月1号

// 比较操作（参数是另一个LocalDate）
LocalDate otherDate = LocalDate.of(2024, 12, 25);
boolean isBefore = date.isBefore(otherDate);    // 是否在之前
boolean isAfter = date.isAfter(otherDate);      // 是否在之后
boolean isEqual = date.isEqual(otherDate);      // 是否相等
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;LocalTime - 时间操作 ⏰&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 语法1：获取现在时间
LocalTime now = LocalTime.now();

// 语法2：创建指定时间 - of(小时, 分钟)
LocalTime time1 = LocalTime.of(14, 30);        // 14:30
//                              参数1  参数2
//                              小时   分钟

// 语法3：创建指定时间 - of(小时, 分钟, 秒)
LocalTime time2 = LocalTime.of(14, 30, 45);    // 14:30:45
//                              参数1  参数2  参数3
//                              小时   分钟   秒

// 语法4：从字符串解析
LocalTime parsed = LocalTime.parse(&quot;14:30:45&quot;);

// 预定义的时间
LocalTime noon = LocalTime.NOON;        // 正午12:00
LocalTime midnight = LocalTime.MIDNIGHT; // 午夜00:00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LocalTime的常用方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LocalTime time = LocalTime.now();

// 获取信息（无参数）
int hour = time.getHour();      // 获取小时（0-23）
int minute = time.getMinute();  // 获取分钟（0-59）
int second = time.getSecond();  // 获取秒（0-59）

// 加减操作（参数是数字）
LocalTime later = time.plusHours(2);        // 加2小时
LocalTime muchLater = time.plusMinutes(30); // 加30分钟
LocalTime evenLater = time.plusSeconds(45); // 加45秒

LocalTime earlier = time.minusHours(1);     // 减1小时

// 设置操作（参数是新的值）
LocalTime newTime = time.withHour(18);      // 设置为18点
LocalTime newTime2 = time.withMinute(0);    // 设置分钟为0
LocalTime newTime3 = time.withSecond(0);    // 设置秒为0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;LocalDateTime - 日期时间操作 📅⏰&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 语法1：获取现在的日期时间
LocalDateTime now = LocalDateTime.now();

// 语法2：创建指定日期时间 - of(年, 月, 日, 小时, 分钟)
LocalDateTime dateTime1 = LocalDateTime.of(2024, 12, 25, 14, 30);
//                                         参数1  参数2  参数3  参数4  参数5
//                                         年份   月份   日期   小时   分钟

// 语法3：创建指定日期时间 - of(年, 月, 日, 小时, 分钟, 秒)
LocalDateTime dateTime2 = LocalDateTime.of(2024, 12, 25, 14, 30, 45);
//                                         参数1  参数2  参数3  参数4  参数5  参数6
//                                         年份   月份   日期   小时   分钟   秒

// 语法4：从字符串解析
LocalDateTime parsed = LocalDateTime.parse(&quot;2024-12-25T14:30:45&quot;);
//                                         必须是这种格式：年-月-日T小时:分钟:秒

// 语法5：组合LocalDate和LocalTime
LocalDate date = LocalDate.of(2024, 12, 25);
LocalTime time = LocalTime.of(14, 30, 45);
LocalDateTime combined = LocalDateTime.of(date, time);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LocalDateTime的常用方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LocalDateTime dateTime = LocalDateTime.now();

// 获取日期和时间部分
LocalDate datePart = dateTime.toLocalDate();    // 获取日期部分
LocalTime timePart = dateTime.toLocalTime();    // 获取时间部分

// 获取各种信息（继承了LocalDate和LocalTime的所有方法）
int year = dateTime.getYear();
int month = dateTime.getMonthValue();
int day = dateTime.getDayOfMonth();
int hour = dateTime.getHour();
int minute = dateTime.getMinute();
int second = dateTime.getSecond();

// 加减操作（支持所有时间单位）
LocalDateTime tomorrow = dateTime.plusDays(1);      // 加1天
LocalDateTime nextHour = dateTime.plusHours(1);     // 加1小时
LocalDateTime nextMinute = dateTime.plusMinutes(30); // 加30分钟

// 可以链式调用（连续调用多个方法）
LocalDateTime complex = dateTime.plusDays(1)        // 加1天
                               .plusHours(2)         // 再加2小时
                               .plusMinutes(30);     // 再加30分钟
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;时间格式化 - DateTimeFormatter 📝&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 创建格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(&quot;格式模式&quot;);

// 使用格式化器
LocalDateTime now = LocalDateTime.now();
String formatted = now.format(formatter);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;格式模式符号详解&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;y&lt;/td&gt;
&lt;td&gt;年份&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M&lt;/td&gt;
&lt;td&gt;月份&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;d&lt;/td&gt;
&lt;td&gt;日期&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H&lt;/td&gt;
&lt;td&gt;小时(0-23)&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m&lt;/td&gt;
&lt;td&gt;分钟&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s&lt;/td&gt;
&lt;td&gt;秒&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;星期&lt;/td&gt;
&lt;td&gt;星期一&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实际格式化示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LocalDateTime now = LocalDateTime.now();

// 格式1：标准格式
DateTimeFormatter fmt1 = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;);
String result1 = now.format(fmt1);
System.out.println(result1); // 输出：2024-12-12 14:30:45

// 格式2：中文格式
DateTimeFormatter fmt2 = DateTimeFormatter.ofPattern(&quot;yyyy年MM月dd日 HH时mm分ss秒&quot;);
String result2 = now.format(fmt2);
System.out.println(result2); // 输出：2024年12月12日 14时30分45秒

// 格式3：只要日期
DateTimeFormatter fmt3 = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&quot;);
String result3 = now.format(fmt3);
System.out.println(result3); // 输出：2024-12-12

// 格式4：只要时间
DateTimeFormatter fmt4 = DateTimeFormatter.ofPattern(&quot;HH:mm:ss&quot;);
String result4 = now.format(fmt4);
System.out.println(result4); // 输出：14:30:45

// 格式5：美式格式
DateTimeFormatter fmt5 = DateTimeFormatter.ofPattern(&quot;MM/dd/yyyy&quot;);
String result5 = now.format(fmt5);
System.out.println(result5); // 输出：12/12/2024
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;字符串解析（反向操作）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 从格式化的字符串解析回日期时间
String dateString = &quot;2024-12-25 14:30:45&quot;;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;);
LocalDateTime parsed = LocalDateTime.parse(dateString, formatter);
System.out.println(parsed); // 输出：2024-12-25T14:30:45
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;时间戳 - Instant ⏱️&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Instant的语法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 语法1：获取当前UTC时间戳
Instant now = Instant.now();

// 语法2：从毫秒时间戳创建
long timestamp = System.currentTimeMillis();
Instant fromMilli = Instant.ofEpochMilli(timestamp);

// 语法3：从秒时间戳创建
Instant fromSecond = Instant.ofEpochSecond(1640995200);

// 转换为时间戳
long milliTimestamp = now.toEpochMilli();    // 毫秒时间戳
long secondTimestamp = now.getEpochSecond(); // 秒时间戳
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instant与LocalDateTime互转&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// LocalDateTime -&amp;gt; Instant（需要指定时区）
LocalDateTime dateTime = LocalDateTime.now();
Instant instant = dateTime.toInstant(ZoneOffset.ofHours(8)); // 东八区

// Instant -&amp;gt; LocalDateTime（需要指定时区）
Instant instant2 = Instant.now();
LocalDateTime dateTime2 = LocalDateTime.ofInstant(instant2, ZoneId.systemDefault());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;时间计算 🧮&lt;/h2&gt;
&lt;p&gt;Duration - 计算时间差&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 语法：Duration.between(开始时间, 结束时间)
LocalDateTime start = LocalDateTime.of(2024, 12, 25, 14, 0, 0);
LocalDateTime end = LocalDateTime.of(2024, 12, 25, 16, 30, 45);

Duration duration = Duration.between(start, end);

// 获取不同单位的时间差
long hours = duration.toHours();        // 小时数：2
long minutes = duration.toMinutes();    // 分钟数：150
long seconds = duration.getSeconds();   // 秒数：9045

System.out.println(&quot;相差&quot; + hours + &quot;小时&quot;);
System.out.println(&quot;相差&quot; + minutes + &quot;分钟&quot;);
System.out.println(&quot;相差&quot; + seconds + &quot;秒&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Period - 计算日期差&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 语法：Period.between(开始日期, 结束日期)
LocalDate birthDate = LocalDate.of(1990, 5, 15);
LocalDate today = LocalDate.now();

Period period = Period.between(birthDate, today);

// 获取年月日差值
int years = period.getYears();      // 年数
int months = period.getMonths();    // 月数
int days = period.getDays();        // 天数

System.out.println(&quot;年龄：&quot; + years + &quot;年&quot; + months + &quot;个月&quot; + days + &quot;天&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实用方法总结 📋&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;判断和比较方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LocalDate date = LocalDate.now();

// 判断方法（返回boolean）
boolean isLeapYear = date.isLeapYear();        // 是否闰年
boolean isBefore = date.isBefore(otherDate);   // 是否在之前
boolean isAfter = date.isAfter(otherDate);     // 是否在之后
boolean isEqual = date.isEqual(otherDate);     // 是否相等

// 获取长度
int monthLength = date.lengthOfMonth();        // 这个月有几天
int yearLength = date.lengthOfYear();          // 这一年有几天（365或366）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用获取方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LocalDate date = LocalDate.now();

// 获取特殊日期
LocalDate firstDayOfMonth = date.withDayOfMonth(1);                    // 本月第一天
LocalDate lastDayOfMonth = date.withDayOfMonth(date.lengthOfMonth());  // 本月最后一天
LocalDate firstDayOfYear = date.withDayOfYear(1);                      // 本年第一天
LocalDate lastDayOfYear = date.withDayOfYear(date.lengthOfYear());     // 本年最后一天
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆口诀 🎯&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;创建时间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;now()&lt;/code&gt; = 现在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;of()&lt;/code&gt; = 指定&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parse()&lt;/code&gt; = 解析&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;加减时间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;plus&lt;/code&gt; = 加&lt;/li&gt;
&lt;li&gt;&lt;code&gt;minus&lt;/code&gt; = 减&lt;/li&gt;
&lt;li&gt;&lt;code&gt;with&lt;/code&gt; = 设置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;获取信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get&lt;/code&gt; = 获取&lt;/li&gt;
&lt;li&gt;&lt;code&gt;to&lt;/code&gt; = 转换&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比较判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;is&lt;/code&gt; = 判断&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Before/After/Equal&lt;/code&gt; = 前/后/等&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    Start([Java 8&amp;lt;br/&amp;gt;时间API]) --&amp;gt; Basic[基础类]
    Start --&amp;gt; Advanced[时区类]
    Start --&amp;gt; Tools[工具类]

    Basic --&amp;gt; LD[LocalDate&amp;lt;br/&amp;gt;📅 日期]
    Basic --&amp;gt; LT[LocalTime&amp;lt;br/&amp;gt;⏰ 时间]
    Basic --&amp;gt; LDT[LocalDateTime&amp;lt;br/&amp;gt;📅⏰ 日期时间]

    Advanced --&amp;gt; ZDT[ZonedDateTime&amp;lt;br/&amp;gt;🌍 带时区]
    Advanced --&amp;gt; Instant[Instant&amp;lt;br/&amp;gt;⏱️ 时间戳]
    Advanced --&amp;gt; Offset[OffsetDateTime&amp;lt;br/&amp;gt;⚡ 带偏移]

    Tools --&amp;gt; Fmt[DateTimeFormatter&amp;lt;br/&amp;gt;📝 格式化]
    Tools --&amp;gt; Dur[Duration&amp;lt;br/&amp;gt;⏳ 时长]
    Tools --&amp;gt; Per[Period&amp;lt;br/&amp;gt;📆 周期]

    LD --&amp;gt; LDMethod[now&amp;lt;br/&amp;gt;of年月日&amp;lt;br/&amp;gt;plusDays&amp;lt;br/&amp;gt;getYear&amp;lt;br/&amp;gt;&amp;amp;nbsp;]

    LT --&amp;gt; LTMethod[now&amp;lt;br/&amp;gt;of时分秒&amp;lt;br/&amp;gt;plusHours&amp;lt;br/&amp;gt;getHour&amp;lt;br/&amp;gt;&amp;amp;nbsp;]

    LDT --&amp;gt; LDTMethod[now&amp;lt;br/&amp;gt;of完整时间&amp;lt;br/&amp;gt;format&amp;lt;br/&amp;gt;plusDays&amp;lt;br/&amp;gt;&amp;amp;nbsp;]

    Fmt --&amp;gt; FmtMethod[ofPattern&amp;lt;br/&amp;gt;parse&amp;lt;br/&amp;gt;format&amp;lt;br/&amp;gt;&amp;amp;nbsp;]

    Dur --&amp;gt; DurMethod[between&amp;lt;br/&amp;gt;ofHours&amp;lt;br/&amp;gt;toMinutes&amp;lt;br/&amp;gt;&amp;amp;nbsp;]

    Per --&amp;gt; PerMethod[between&amp;lt;br/&amp;gt;ofYears&amp;lt;br/&amp;gt;getYears&amp;lt;br/&amp;gt;&amp;amp;nbsp;]

    style Start fill:#1976d2,stroke:#0d47a1,stroke-width:3px,color:#ffffff
    style Basic fill:#4caf50,stroke:#2e7d32,stroke-width:2px,color:#ffffff
    style Advanced fill:#9c27b0,stroke:#6a1b9a,stroke-width:2px,color:#ffffff
    style Tools fill:#ff9800,stroke:#e65100,stroke-width:2px,color:#ffffff
    style LD fill:#e8f5e8,stroke:#4caf50,stroke-width:2px,color:#1b5e20
    style LT fill:#e8f5e8,stroke:#4caf50,stroke-width:2px,color:#1b5e20
    style LDT fill:#e8f5e8,stroke:#4caf50,stroke-width:2px,color:#1b5e20
    style ZDT fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px,color:#4a148c
    style Instant fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px,color:#4a148c
    style Offset fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px,color:#4a148c
    style Fmt fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100
    style Dur fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100
    style Per fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100
    style LDMethod fill:#f1f8e9,stroke:#689f38,stroke-width:2px,color:#33691e
    style LTMethod fill:#f1f8e9,stroke:#689f38,stroke-width:2px,color:#33691e
    style LDTMethod fill:#f1f8e9,stroke:#689f38,stroke-width:2px,color:#33691e
    style FmtMethod fill:#f1f8e9,stroke:#689f38,stroke-width:2px,color:#33691e
    style DurMethod fill:#f1f8e9,stroke:#689f38,stroke-width:2px,color:#33691e
    style PerMethod fill:#f1f8e9,stroke:#689f38,stroke-width:2px,color:#33691e
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;一对多分页模糊查询解决方案&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一步：分页查询父实体ID列表
只查询 父表，进行分页和排序，目的是获取当前页应该显示的 父实体的ID集合。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二步：根据父实体ID列表，查询所有相关的子实体
使用第一步获取的ID集合，通过 &lt;code&gt;WHERE parent_id IN (...)&lt;/code&gt; 一次性查询出所有相关的子实体。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三步：在应用层（Java代码中）进行数据组装
将第二步查出的子实体列表，根据 parent_id 分配给第一步查出的父实体对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
Start[开始分页查询] --&amp;gt; Step1[第1步: PageHelper分页查询用户ID]
Step1 --&amp;gt; Step2[第2步: 根据用户ID查询用户和订单详情]
Step2 --&amp;gt; Step3[第3步: 组装数据返回PageInfo]

Step1 --&amp;gt; SQL1[SELECT id FROM users
WHERE 条件
ORDER BY create_time
-- PageHelper自动添加LIMIT &amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;]

Step2 --&amp;gt; SQL2[SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id IN 用户ID列表
ORDER BY u.create_time, o.create_time &amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;]

Step3 --&amp;gt; Result[返回正确的分页结果
• 按用户分页
• 每个用户显示完整订单
• count统计准确 &amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;]

style Start fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style Step1 fill:#e8f5e8,stroke:#4caf50,stroke-width:2px
style Step2 fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style Step3 fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
style Result fill:#ffebee,stroke:#f44336,stroke-width:3px
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分页主体：永远是父实体。pageSize 定义的是每页显示多少个父实体。&lt;/p&gt;
&lt;p&gt;让我们用一个具体例子：用户(User) 和 订单(Order) 的一对多关系&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据库表结构&lt;/strong&gt; 🗄️&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 用户表（父表）
CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100),
    phone VARCHAR(20),
    address VARCHAR(200),
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 订单表（子表）
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    order_no VARCHAR(50) NOT NULL,
    product_name VARCHAR(200),
    amount DECIMAL(10,2),
    status TINYINT DEFAULT 1,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id),
    INDEX idx_user_id (user_id),
    INDEX idx_order_no (order_no),
    INDEX idx_product_name (product_name)
);

-- 插入测试数据
INSERT INTO users (username, email, phone, address) VALUES
(&apos;张三&apos;, &apos;zhangsan@email.com&apos;, &apos;13800138000&apos;, &apos;北京市朝阳区&apos;),
(&apos;李四&apos;, &apos;lisi@email.com&apos;, &apos;13800138001&apos;, &apos;上海市浦东新区&apos;),
(&apos;王五&apos;, &apos;wangwu@email.com&apos;, &apos;13800138002&apos;, &apos;广州市天河区&apos;),
(&apos;赵六&apos;, &apos;zhaoliu@email.com&apos;, &apos;13800138003&apos;, &apos;深圳市南山区&apos;);

INSERT INTO orders (user_id, order_no, product_name, amount, status) VALUES
(1, &apos;ORD001&apos;, &apos;iPhone 15手机&apos;, 7999.00, 1),
(1, &apos;ORD002&apos;, &apos;MacBook Pro电脑&apos;, 12999.00, 1),
(1, &apos;ORD003&apos;, &apos;Java编程书籍&apos;, 89.00, 2),
(2, &apos;ORD004&apos;, &apos;耐克运动鞋&apos;, 899.00, 1),
(2, &apos;ORD005&apos;, &apos;Adidas衣服&apos;, 299.00, 1),
(3, &apos;ORD006&apos;, &apos;有机食品大礼包&apos;, 199.00, 1),
(4, &apos;ORD007&apos;, &apos;小米手机&apos;, 2999.00, 1),
(4, &apos;ORD008&apos;, &apos;华为耳机&apos;, 399.00, 2);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 用户实体
@Data
public class User {
    private Long id;
    private String username;
    private String email;
    private String phone;
    private String address;
    private Date createTime;
}

// 订单实体
@Data
public class Order {
    private Long id;
    private Long userId;
    private String orderNo;
    private String productName;
    private BigDecimal amount;
    private Integer status;
    private Date createTime;
}

// 用户订单VO（用于返回给前端）
@Data
public class UserWithOrdersVO {
    private Long userId;
    private String username;
    private String email;
    private String phone;
    private String address;
    private Date userCreateTime;
    private List&amp;lt;OrderVO&amp;gt; orders;
    private Integer orderCount;
    private BigDecimal totalAmount;
}

@Data
public class OrderVO {
    private Long orderId;
    private String orderNo;
    private String productName;
    private BigDecimal amount;
    private Integer status;
    private Date orderCreateTime;
}

// 查询请求对象
@Data
public class UserOrderQueryRequest {
    private String userKeyword;     // 用户搜索关键词
    private String orderKeyword;    // 订单搜索关键词
    private Integer orderStatus;    // 订单状态
    private Date startTime;         // 开始时间
    private Date endTime;           // 结束时间
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Mapper层实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Mapper
public interface UserOrderMapper {

    /**
     * 第1步：分页查询符合条件的用户ID列表
     * 这里PageHelper会自动添加LIMIT和COUNT查询
     */
    @Select(&quot;&amp;lt;script&amp;gt;&quot; +
            &quot;SELECT DISTINCT u.id, u.create_time &quot; +
            &quot;FROM users u &quot; +
            &quot;&amp;lt;if test=&apos;request.orderKeyword != null and request.orderKeyword != \&quot;\&quot;&apos;&amp;gt;&quot; +
            &quot;  INNER JOIN orders o ON u.id = o.user_id &quot; +
            &quot;&amp;lt;/if&amp;gt;&quot; +
            &quot;WHERE 1=1 &quot; +
            &quot;&amp;lt;if test=&apos;request.userKeyword != null and request.userKeyword != \&quot;\&quot;&apos;&amp;gt;&quot; +
            &quot;  AND (u.username LIKE CONCAT(&apos;%&apos;, #{request.userKeyword}, &apos;%&apos;) &quot; +
            &quot;       OR u.email LIKE CONCAT(&apos;%&apos;, #{request.userKeyword}, &apos;%&apos;) &quot; +
            &quot;       OR u.phone LIKE CONCAT(&apos;%&apos;, #{request.userKeyword}, &apos;%&apos;)) &quot; +
            &quot;&amp;lt;/if&amp;gt;&quot; +
            &quot;&amp;lt;if test=&apos;request.orderKeyword != null and request.orderKeyword != \&quot;\&quot;&apos;&amp;gt;&quot; +
            &quot;  AND (o.order_no LIKE CONCAT(&apos;%&apos;, #{request.orderKeyword}, &apos;%&apos;) &quot; +
            &quot;       OR o.product_name LIKE CONCAT(&apos;%&apos;, #{request.orderKeyword}, &apos;%&apos;)) &quot; +
            &quot;&amp;lt;/if&amp;gt;&quot; +
            &quot;&amp;lt;if test=&apos;request.orderStatus != null&apos;&amp;gt;&quot; +
            &quot;  AND o.status = #{request.orderStatus} &quot; +
            &quot;&amp;lt;/if&amp;gt;&quot; +
            &quot;&amp;lt;if test=&apos;request.startTime != null&apos;&amp;gt;&quot; +
            &quot;  AND u.create_time &amp;gt;= #{request.startTime} &quot; +
            &quot;&amp;lt;/if&amp;gt;&quot; +
            &quot;&amp;lt;if test=&apos;request.endTime != null&apos;&amp;gt;&quot; +
            &quot;  AND u.create_time &amp;lt;= #{request.endTime} &quot; +
            &quot;&amp;lt;/if&amp;gt;&quot; +
            &quot;ORDER BY u.create_time DESC&quot; +
            &quot;&amp;lt;/script&amp;gt;&quot;)
    List&amp;lt;UserIdWithCreateTime&amp;gt; selectUserIdsWithPaging(@Param(&quot;request&quot;) UserOrderQueryRequest request);

    /**
     * 第2步：根据用户ID列表查询用户和订单详情
     */
    @Select(&quot;&amp;lt;script&amp;gt;&quot; +
            &quot;SELECT &quot; +
            &quot;    u.id as user_id, u.username, u.email, u.phone, u.address, &quot; +
            &quot;    u.create_time as user_create_time, &quot; +
            &quot;    o.id as order_id, o.order_no, o.product_name, o.amount, &quot; +
            &quot;    o.status, o.create_time as order_create_time &quot; +
            &quot;FROM users u &quot; +
            &quot;LEFT JOIN orders o ON u.id = o.user_id &quot; +
            &quot;WHERE u.id IN &quot; +
            &quot;&amp;lt;foreach collection=&apos;userIds&apos; item=&apos;userId&apos; open=&apos;(&apos; separator=&apos;,&apos; close=&apos;)&apos;&amp;gt;&quot; +
            &quot;    #{userId}&quot; +
            &quot;&amp;lt;/foreach&amp;gt; &quot; +
            &quot;&amp;lt;if test=&apos;request.orderKeyword != null and request.orderKeyword != \&quot;\&quot;&apos;&amp;gt;&quot; +
            &quot;  AND (o.id IS NULL OR &quot; +
            &quot;       o.order_no LIKE CONCAT(&apos;%&apos;, #{request.orderKeyword}, &apos;%&apos;) OR &quot; +
            &quot;       o.product_name LIKE CONCAT(&apos;%&apos;, #{request.orderKeyword}, &apos;%&apos;)) &quot; +
            &quot;&amp;lt;/if&amp;gt;&quot; +
            &quot;&amp;lt;if test=&apos;request.orderStatus != null&apos;&amp;gt;&quot; +
            &quot;  AND (o.id IS NULL OR o.status = #{request.orderStatus}) &quot; +
            &quot;&amp;lt;/if&amp;gt;&quot; +
            &quot;ORDER BY &quot; +
            &quot;o.create_time DESC&quot; +
            &quot;&amp;lt;/script&amp;gt;&quot;)
    @Results({
        @Result(property = &quot;userId&quot;, column = &quot;user_id&quot;),
        @Result(property = &quot;username&quot;, column = &quot;username&quot;),
        @Result(property = &quot;email&quot;, column = &quot;email&quot;),
        @Result(property = &quot;phone&quot;, column = &quot;phone&quot;),
        @Result(property = &quot;address&quot;, column = &quot;address&quot;),
        @Result(property = &quot;userCreateTime&quot;, column = &quot;user_create_time&quot;),
        @Result(property = &quot;orderId&quot;, column = &quot;order_id&quot;),
        @Result(property = &quot;orderNo&quot;, column = &quot;order_no&quot;),
        @Result(property = &quot;productName&quot;, column = &quot;product_name&quot;),
        @Result(property = &quot;amount&quot;, column = &quot;amount&quot;),
        @Result(property = &quot;status&quot;, column = &quot;status&quot;),
        @Result(property = &quot;orderCreateTime&quot;, column = &quot;order_create_time&quot;)
    })
    List&amp;lt;UserOrderRawData&amp;gt; selectUserOrderDetailsByUserIds(
        @Param(&quot;userIds&quot;) List&amp;lt;Long&amp;gt; userIds,
        @Param(&quot;request&quot;) UserOrderQueryRequest request
    );
}

// 辅助类：用于接收用户ID和创建时间
@Data
public class UserIdWithCreateTime {
    private Long id;
    private Date createTime;
}

// 原始数据类：用于接收JOIN查询结果
@Data
public class UserOrderRawData {
    private Long userId;
    private String username;
    private String email;
    private String phone;
    private String address;
    private Date userCreateTime;
    private Long orderId;
    private String orderNo;
    private String productName;
    private BigDecimal amount;
    private Integer status;
    private Date orderCreateTime;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Service层核心实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
@Slf4j
public class UserOrderService {

    @Autowired
    private UserOrderMapper userOrderMapper;

    /**
     * 方案1：完整版 - 支持复杂查询条件
     */
    public PageInfo&amp;lt;UserWithOrdersVO&amp;gt; getUserOrdersWithPageHelper(
            UserOrderQueryRequest request, int pageNum, int pageSize) {

        log.info(&quot;开始分页查询用户订单: pageNum={}, pageSize={}, request={}&quot;, pageNum, pageSize, request);

        // 第1步：使用PageHelper分页查询用户ID
        PageHelper.startPage(pageNum, pageSize);
        List&amp;lt;UserIdWithCreateTime&amp;gt; userIdList = userOrderMapper.selectUserIdsWithPaging(request);
        PageInfo&amp;lt;UserIdWithCreateTime&amp;gt; pageInfo = new PageInfo&amp;lt;&amp;gt;(userIdList);

        log.info(&quot;查询到用户ID数量: {}, 总记录数: {}&quot;, userIdList.size(), pageInfo.getTotal());

        // 如果没有用户，直接返回空结果
        if (userIdList.isEmpty()) {
            return createEmptyPageInfo(pageNum, pageSize);
        }

        // 第2步：根据用户ID查询详细信息
        List&amp;lt;Long&amp;gt; userIds = userIdList.stream()
                .map(UserIdWithCreateTime::getId)
                .collect(Collectors.toList());

        List&amp;lt;UserOrderRawData&amp;gt; rawDataList = userOrderMapper.selectUserOrderDetailsByUserIds(userIds, request);

        // 第3步：数据转换和组装
        List&amp;lt;UserWithOrdersVO&amp;gt; userOrderList = convertRawDataToVO(rawDataList);

        // 第4步：构建最终的PageInfo
        PageInfo&amp;lt;UserWithOrdersVO&amp;gt; result = new PageInfo&amp;lt;&amp;gt;();
        result.setList(userOrderList);
        result.setPageNum(pageInfo.getPageNum());
        result.setPageSize(pageInfo.getPageSize());
        result.setTotal(pageInfo.getTotal());
        result.setPages(pageInfo.getPages());
        result.setSize(userOrderList.size());
        result.setStartRow(pageInfo.getStartRow());
        result.setEndRow(pageInfo.getEndRow());
        result.setIsFirstPage(pageInfo.isIsFirstPage());
        result.setIsLastPage(pageInfo.isIsLastPage());
        result.setHasPreviousPage(pageInfo.isHasPreviousPage());
        result.setHasNextPage(pageInfo.isHasNextPage());
        result.setNavigatePages(pageInfo.getNavigatePages());
        result.setNavigatepageNums(pageInfo.getNavigatepageNums());
        result.setNavigateFirstPage(pageInfo.getNavigateFirstPage());
        result.setNavigateLastPage(pageInfo.getNavigateLastPage());
        result.setPrePage(pageInfo.getPrePage());
        result.setNextPage(pageInfo.getNextPage());

        log.info(&quot;分页查询完成，返回用户数量: {}&quot;, userOrderList.size());
        return result;
    }

    /**
     * 数据转换：将原始数据转换为VO
     */
    private List&amp;lt;UserWithOrdersVO&amp;gt; convertRawDataToVO(List&amp;lt;UserOrderRawData&amp;gt; rawDataList) {
        Map&amp;lt;Long, List&amp;lt;UserOrderRawData&amp;gt;&amp;gt; userDataMap = rawDataList.stream()
                .collect(Collectors.groupingBy(UserOrderRawData::getUserId));

        List&amp;lt;UserWithOrdersVO&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();

        for (Map.Entry&amp;lt;Long, List&amp;lt;UserOrderRawData&amp;gt;&amp;gt; entry : userDataMap.entrySet()) {
            List&amp;lt;UserOrderRawData&amp;gt; userRawDataList = entry.getValue();
            UserOrderRawData firstRecord = userRawDataList.get(0);

            // 构建用户信息
            UserWithOrdersVO userVO = new UserWithOrdersVO();
            userVO.setUserId(firstRecord.getUserId());
            userVO.setUsername(firstRecord.getUsername());
            userVO.setEmail(firstRecord.getEmail());
            userVO.setPhone(firstRecord.getPhone());
            userVO.setAddress(firstRecord.getAddress());
            userVO.setUserCreateTime(firstRecord.getUserCreateTime());

            // 构建订单列表
            List&amp;lt;OrderVO&amp;gt; orders = userRawDataList.stream()
                    .filter(data -&amp;gt; data.getOrderId() != null)
                    .map(this::convertToOrderVO)
                    .collect(Collectors.toList());

            userVO.setOrders(orders);
            userVO.setOrderCount(orders.size());
            userVO.setTotalAmount(calculateTotalAmount(orders));

            result.add(userVO);
        }

        return result;
    }

    private OrderVO convertToOrderVO(UserOrderRawData rawData) {
        OrderVO orderVO = new OrderVO();
        orderVO.setOrderId(rawData.getOrderId());
        orderVO.setOrderNo(rawData.getOrderNo());
        orderVO.setProductName(rawData.getProductName());
        orderVO.setAmount(rawData.getAmount());
        orderVO.setStatus(rawData.getStatus());
        orderVO.setOrderCreateTime(rawData.getOrderCreateTime());
        return orderVO;
    }

    private BigDecimal calculateTotalAmount(List&amp;lt;OrderVO&amp;gt; orders) {
        return orders.stream()
                .map(OrderVO::getAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private PageInfo&amp;lt;UserWithOrdersVO&amp;gt; createEmptyPageInfo(int pageNum, int pageSize) {
        PageInfo&amp;lt;UserWithOrdersVO&amp;gt; pageInfo = new PageInfo&amp;lt;&amp;gt;();
        pageInfo.setList(new ArrayList&amp;lt;&amp;gt;());
        pageInfo.setPageNum(pageNum);
        pageInfo.setPageSize(pageSize);
        pageInfo.setTotal(0);
        pageInfo.setPages(0);
        pageInfo.setSize(0);
        pageInfo.setIsFirstPage(true);
        pageInfo.setIsLastPage(true);
        pageInfo.setHasPreviousPage(false);
        pageInfo.setHasNextPage(false);
        return pageInfo;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心要点&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;两步查询法：先分页查父表ID，再查详情&lt;/li&gt;
&lt;li&gt;正确使用&lt;code&gt;PageHelper&lt;/code&gt;：只在第一个查询使用，立即获取&lt;code&gt;PageInfo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;数据转换：在Service层进行数据组装和转换&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;注意事项&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PageHelper.startPage()&lt;/code&gt; 必须紧跟第一个查询&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;统一返回类封装&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 统一返回结果封装类
 *
 * @author zzy
 * @date 2025/12/4
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result&amp;lt;T&amp;gt; implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 状态码：200-成功，500-失败
     */
    private Integer code;

    /**
     * 返回消息
     */
    private String message;

    /**
     * 返回数据
     */
    private T data;

    /**
     * 成功返回（带数据）
     *
     * @param data 返回的数据
     * @param &amp;lt;T&amp;gt;  数据类型
     * @return Result对象
     */
    public static &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; ok(T data) {
        return new Result&amp;lt;&amp;gt;(200, &quot;操作成功&quot;, data);
    }

    /**
     * 成功返回（无数据）
     *
     * @param &amp;lt;T&amp;gt; 数据类型
     * @return Result对象
     */
    public static &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; ok() {
        return new Result&amp;lt;&amp;gt;(200, &quot;操作成功&quot;, null);
    }

    /**
     * 成功返回（自定义消息）
     *
     * @param message 自定义消息
     * @param &amp;lt;T&amp;gt;     数据类型
     * @return Result对象
     */
    public static &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; ok(String message) {
        return new Result&amp;lt;&amp;gt;(200, message, null);
    }

    /**
     * 成功返回（自定义消息和数据）
     *
     * @param message 自定义消息
     * @param data    返回的数据
     * @param &amp;lt;T&amp;gt;     数据类型
     * @return Result对象
     */
    public static &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; ok(String message, T data) {
        return new Result&amp;lt;&amp;gt;(200, message, data);
    }

    /**
     * 失败返回（带错误消息）
     *
     * @param message 错误消息
     * @param &amp;lt;T&amp;gt;     数据类型
     * @return Result对象
     */
    public static &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; fail(String message) {
        return new Result&amp;lt;&amp;gt;(500, message, null);
    }

    /**
     * 失败返回（带错误消息和数据）
     *
     * @param message 错误消息
     * @param data    返回的数据
     * @param &amp;lt;T&amp;gt;     数据类型
     * @return Result对象
     */
    public static &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; fail(String message, T data) {
        return new Result&amp;lt;&amp;gt;(500, message, data);
    }

    /**
     * 失败返回（自定义状态码和消息）
     *
     * @param code    状态码
     * @param message 错误消息
     * @param &amp;lt;T&amp;gt;     数据类型
     * @return Result对象
     */
    public static &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; fail(Integer code, String message) {
        return new Result&amp;lt;&amp;gt;(code, message, null);
    }

    /**
     * 判断是否成功
     *
     * @return true-成功，false-失败
     */
    public boolean isSuccess() {
        return this.code != null &amp;amp;&amp;amp; this.code == 200;
    }

    /**
     * 判断是否失败
     *
     * @return true-失败，false-成功
     */
    public boolean isFail() {
        return !isSuccess();
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>RabbitMQ</title><link>https://zzyang.top/posts/rabbitmq-a/</link><guid isPermaLink="true">https://zzyang.top/posts/rabbitmq-a/</guid><pubDate>Wed, 21 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;RabbitMQ&lt;/h1&gt;
&lt;h2&gt;初识MQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;同步调用&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;同步调用的优势是什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;时效性强，等待到结果后才返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;同步调用的问题是什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;拓展性差&lt;/li&gt;
&lt;li&gt;性能下降&lt;/li&gt;
&lt;li&gt;级联失败问题&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;异步调用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;异步调用通常是基于消息通知的方式，包含三个角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息发送者：投递消息的人，就是原来的调用者&lt;/li&gt;
&lt;li&gt;消息接收者：接收和处理消息的人，就是原来的服务提供者&lt;/li&gt;
&lt;li&gt;消息代理：管理、暂存、转发消息，你可以把它理解成微信服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;支付服务不再同步调用业务关联度低的服务，而是发送消息通知到Broker。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-22_22-16-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在异步调用中，发送者不再直接同步调用接收者的业务接口，而是发送一条消息投递给消息Broker。然后接收者根据自己的需求从消息Broker那里订阅消息。每当发送方发送消息后，接受者都能获取消息并处理。&lt;/p&gt;
&lt;p&gt;这样，发送消息的人和接收消息的人就完全解耦了。&lt;/p&gt;
&lt;p&gt;除了扣减余额、更新支付流水单状态以外，其它调用逻辑全部取消。而是改为发送一条消息到Broker。而相关的微服务都可以订阅消息通知，一旦消息到达Broker，则会分发给每一个订阅了的微服务，处理各自的业务。&lt;/p&gt;
&lt;p&gt;不管后期增加了多少消息订阅者，作为支付服务来讲，执行问扣减余额、更新支付流水状态后，发送消息即可。业务耗时仅仅是这三部分业务耗时，仅仅100ms，大大提高了业务性能。&lt;/p&gt;
&lt;p&gt;另外，不管是交易服务、通知服务，还是积分服务，他们的业务与支付关联度低。现在采用了异步调用，解除了耦合，他们即便执行过程中出现了故障，也不会影响到支付服务。&lt;/p&gt;
&lt;p&gt;异调用的优势是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;耦合度低，拓展性强&lt;/li&gt;
&lt;li&gt;异步调用，无需等待，性能好&lt;/li&gt;
&lt;li&gt;故障隔离，下游服务故障不影响上游业务&lt;/li&gt;
&lt;li&gt;缓存消息，流量削峰填谷&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;异步调用的问题是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不能立即得到调用结果，时效性差&lt;/li&gt;
&lt;li&gt;不确定下游业务执行是否成功&lt;/li&gt;
&lt;li&gt;业务安全依赖于Broker的可靠性&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MQ技术选型&lt;/h2&gt;
&lt;p&gt;MQ （MessageQueue），中文是消息队列，字面来看就是存放消息的队列。也就是异步调用中的Broker。&lt;/p&gt;
&lt;p&gt;目比较常见的MQ实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ActiveMQ&lt;/li&gt;
&lt;li&gt;RabbitMQ&lt;/li&gt;
&lt;li&gt;RocketMQ&lt;/li&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;几种常见MQ的对比：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;RabbitMQ&lt;/th&gt;
&lt;th&gt;ActiveMQ&lt;/th&gt;
&lt;th&gt;RocketMQ&lt;/th&gt;
&lt;th&gt;Kafka&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;公司/社区&lt;/td&gt;
&lt;td&gt;Rabbit&lt;/td&gt;
&lt;td&gt;Apache&lt;/td&gt;
&lt;td&gt;阿里&lt;/td&gt;
&lt;td&gt;Apache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开发语言&lt;/td&gt;
&lt;td&gt;Erlang&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;Scala&amp;amp;Java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;协议支持&lt;/td&gt;
&lt;td&gt;AMQP，XMPP，SMTP，STOMP&lt;/td&gt;
&lt;td&gt;OpenWire,STOMP，REST,XMPP,AMQP&lt;/td&gt;
&lt;td&gt;自定义协议&lt;/td&gt;
&lt;td&gt;自定义协议&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可用性&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单机吞吐量&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;td&gt;差&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;非常高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;消息延迟&lt;/td&gt;
&lt;td&gt;微秒级&lt;/td&gt;
&lt;td&gt;毫秒级&lt;/td&gt;
&lt;td&gt;毫秒级&lt;/td&gt;
&lt;td&gt;毫秒以内&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;消息可靠性&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;追求可用性：Kafka、 RocketMQ 、RabbitMQ&lt;/li&gt;
&lt;li&gt;追求可靠性：RabbitMQ、RocketMQ&lt;/li&gt;
&lt;li&gt;追求吞吐能力：RocketMQ、Kafka&lt;/li&gt;
&lt;li&gt;追求消息低延迟：RabbitMQ、Kafka&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;据统计，目前国内消息队列使用最多的还是RabbitMQ，再加上其各方面都比较均衡，稳定性也好&lt;/p&gt;
&lt;h2&gt;安装部署rabbitmq&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Docker部署&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;上传我们的&lt;code&gt;mq.tar&lt;/code&gt;,rabbitmq的镜像文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker load -i mq.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行docker命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run \
 -e RABBITMQ_DEFAULT_USER=itheima \
 -e RABBITMQ_DEFAULT_PASS=123321 \
 -v mq-plugins:/plugins \
 --name mq \
 --hostname mq \
 -p 15672:15672 \
 -p 5672:5672 \
 --network hm-net\
 -d \
 rabbitmq:3.8-management

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info
15672是访问控制rabbitmq的控制台&lt;/p&gt;
&lt;p&gt;5672是将来收发消息的端口
:::&lt;/p&gt;
&lt;p&gt;我们访问 http://192.168.146.131:15672  即可看到管理控制台。首次访问需要登录，默认的用户名和密码在配置文件中已经指定了。&lt;/p&gt;
&lt;p&gt;登录后即可看到管理控制台总览页面;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CentOS 7 部署&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Erlang和RabbitMQ版本对照：https://www.rabbitmq.com/which-erlang.html&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先将下载好的文件上传到服务器，创建一个文件夹用来存放文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /usr/rabbitmq
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;然后切换到&lt;code&gt;/usr/rabbitmq&lt;/code&gt;目录，解压安装erlang&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 解压
rpm -Uvh erlang-23.2.7-2.el7.x86_64.rpm

# 安装
yum install -y erlang

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后输入如下指令查看版本号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;erl -v
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;安装RabbitMQ-在RabiitMQ安装过程中需要依赖socat插件，首先安装该插件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;yum install -y socat

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后解压安装RabbitMQ的安装包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 解压
rpm -Uvh rabbitmq-server-3.8.14-1.el7.noarch.rpm

# 安装
yum install -y rabbitmq-server

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动RabbitMQ服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动rabbitmq
systemctl start rabbitmq-server

# 查看rabbitmq状态
systemctl status rabbitmq-server

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显示active则表示服务安装并启动成功&lt;/p&gt;
&lt;p&gt;其他命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 设置rabbitmq服务开机自启动
systemctl enable rabbitmq-server

# 关闭rabbitmq服务
systemctl stop rabbitmq-server

# 重启rabbitmq服务
systemctl restart rabbitmq-server

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装启动RabbitMQ Web管理界面,默认情况下，rabbitmq没有安装web端的客户端软件，需要安装才可以生效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 打开RabbitMQWeb管理界面插件
rabbitmq-plugins enable rabbitmq_management

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加远程用户&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 添加用户
rabbitmqctl add_user 用户名 密码

# 设置用户角色,分配操作权限
rabbitmqctl set_user_tags 用户名 角色

# 为用户添加资源权限(授予访问虚拟机根节点的所有权限)
rabbitmqctl set_permissions -p / 用户名 &quot;.*&quot; &quot;.*&quot; &quot;.*&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建完成后，访问&lt;code&gt;服务器公网ip:15672&lt;/code&gt;进行登录，然后便可进入到后台&lt;/p&gt;
&lt;p&gt;命令示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rabbitmqctl add_user root &apos;1qaz!QAZ&apos;

rabbitmqctl set_user_tags root administrator

rabbitmqctl set_permissions -p / root &quot;.*&quot; &quot;.*&quot; &quot;.*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启用延迟插件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@iZgc7j16wv0j7v97crrcpgZ rabbitmq]# ls
erlang-23.3.4.11-1.el7.x86_64.rpm            rabbitmq-server-3.8.9-1.el7.noarch.rpm
rabbitmq_delayed_message_exchange-3.10.0.ez  socat-1.7.3.2-2.el7.x86_64.rpm
[root@iZgc7j16wv0j7v97crrcpgZ rabbitmq]# find / -name &quot;plugins&quot; -type d 2&amp;gt;/dev/null | grep rabbitmq
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.9/plugins
[root@iZgc7j16wv0j7v97crrcpgZ rabbitmq]#

# 1. 复制插件到正确目录
cp rabbitmq_delayed_message_exchange-3.10.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.9/plugins/

# 2. 验证复制成功
ls -la /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.9/plugins/ | grep delayed

# 3. 启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

# 4. 重启 RabbitMQ
systemctl restart rabbitmq-server

# 5. 查看插件状态
rabbitmq-plugins list | grep delayed
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;RabbitMQ对应的架构如图：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-23_21-09-41.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中包含几个概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;publisher：生产者，也就是发送消息的一方&lt;/li&gt;
&lt;li&gt;consumer：消费者，也就是消费消息的一方&lt;/li&gt;
&lt;li&gt;queue：队列，存储消息。生产者投递的消息会暂存在消息队列中，等待消费者处理&lt;/li&gt;
&lt;li&gt;exchange：交换机，负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。&lt;/li&gt;
&lt;li&gt;virtual host：虚拟主机，起到数据隔离的作用。每个虚拟主机相互独立，有各自的exchange、queue&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;快速入门&lt;/h3&gt;
&lt;p&gt;需求:在RabbitMQ的控制台完成下列操作:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新建队列hello.queue1和hello.queue2&lt;/li&gt;
&lt;li&gt;向默认的amp.fanout交换机发送一条消息&lt;/li&gt;
&lt;li&gt;查看消息是否到达hello.queue1和hello.queue2&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;队列&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们打开Queues选项卡，新建一个队列：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-23_21-24-03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-23_21-24-34.png&quot; alt=&quot;&quot; /&gt;
再以相同的方式，创建一个队列，命名为hello.queue2&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;绑定关系&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;点击Exchanges选项卡，点击amq.fanout交换机，进入交换机详情页，然后点击Bindings菜单，在表单中填写要绑定的队列名称：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-23_21-25-56.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送消息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;再次回到&lt;code&gt;exchange&lt;/code&gt;页面，找到刚刚绑定的&lt;code&gt;amq.fanout&lt;/code&gt;，点击进入详情页，再次发送一条消息&lt;/p&gt;
&lt;p&gt;回到&lt;code&gt;Queues&lt;/code&gt;页面，可以发现&lt;code&gt;hello.queue&lt;/code&gt;中已经有一条消息了&lt;/p&gt;
&lt;p&gt;点击队列名称，进入详情页，查看队列详情，这次我们点击&lt;code&gt;get message&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;可以看到消息到达队列了&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;消息发送的注意事项有哪些?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;交换机只能路由消息，无法存储消息&lt;/li&gt;
&lt;li&gt;交换机只会路由消息给与其绑定的队列，因此队列必须与交
换机绑定&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据隔离&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;用户管理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;点击Admin选项卡，首先会看到RabbitMQ控制台的用户管理界面：&lt;/p&gt;
&lt;p&gt;这里的用户都是RabbitMQ的管理或运维人员。目前只有安装RabbitMQ时添加的&lt;code&gt;itheima&lt;/code&gt;这个用户。仔细观察用户表格中的字段，如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name：&lt;code&gt;itheima&lt;/code&gt;，也就是用户名&lt;/li&gt;
&lt;li&gt;Tags：&lt;code&gt;administrator&lt;/code&gt;，说明&lt;code&gt;itheima&lt;/code&gt;用户是超级管理员，拥有所有权限&lt;/li&gt;
&lt;li&gt;Can access virtual host： /，可以访问的&lt;code&gt;virtual host&lt;/code&gt;，这里的&lt;code&gt;/&lt;/code&gt;是默认的&lt;code&gt;virtual host&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于小型企业而言，出于成本考虑，我们通常只会搭建一套MQ集群，公司内的多个不同项目同时使用。这个时候为了避免互相干扰， 我们会利用&lt;code&gt;virtual host&lt;/code&gt;的隔离特性，将不同项目隔离。一般会做两件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给每个项目创建独立的运维账号，将管理权限分离。&lt;/li&gt;
&lt;li&gt;给每个项目创建不同的&lt;code&gt;virtual host&lt;/code&gt;，将每个项目的数据隔离。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;快速入门案例(Simple模式)&lt;/h2&gt;
&lt;p&gt;SpringAmqp的官方地址：https://spring.io/projects/spring-amqp&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AMQP&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Advanced Message Queuing Protocol，是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关，更符合微服务中独立性的要求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spring AMQP&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Spring AMQP是基于AMQP协议定义的一套API规范，提供了模板来发送和接收消息。包含两部分，其中spring-amqp是基础抽象，spring-rabbit是底层的默认实现。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;需求如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用控制台创建队列simple.queue&lt;/li&gt;
&lt;li&gt;在publisher服务中，利用SpringAMQP直接向simple.queue发送消息&lt;/li&gt;
&lt;li&gt;在consumer服务中，利用SpringAMQP编写消费者，监听simple.queue队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-26_21-52-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;新建一个队列
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-26_22-22-48.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--AMQP依赖，包含RabbitMQ--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-amqp&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置RabbitMQ服务端信息&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;spring:
  rabbitmq:
    host: 192.168.146.131 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123321 # 密码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在test包中建一个消息发送者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSimpleQueue() {
        String queueName = &quot;simple.queue&quot;;
        String message = &quot;Hello Spring AMQP!&quot;;
        rabbitTemplate.convertAndSend(queueName, message);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在项目代码中建一个消息消费者，并注册为bean&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@Slf4j
public class SpringRabbitListener {

    @RabbitListener(queues = &quot;simple.queue&quot;)
    public void listenSimpleQueue(String msg) {
        log.info(&quot;监听到simple.queue的消息:{}&quot;, msg);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WorkQueue(Work模式)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Work queues&lt;/code&gt;，任务模型。简单来说就是让多个消费者绑定到一个队列，共同消费队列中的消息。&lt;/p&gt;
&lt;p&gt;成员：一个生产者，一个队列，多个消费者&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-26_22-27-11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void testWorkQueue() {
    String queueName = &quot;work.queue&quot;;
    for (int i = 1; i &amp;lt;= 50; i++) {
        String message = &quot;Hello Spring AMQP!&quot; + &quot;  &quot; + i;
        rabbitTemplate.convertAndSend(queueName, message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    @RabbitListener(queues = &quot;work.queue&quot;)
    public void listenWorkQueue1(String msg) {
        System.out.println(&quot;消费者1接收到消息：&quot; + msg + &quot;  &quot; + LocalDateTime.now());
    }

    @RabbitListener(queues = &quot;work.queue&quot;)
    public void listenWorkQueue2(String msg) {
        System.err.println(&quot;消费者2接收到消息：&quot; + msg + &quot;  &quot; + LocalDateTime.now());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，RabbitMQ的会将消息依次轮询投递给绑定在队列上的每一个消费者。但这并没有考虑到消费者是否已经处理完消息，可能出现消息堆积。&lt;/p&gt;
&lt;p&gt;因此我们需要修改application.yml，设置preFetch值为1，确保同一时刻最多投递给消费者1条消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  rabbitmq:
    host: 192.168.146.131 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123321 # 密码
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息，处理完成才能获取下一个消息

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;Work模型的使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多个消费者绑定到一个队列，可以加快消息处理速度&lt;/li&gt;
&lt;li&gt;同一条消息只会被一个消费者处理&lt;/li&gt;
&lt;li&gt;通过设置prefetch来控制消费者预取的消息数量，处理完一条再处理下一条，实现能者多劳&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Fanout交换机&lt;/h2&gt;
&lt;p&gt;交换机的作用主要是接收发送者发送的消息，并将消息路由到与其绑定的队列。&lt;/p&gt;
&lt;p&gt;常见交换机的类型有以下三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fanout：广播&lt;/li&gt;
&lt;li&gt;Direct：定向&lt;/li&gt;
&lt;li&gt;Topic：话题&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;Fanout，英文翻译是扇出，我觉得在MQ中叫广播更合适。&lt;/p&gt;
&lt;p&gt;在广播模式下，消息发送流程是这样的：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-26_23-04-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1）  可以有多个队列&lt;/li&gt;
&lt;li&gt;2）  每个队列都要绑定到Exchange（交换机）&lt;/li&gt;
&lt;li&gt;3）  生产者发送的消息，只能发送到交换机&lt;/li&gt;
&lt;li&gt;4）  交换机把消息发送给绑定过的所有队列&lt;/li&gt;
&lt;li&gt;5）  订阅队列的消费者都能拿到消息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现思路如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在RabbitMQ控制台中，声明队列fanout.queue1和fanout.queue2&lt;/li&gt;
&lt;li&gt;在RabbitMQ控制台中，声明交换机hmall.fanout，将两个队列与其绑定&lt;/li&gt;
&lt;li&gt;在consumer服务中，编写两个消费者方法，分别监听fanout.queue1和fanout.queue2&lt;/li&gt;
&lt;li&gt;在publisher中编写测试方法，向hmall.fanout发送消息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-26_23-08-02.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    public void testFanoutQueue() {
        // 队列名称
        String exchangeName = &quot;hmall.fanout&quot;;
        String message = &quot;Hello everyone&quot;;
        // 发送消息，参数分别是:交互机名称、RoutingKey(暂时为空)、消息
        rabbitTemplate.convertAndSend(exchangeName, null, message);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    @RabbitListener(queues = &quot;fanout.queue1&quot;)
    public void listenFanoutQueue1(String msg) {
        log.info(&quot;消费者1监听到fanout.queue1的消息:{}&quot;, msg);
    }

    @RabbitListener(queues = &quot;fanout.queue2&quot;)
    public void listenFanoutQueue2(String msg) {
        log.info(&quot;消费者2监听到fanout.queue2的消息:{}&quot;, msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;交换机的作用是什么?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收publisher发送的消息&lt;/li&gt;
&lt;li&gt;将消息按照规则路由到与之绑定的队列&lt;/li&gt;
&lt;li&gt;FanoutExchange的会将消息路由到每个绑定的队列&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Direct交换机&lt;/h2&gt;
&lt;p&gt;在Fanout模式中，一条消息，会被所有订阅的队列都消费。但是，在某些场景下，我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Direct Exchange&lt;/code&gt; 会将接收到的消息根据规则路由到指定的&lt;code&gt;Queue&lt;/code&gt;，因此称为定向路由。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一个&lt;code&gt;Queue&lt;/code&gt;都与&lt;code&gt;Exchange&lt;/code&gt;设置一个&lt;code&gt;BindingKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;发布者发送消息时，指定消息的&lt;code&gt;RoutingKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Exchange&lt;/code&gt;将消息路由到&lt;code&gt;BindingKey&lt;/code&gt;与消息&lt;code&gt;RoutingKey&lt;/code&gt;一致的队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-27_21-58-51.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;案例需求如图：&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-27_22-09-52.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建两个&lt;code&gt;queue&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;direct.queue1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;direct.queue2&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;创建交换机并绑定：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;hmall.direct&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-27_22-09-17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生产者&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    public void testDirectQueue() {
        String exchangeName = &quot;hmall.direct&quot;;
        String message = &quot;Hello red&quot;;
        rabbitTemplate.convertAndSend(exchangeName, &quot;red&quot;, message);
    }


    @Test
    public void testDirectQueue() {
        String exchangeName = &quot;hmall.direct&quot;;
        String message = &quot;Hello blue&quot;;
        rabbitTemplate.convertAndSend(exchangeName, &quot;blue&quot;, message);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;消费者&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    @RabbitListener(queues = &quot;direct.queue1&quot;)
    public void listenDirectQueue1(String msg) {
        log.info(&quot;消费者1监听到direct.queue1的消息:{}&quot;, msg);
    }

    @RabbitListener(queues = &quot;direct.queue2&quot;)
    public void listenDirectQueue2(String msg) {
        log.info(&quot;消费者2监听到direct.queue2的消息:{}&quot;, msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;描述下Direct交换机与Fanout交换机的差异？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fanout交换机将消息路由给每一个与之绑定的队列&lt;/li&gt;
&lt;li&gt;Direct交换机根据RoutingKey判断路由给哪个队列&lt;/li&gt;
&lt;li&gt;如果多个队列具有相同的RoutingKey，则与Fanout功能类似&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Topic交换机&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Topic&lt;/code&gt;类型的&lt;code&gt;Exchange&lt;/code&gt;与&lt;code&gt;Direct&lt;/code&gt;相比，都是可以根据&lt;code&gt;RoutingKey&lt;/code&gt;把消息路由到不同的队列。&lt;/p&gt;
&lt;p&gt;只不过&lt;code&gt;Topic&lt;/code&gt;类型&lt;code&gt;Exchange&lt;/code&gt;可以让队列在绑定&lt;code&gt;BindingKey&lt;/code&gt; 的时候使用通配符！&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BindingKey&lt;/code&gt; 一般都是有一个或多个单词组成，多个单词之间以&lt;code&gt;.&lt;/code&gt;分割，例如： &lt;code&gt;item.insert&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;通配符规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt;：匹配一个或多个词&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;：匹配不多不少恰好1个词&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;item.#&lt;/code&gt;：能够匹配&lt;code&gt;item.spu.insert&lt;/code&gt; 或者 &lt;code&gt;item.spu&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;item.*&lt;/code&gt;：只能匹配&lt;code&gt;item.spu&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-27_22-55-44.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;利用SpringAMQP演示DirectExchange的使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需求如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在RabbitMQ控制台中，声明队列topic.queue1和topic.queue2&lt;/li&gt;
&lt;li&gt;在RabbitMQ控制台中，声明交换机hmall. topic ，将两个队列与其绑定&lt;/li&gt;
&lt;li&gt;在consumer服务中，编写两个消费者方法，分别监听topic.queue1和topic.queue2&lt;/li&gt;
&lt;li&gt;在publisher中编写测试方法，利用不同的RoutingKey向hmall. topic发送消息&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;创建队列&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;topic.queue1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;topic.queue2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;创建交换机类型为&lt;code&gt;topic&lt;/code&gt;，并绑定队列&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hmall.topic&lt;/code&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-27_23-02-31.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;生产者&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    public void testTopiceQueue() {
        String exchangeName = &quot;hmall.topic&quot;;
        String message = &quot;Hello all&quot;;
        // china.news所有消费者都能收到
        rabbitTemplate.convertAndSend(exchangeName, &quot;china.news&quot;, message);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;消费者&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    @RabbitListener(queues = &quot;topic.queue1&quot;)
    public void listenTopicQueue1(String msg) {
        log.info(&quot;消费者1监听到topic.queue1的消息:{}&quot;, msg);
    }

    @RabbitListener(queues = &quot;topic.queue2&quot;)
    public void listenTopicQueue2(String msg) {
        log.info(&quot;消费者2监听到topic.queue2的消息:{}&quot;, msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;描述下&lt;code&gt;Direct&lt;/code&gt;交换机与&lt;code&gt;Topic&lt;/code&gt;交换机的差异？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Topic&lt;/code&gt;交换机接收的消息&lt;code&gt;RoutingKey&lt;/code&gt;必须是多个单词，以 &lt;code&gt;.&lt;/code&gt;分割&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Topic&lt;/code&gt;交换机与队列绑定时的&lt;code&gt;bindingKey&lt;/code&gt;可以指定通配符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt;：代表0个或多个词&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;：代表1个词&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基于Bean声明队列交换机&lt;/h2&gt;
&lt;p&gt;在之前我们都是基于RabbitMQ控制台来创建队列、交换机。但是在实际开发时，队列和交换机是程序员定义的，将来项目上线，又要交给运维去创建。那么程序员就需要把程序中运行的所有队列和交换机都写下来，交给运维。在这个过程中是很容易出现错误的。&lt;/p&gt;
&lt;p&gt;因此推荐的做法是由程序启动时检查队列和交换机是否存在，如果不存在自动创建。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;SpringAMQP提供了几个类，用来声明队列、交换机及其绑定关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Queue：用于声明队列，可以用工厂类QueueBuilder构建&lt;/li&gt;
&lt;li&gt;Exchange：用于声明交换机，可以用工厂类ExchangeBuilder构建&lt;/li&gt;
&lt;li&gt;Binding：用于声明队列和交换机的绑定关系，可以用工厂类BindingBuilder构建&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-29_20-08-57.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;fanout示例&lt;/h3&gt;
&lt;p&gt;在consumer中创建一个类，声明队列和交换机：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一般可以在消费者这边声明队列、交换机和绑定关系，因为作为发送方来讲，发送方不需要关心队列，发送发唯一关心的是交换机，向某个交换机发消息就可以了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class FanoutConfig {

    @Bean
    public FanoutExchange fanoutExchange(){
//        return new FanoutExchange(&quot;hmall.fanout&quot;);
        return ExchangeBuilder.fanoutExchange(&quot;hmall.fanout&quot;).build();
    }

    @Bean
    public Queue fanoutQueue1() {
        return QueueBuilder.durable(&quot;fanout.queue1&quot;).build();
    }

    @Bean
    public Queue fanoutQueue2() {
        return new Queue(&quot;fanout.queue2&quot;);
    }

    @Bean
    public Binding fanoutQueue1Binding(Queue fanoutQueue1,FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    @Bean
    public Binding fanoutQueue2Binding(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;direct示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;direct模式由于要绑定多个KEY，会非常麻烦，每一个Key都要编写一个binding：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.itheima.consumer.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectConfig {

    /**
     * 声明交换机
     * @return Direct类型交换机
     */
    @Bean
    public DirectExchange directExchange(){
        return ExchangeBuilder.directExchange(&quot;hmall.direct&quot;).build();
    }

    /**
     * 第1个队列
     */
    @Bean
    public Queue directQueue1(){
        return new Queue(&quot;direct.queue1&quot;);
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1WithRed(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with(&quot;red&quot;);
    }
    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1WithBlue(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with(&quot;blue&quot;);
    }

    /**
     * 第2个队列
     */
    @Bean
    public Queue directQueue2(){
        return new Queue(&quot;direct.queue2&quot;);
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2WithRed(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with(&quot;red&quot;);
    }
    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2WithYellow(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with(&quot;yellow&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基于注解声明队列交换机&lt;/h2&gt;
&lt;p&gt;基于&lt;code&gt;@Bean&lt;/code&gt;的方式声明队列和交换机比较麻烦，Spring还提供了基于注解方式来声明。&lt;/p&gt;
&lt;p&gt;例如，我们同样声明Direct模式的交换机和队列：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@Slf4j
public class SpringRabbitListener {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(&quot;direct.queue1&quot;),
            exchange = @Exchange(name = &quot;hmall.exchange&quot;,type = ExchangeTypes.DIRECT),
            key = {&quot;blue&quot;,&quot;red&quot;}
    ))
    public void listenDirectQueue1(String msg) {
        log.info(&quot;消费者1监听到direct.queue1的消息:{}&quot;, msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(&quot;direct.queue2&quot;),
            exchange = @Exchange(name = &quot;hmall.exchange&quot;,type = ExchangeTypes.DIRECT),
            key = {&quot;yellow&quot;,&quot;red&quot;}
    ))
    public void listenDirectQueue2(String msg) {
        log.info(&quot;消费者2监听到direct.queue2的消息:{}&quot;, msg);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再试试Topic模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = &quot;topic.queue1&quot;),
    exchange = @Exchange(name = &quot;hmall.topic&quot;, type = ExchangeTypes.TOPIC),
    key = &quot;china.#&quot;
))
public void listenTopicQueue1(String msg){
    System.out.println(&quot;消费者1接收到topic.queue1的消息：【&quot; + msg + &quot;】&quot;);
}

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = &quot;topic.queue2&quot;),
    exchange = @Exchange(name = &quot;hmall.topic&quot;, type = ExchangeTypes.TOPIC),
    key = &quot;#.news&quot;
))
public void listenTopicQueue2(String msg){
    System.out.println(&quot;消费者2接收到topic.queue2的消息：【&quot; + msg + &quot;】&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;消息转换器&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Spring&lt;/code&gt;的消息发送代码接收的消息体是一个&lt;code&gt;Object&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Spring&lt;/code&gt;的对消息对象的处理是由&lt;code&gt;org.springframework.amqp.support.converter.MessageConverter&lt;/code&gt;来处理的。而默认实现是&lt;code&gt;SimpleMessageConverter&lt;/code&gt;，基于JDK的&lt;code&gt;ObjectOutputStream&lt;/code&gt;完成序列化。&lt;/p&gt;
&lt;p&gt;存在下列问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK的序列化有安全风险&lt;/li&gt;
&lt;li&gt;JDK序列化的消息太大&lt;/li&gt;
&lt;li&gt;JDK序列化的消息可读性差&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;建议采用JSON序列化代替默认的JDK序列化，要做两件事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在publisher和consumer中都要引入jackson依赖：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.fasterxml.jackson.dataformat&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jackson-dataformat-xml&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.9.10&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，如果项目中引入了spring-boot-starter-web依赖，则无需再次引入Jackson依赖。&lt;/p&gt;
&lt;p&gt;配置消息转换器，在publisher和consumer两个服务的启动类中添加一个Bean即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public MessageConverter messageConverter(){
    // 1.定义消息转换器
    Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
    // 2.配置自动创建消息id，用于识别不同消息，也可以在业务中基于ID判断是否是重复消息
    jackson2JsonMessageConverter.setCreateMessageIds(true);
    return jackson2JsonMessageConverter;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;消息转换器中添加的messageId可以便于我们将来做幂等性判断。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-29_21-11-54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;消费者：&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @RabbitListener(queues = &quot;object.queue&quot;)
    public void listenObjectQueue2(Map&amp;lt;String, Object&amp;gt; msg) {
        log.info(&quot;消费者监听到object.queue的消息:{}&quot;, msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;05-29 21:15:53:382  INFO 8660 --- [ntContainer#7-1] c.i.consumer.mq.SpringRabbitListener     : 消费者监听到object.queue的消息:{age=21, name=jack}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;业务集成及改造&lt;/h2&gt;
&lt;p&gt;不管是生产者还是消费者，都需要配置MQ的基本信息。分为两步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;!--消息发送--&amp;gt;
  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-boot-starter-amqp&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;spring:
  rabbitmq:
    host: 192.168.150.101 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123 # 密码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在common的配置类中，配置消息转换器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ConditionalOnClass(RabbitTemplate.class)
public class MqConfig {

    @Bean
    public MessageConverter messageConverter(){
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        return jackson2JsonMessageConverter;
    }

}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;hm-common\src\main\resources\META-INF\spring.factories&lt;/code&gt; 下配置扫描包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.JsonConfig,\
  com.hmall.common.config.MqConfig,\
  com.hmall.common.config.MvcConfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;接收消息&lt;/strong&gt;(消费者)&lt;/p&gt;
&lt;p&gt;建一个&lt;code&gt;listener&lt;/code&gt;包&lt;/p&gt;
&lt;p&gt;在trade-service服务中定义一个消息监听类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class PayStatusListener {

    private final IOrderService orderService;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = &quot;trade.pay.success.queue&quot;, durable = &quot;true&quot;),
            exchange = @Exchange(name = &quot;pay.direct&quot;),
            key = &quot;pay.success&quot;
    ))
    public void listenPaySuccess(Long orderId) {
        orderService.markOrderPaySuccess(orderId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;发送消息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;修改&lt;code&gt;pay-service&lt;/code&gt;服务下的&lt;code&gt;com.hmall.pay.service.impl.PayOrderServiceImpl&lt;/code&gt;类中的&lt;code&gt;tryPayOrderByBalance&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    @Transactional
    public void tryPayOrderByBalance(PayOrderFormDTO payOrderFormDTO) {
        // 1.查询支付单
        PayOrder po = getById(payOrderFormDTO.getId());
        // 2.判断状态
        if (!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())) {
            // 订单不是未支付，状态异常
            throw new BizIllegalException(&quot;交易已支付或关闭！&quot;);
        }
        // 3.尝试扣减余额
        userClient.deductMoney(payOrderFormDTO.getPw(), po.getAmount());
        // 4.修改支付单状态
        boolean success = markPayOrderSuccess(payOrderFormDTO.getId(), LocalDateTime.now());
        if (!success) {
            throw new BizIllegalException(&quot;交易已支付或关闭！&quot;);
        }
        // 5.修改订单状态
//        Order order = new Order();
//        order.setId(po.getBizOrderNo());
//        order.setStatus(2);
//        order.setPayTime(LocalDateTime.now());
        try {
            rabbitTemplate.convertAndSend(&quot;pay.direct&quot;, &quot;pay.success&quot;, po.getBizOrderNo());
        } catch (AmqpException e) {
            log.error(&quot;发送支付状态失败，订单id:{}&quot;, po.getBizOrderNo(), e);
        }
//        tradeClient.markOrderPaySuccess(po.getBizOrderNo());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h1&gt;MQ高级&lt;/h1&gt;
&lt;h2&gt;消息的可靠投递&lt;/h2&gt;
&lt;h3&gt;发送者重连&lt;/h3&gt;
&lt;p&gt;有的时候由于网络波动，可能会出现发送者连接MQ失败的情况。通过配置我们可以开启连接失败后的重连机制:&lt;/p&gt;
&lt;p&gt;在消息的发送者配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  rabbitmq:
    connection-timeout: 1s # 设置MQ的连接超时时间
    template:
      retry:
        enabled: true # 开启超时重试机制
        initial-interval: 1000ms # 失败后的初始等待时间
        multiplier: 1 # 失败后下次的等待时长倍数，下次等待时长 = initial-interval * multiplier
        max-attempts: 3 # 最大重试次数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们利用命令停掉RabbitMQ服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker stop mq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后测试发送一条消息;&lt;/p&gt;
&lt;p&gt;:::warning
当网络不稳定的时候，利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是&lt;strong&gt;阻塞式&lt;/strong&gt;的重试，也就是说多次重试等待的过程中，当前线程是被阻塞的，会影响业务性能。&lt;/p&gt;
&lt;p&gt;如果对于业务性能有要求，建议&lt;strong&gt;禁用&lt;/strong&gt;重试机制。如果一定要使用，请合理配置等待时长和重试次数，当然也可以考虑使用&lt;strong&gt;异步&lt;/strong&gt;线程来执行发送消息的代码。
:::&lt;/p&gt;
&lt;h3&gt;发送者确认&lt;/h3&gt;
&lt;p&gt;一般情况下，只要生产者与MQ之间的网路连接顺畅，基本不会出现发送消息丢失的情况，因此大多数情况下我们无需考虑这种问题。
不过，在少数情况下，也会出现消息发送到MQ之后丢失的现象，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MQ内部处理消息的进程发生了异常&lt;/li&gt;
&lt;li&gt;生产者发送消息到达MQ后未找到Exchange&lt;/li&gt;
&lt;li&gt;生产者发送消息到达MQ的Exchange后，未找到合适的Queue，因此无法路由&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SpringAMQP提供了&lt;code&gt;Publisher Confirm&lt;/code&gt;和&lt;code&gt;Publisher Return&lt;/code&gt;两种确认机制。开启确机制认后，当发送者发送消息给MQ后，MQ会返回确认结果给发送者。返回的结果有以下几种情况:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息投递到了MQ，但是路由失败。此时会通过&lt;code&gt;PublisherReturn&lt;/code&gt;返回路由异常原因，然后返回&lt;code&gt;ACK&lt;/code&gt;，告知投递成功&lt;/li&gt;
&lt;li&gt;临时消息投递到了MQ，并且入队成功，返回&lt;code&gt;ACK&lt;/code&gt;，告知投递成功&lt;/li&gt;
&lt;li&gt;持久消息投递到了MQ，并且入队完成持久化，返回&lt;code&gt;ACK&lt;/code&gt;，告知投递成功&lt;/li&gt;
&lt;li&gt;其它情况都会返回&lt;code&gt;NACK&lt;/code&gt;，告知投递失败&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中&lt;code&gt;ack&lt;/code&gt;和&lt;code&gt;nack&lt;/code&gt;属于&lt;code&gt;Publisher Confirm&lt;/code&gt;机制，&lt;code&gt;ack&lt;/code&gt;是投递成功；&lt;code&gt;nack&lt;/code&gt;是投递失败。而&lt;code&gt;return&lt;/code&gt;则属于&lt;code&gt;Publisher Return&lt;/code&gt;机制。&lt;/p&gt;
&lt;p&gt;默认两种机制都是关闭状态，需要通过配置文件来开启。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;SpringAMQP实现发送者确认🤫&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在publisher模块的application.yaml中添加配置：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;spring:
  rabbitmq:
    publisher-confirm-type: correlated # 开启publisher confirm机制，并设置confirm类型
    publisher-returns: true # 开启publisher return机制
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里&lt;code&gt;publisher-confirm-type&lt;/code&gt;有三种模式可选🤠：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;none&lt;/code&gt;：关闭confirm机制&lt;/li&gt;
&lt;li&gt;&lt;code&gt;simple&lt;/code&gt;：同步阻塞等待MQ的回执&lt;/li&gt;
&lt;li&gt;&lt;code&gt;correlated&lt;/code&gt;：MQ异步回调返回回执&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般我们推荐使用&lt;code&gt;correlated&lt;/code&gt;，回调机制。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义ReturnCallback&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;每个RabbitTemplate只能配置一个ReturnCallback，因此我们可以在配置类中统一设置。我们在publisher模块定义一个配置类：&lt;/p&gt;
&lt;p&gt;在config包下创建&lt;code&gt;MqConfig&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {
    private final RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returned) {
                log.error(&quot;触发return callback,&quot;);
                log.debug(&quot;exchange: {}&quot;, returned.getExchange());
                log.debug(&quot;routingKey: {}&quot;, returned.getRoutingKey());
                log.debug(&quot;message: {}&quot;, returned.getMessage());
                log.debug(&quot;replyCode: {}&quot;, returned.getReplyCode());
                log.debug(&quot;replyText: {}&quot;, returned.getReplyText());
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置类 &lt;code&gt;MqConfig&lt;/code&gt; 中设置了一个消息返回的回调处理机制。当发送的消息因为某些原因未能成功投递到目标队列时（如交换机、路由键不匹配等），&lt;code&gt;rabbitTemplate&lt;/code&gt; 会触发 &lt;code&gt;ReturnedMessage&lt;/code&gt; 回调，可以日志记录详细的错误信息或补偿。&lt;/p&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;定义ConfirmCallback，发送消息，指定消息ID、消息ConfirmCallback&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于每个消息发送时的处理逻辑不一定相同，因此&lt;code&gt;ConfirmCallback&lt;/code&gt;需要在每次发消息时定义。具体来说，是在调用&lt;code&gt;RabbitTemplate&lt;/code&gt;中的&lt;code&gt;convertAndSend&lt;/code&gt;方法时，多传递一个参数：&lt;code&gt;CorrelationData&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这里的&lt;code&gt;CorrelationData&lt;/code&gt;中包含两个核心的东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;：消息的唯一标示，MQ对不同的消息的回执以此做判断，避免混淆&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SettableListenableFuture&lt;/code&gt;：回执结果的&lt;code&gt;Future&lt;/code&gt;对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将来MQ的回执就会通过这个&lt;code&gt;Future&lt;/code&gt;来返回，我们可以提前给&lt;code&gt;CorrelationData&lt;/code&gt;中的&lt;code&gt;Future&lt;/code&gt;添加回调函数来处理消息回执：&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么returnCallback只用写一次配置，而ConfirmCallback需要每次都写?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为消息需要被确认，并且是每条消息都需要被确认; &lt;code&gt;ConfirmCallback&lt;/code&gt;只要记住 临时消息 到了交换机 就&lt;code&gt;ack&lt;/code&gt;；持久化消息时进入了队并完成了消息持久化，才&lt;code&gt;ack&lt;/code&gt;，这就是&lt;code&gt;ConfirmCallback&lt;/code&gt;的作用；&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我们新建一个测试，向系统自带的交换机发送消息，并且添加&lt;code&gt;ConfirmCallback&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    public void testConfirmCallback() {
        CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
        cd.getFuture().addCallback(new ListenableFutureCallback&amp;lt;CorrelationData.Confirm&amp;gt;() {
            @Override
            public void onFailure(Throwable ex) {
                // Future发生异常时的处理逻辑，基本不会触发
                log.error(&quot;send message fail&quot;, ex);
            }

            @Override
            public void onSuccess(CorrelationData.Confirm result) {
                // Future接收到回执的处理逻辑，参数中的result就是回执内容
                if (result.isAck()) { // result.isAck()，boolean类型，true代表ack回执，false 代表 nack回执
                    log.debug(&quot;发送消息成功，收到 ack!&quot;);
                } else { // result.getReason()，String类型，返回nack时的异常描述
                    log.error(&quot;发送消息失败，收到 nack, reason : {}&quot;, result.getReason());
                    // 消息重发或持久化
                }
            }
        });
        String exchangeName = &quot;hmall.topic&quot;;  // 可以把交换机的名字改成不存在，这样就会走到nack
        String message = &quot;Hello all&quot;;
        rabbitTemplate.convertAndSend(exchangeName, &quot;blue&quot;, message, cd);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info
如果没有看到日志：日志级别不够&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
  level:
    com.itheima: debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是test中测试，我们是没有看到回执的，因为test不是正在运行的项目，我们可以在代码结尾添加睡眠时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread.sleep(2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::warning
❗️注意：&lt;/p&gt;
&lt;p&gt;开启生产者确认比较消耗MQ性能，一般不建议开启。而且大家思考一下触发确认的几种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;路由失败：一般是因为RoutingKey错误导致，往往是编程导致&lt;/li&gt;
&lt;li&gt;交换机名称错误：同样是编程错误导致&lt;/li&gt;
&lt;li&gt;MQ内部故障：这种需要处理，但概率往往较低。因此只有对消息可靠性要求非常高的业务才需要开启，而且仅仅需要开启&lt;code&gt;ConfirmCallback&lt;/code&gt;处理&lt;code&gt;nack&lt;/code&gt;就可以了。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;关于Publisher Return&amp;lt;/summary&amp;gt;
Publisher Return机制没必要开，因为路由失败是自己的编程问题导致，而不是mq的内部故障问题
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h3&gt;MQ可靠性&lt;/h3&gt;
&lt;p&gt;在默认情况下，RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一旦MQ宕机，内存中的消息会丢失&lt;/li&gt;
&lt;li&gt;内存空间有限，当消费者故障或处理过慢时，会导致消息积压，引发MQ阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;数据持久化&lt;/h4&gt;
&lt;p&gt;RabbitMQ实现数据持久化包括3个方面:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;交换机持久化&lt;/li&gt;
&lt;li&gt;队列持久化&lt;/li&gt;
&lt;li&gt;消息持久化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在控制台的Exchanges页面，添加交换机时可以配置交换机的Durability参数：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-03_20-25-36.png&quot; alt=&quot;&quot; /&gt;
设置为Durable就是持久化模式，Transient就是临时模式。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;在控制台的Queues页面，添加队列时，同样可以配置队列的Durability参数：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_2025-06-03_202659_754.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;在控制台发送消息的时候，可以添加很多参数，而消息的持久化是要配置一个properties：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-03_20-27-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
    void testSendMessage() {
        // 消息自定义为非持久化
        Message message = MessageBuilder.withBody(&quot;hello springAMQP&quot;.getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT).build();
        for (int i = 0; i &amp;lt; 1000000; i++) {
            rabbitTemplate.convertAndSend(&quot;simple.queue&quot;, message);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;耗时50秒左右⬆️&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testSendMessage() {
        // 消息持久化
        Message message = MessageBuilder.withBody(&quot;hello springAMQP&quot;.getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
        for (int i = 0; i &amp;lt; 1000000; i++) {
            rabbitTemplate.convertAndSend(&quot;simple.queue&quot;, message);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅耗时20秒⬆️&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MessageDeliveryMode.PERSISTENT&lt;/code&gt;：消息会被写入磁盘（持久化），即使 RabbitMQ 重启也不会丢失。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessageDeliveryMode.NON_PERSISTENT&lt;/code&gt;：消息仅保存在内存中，RabbitMQ 重启后会丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;持久化的方式峰值性能是不会到底最低谷的，因为它是边发送边持久化，性能不会有太大影响，而不持久化的方式，一旦消息量过多，内存不够了，就会抽出时间去往磁盘中写入，所以峰值不稳定，性能一般，持久化时性能会达到最低谷&lt;/p&gt;
&lt;p&gt;:::warning
说明：在开启持久化机制以后，如果同时还开启了生产者确认，那么MQ会在消息持久化以后才发送ACK回执，进一步确保消息的可靠性。&lt;/p&gt;
&lt;p&gt;不过出于性能考虑，为了减少IO次数，发送到MQ的消息并不是逐条持久化到数据库的，而是每隔一段时间批量持久化。一般间隔在100毫秒左右，这就会导致ACK有一定的延迟，因此建议生产者确认全部采用异步方式。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;LazyQueue&lt;/h4&gt;
&lt;p&gt;从RabbitMQ的3.6.0版本开始，就增加了LazyQueue的概念，也就是&lt;code&gt;惰性队列&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;惰性队列的特征如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收到消息后直接存入磁盘，不再存储到内存(既可以保证并发能力，也不用去写入内存)&lt;/li&gt;
&lt;li&gt;消费者要消费消息时才会从磁盘中读取并加载到内存(可以提前缓存部分消息到内存，最多2048条)在&lt;code&gt;3.12&lt;/code&gt;版本后，所有队列都是&lt;code&gt;LazyQueue&lt;/code&gt;模式，无法更改。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要设置一个队列为惰性队列，只需要在声明队列时，指定&lt;code&gt;x-queue-mode&lt;/code&gt;属性为&lt;code&gt;lazy&lt;/code&gt;即可:&lt;/p&gt;
&lt;p&gt;控制台方式：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/wechat_2025-06-03_210021_855.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码方式 &amp;amp; 注解方式
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-03_21-01-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对比结果：&lt;/strong&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/wechat_2025-06-03_210525_623.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结🫡&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RabbitMQ&lt;/code&gt;如何保证消息的可靠性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先通过配置可以让交换机、队列、以及发送的消息都持久化。这样队列中的消息会持久化到磁盘，MQ重启消息依然存在。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RabbitMQ&lt;/code&gt;在&lt;code&gt;3.6&lt;/code&gt;版本引入了&lt;code&gt;LazyQueue&lt;/code&gt;，并且在&lt;code&gt;3.12&lt;/code&gt;版本后会称为队列的默认模式。&lt;code&gt;LazyQueue&lt;/code&gt;会将所有消息都持久化。&lt;/li&gt;
&lt;li&gt;开启持久化和生产者确认时，&lt;code&gt;RabbitMQ&lt;/code&gt;只有在消息持久化完成后才会给生产者返回ACK回执&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;消费者的可靠性&lt;/h3&gt;
&lt;p&gt;当RabbitMQ向消费者投递消息以后，需要知道消费者的处理状态如何。因为消息投递给消费者并不代表就一定被正确消费了，可能出现的故障有很多，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息投递的过程中出现了网络故障&lt;/li&gt;
&lt;li&gt;消费者接收到消息后突然宕机&lt;/li&gt;
&lt;li&gt;消费者接收到消息后，因处理不当导致异常&lt;/li&gt;
&lt;li&gt;...
一旦发生上述情况，消息也会丢失。因此，RabbitMQ必须知道消费者的处理状态，一旦消息处理失败才能重新投递消息。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;消费者确认机制&lt;/h4&gt;
&lt;p&gt;消费者确认机制(Consumer Acknowledgement)是为了确认消费者是否成功处理消息。当消费者处理消息结束后应该向&lt;code&gt;RabbitMQ&lt;/code&gt;发送一个回执，告知&lt;code&gt;RabbitMQ&lt;/code&gt;自己消息处理状态:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ack&lt;/code&gt;:成功处理消息，&lt;code&gt;RabbitMQ&lt;/code&gt;从队列中删除该消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nack&lt;/code&gt;:消息处理失败，&lt;code&gt;RabbitMQ&lt;/code&gt;需要再次投递消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reject&lt;/code&gt;:消息处理失败并拒绝该消息，&lt;code&gt;RabbitMQ&lt;/code&gt;从队列中删除该消息&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;code&gt;SpringAMQP&lt;/code&gt;已经实现了消息确认功能。并允许我们通过配置文件选择&lt;code&gt;ACK&lt;/code&gt;处理方式，有三种方式:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;none&lt;/code&gt;:不处理。即消息投递给消费者后立刻ack，消息会立刻从MQ删除。非常不安全，不建议使用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;manual&lt;/code&gt;:手动模式。需要自己在业务代码中调用api，发送&lt;code&gt;ack&lt;/code&gt;或&lt;code&gt;reject&lt;/code&gt;，存在业务入侵，但更灵活&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto&lt;/code&gt;:自动模式。&lt;code&gt;SpringAMQP&lt;/code&gt;利用&lt;code&gt;AOP&lt;/code&gt;对我们的消息处理逻辑做了环绕增强，当业务正常执行时则自动返回&lt;code&gt;ack&lt;/code&gt;当业务出现异常时，根据异常判断返回不同结果:
&lt;ul&gt;
&lt;li&gt;如果是业务异常，会自动返回&lt;code&gt;nack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果是消息处理或校验异常，自动返回&lt;code&gt;reject&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过下面的配置可以修改SpringAMQP的ACK处理方式：&lt;/p&gt;
&lt;p&gt;是在消费者方配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none # 不做处理  auto # 自动ack   manual 手动ack
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;测试&lt;/em&gt;🧑&lt;/p&gt;
&lt;p&gt;在消费者这里故意抛个异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@Slf4j
public class SpringRabbitListener {

    @RabbitListener(queues = &quot;simple.queue&quot;)
    public void listenSimpleQueue(String msg) {
        log.info(&quot;监听到simple.queue的消息:{}&quot;, msg);
        throw new RuntimeException(&quot;故意的&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么就会 回执给rabbitmq &lt;strong&gt;nack&lt;/strong&gt;,队列就会进行重新发送，重新发送到消费者再次尝试消费&lt;/p&gt;
&lt;p&gt;如果抛的是该异常⬇️，那么回执的是 &lt;strong&gt;reject&lt;/strong&gt;，队列就会丢弃消息或发送到死信&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;throw new MessageConversionException(&quot;故意的&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;06-03 21:44:58:825  WARN 10276 --- [ntContainer#3-1] ingErrorHandler$DefaultExceptionStrategy : Fatal message conversion error; message rejected; it will be dropped or routed to a dead letter exchange, if so configured: (Body:&apos;&quot;Hello Spring AMQP!&quot;&apos; MessageProperties [headers={__TypeId__=java.lang.String}, messageId=eb2e4bd1-6077-4419-be95-90ebfb307fae, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=true, receivedExchange=, receivedRoutingKey=simple.queue, deliveryTag=1, consumerTag=amq.ctag-tZhHXR8ze87Bjd1Ye_1Tww, consumerQueue=simple.queue])
06-03 21:44:58:825 ERROR 10276 --- [ntContainer#3-1] o.s.a.r.l.SimpleMessageListenerContainer : Execution of Rabbit message listener failed, and the error handler threw an exception

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;失败重试机制&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;SpringAMQP&lt;/code&gt;提供了消费者失败重试机制，在消费者出现异常时利用本地重试，而不是无限的requeue到mq。我们可以通过在&lt;code&gt;application.yaml&lt;/code&gt;文件中添加配置来开启重试机制：&lt;/p&gt;
&lt;p&gt;在消费者端配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000ms # 初识的失败等待时长为1秒
          multiplier: 1 # 失败的等待时长倍数，下次等待时长 = multiplier * last-interval
          max-attempts: 3 # 最大重试次数
          stateless: true # true无状态；false有状态。如果业务中包含事务，这里改为false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启&lt;code&gt;consumer&lt;/code&gt;服务，重复之前的测试。可以发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消费者在失败后消息没有重新回到MQ无限重新投递，而是在本地重试了3次&lt;/li&gt;
&lt;li&gt;本地重试3次以后，抛出了&lt;code&gt;AmqpRejectAndDontRequeueException&lt;/code&gt;异常。查看&lt;code&gt;RabbitMQ&lt;/code&gt;控制台，发现消息被删除了，说明最后&lt;code&gt;SpringAMQP&lt;/code&gt;返回的是&lt;code&gt;reject&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开启本地重试时，消息处理过程中抛出异常，不会requeue到队列，而是在消费者本地重试&lt;/li&gt;
&lt;li&gt;重试达到最大次数后，Spring会返回&lt;code&gt;reject&lt;/code&gt;，消息会被丢弃&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;失败消息处理策略&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在开启重试模式后，重试次数耗尽，如果消息依然失败，则需要有&lt;code&gt;MessageRecoverer&lt;/code&gt;接口来处理，它包含三种不同的实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RejectAndDontRequeueRecoverer&lt;/code&gt;：重试耗尽后，直接&lt;code&gt;reject&lt;/code&gt;，丢弃消息。默认就是这种方式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ImmediateRequeueMessageRecoverer&lt;/code&gt;：重试耗尽后，返回&lt;code&gt;nack&lt;/code&gt;，消息重新入队&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RepublishMessageRecoverer&lt;/code&gt;：重试耗尽后，将失败消息投递到指定的交换机&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;实现步骤：&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;将失败处理策略改为&lt;code&gt;RepublishMessageRecoverer&lt;/code&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先，定义接收失败消息的交换机、队列及其绑定关系;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后，定义&lt;code&gt;RepublishMessageRecoverer&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在消费者端，&lt;code&gt;config&lt;/code&gt;包下创建&lt;code&gt;ErrorMessageConfiguration&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class ErrorMessageConfiguration {

    @Bean
    public DirectExchange errorExchange() {
        return new DirectExchange(&quot;error.direct&quot;);
    }

    @Bean
    public Queue errorQueue() {
        return new Queue(&quot;error.queue&quot;);
    }

    @Bean
    public Binding errorQueueBinding(Queue errorQueue, DirectExchange errorExchange) {
        return BindingBuilder.bind(errorQueue).to(errorExchange).with(&quot;error&quot;);
    }

    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
        return new RepublishMessageRecoverer(rabbitTemplate, &quot;error.direct&quot;, &quot;error&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次发送，我们发现，重试了三次都失败了，就发送到了&lt;code&gt;error.queue&lt;/code&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-03_22-42-32.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;业务幂等性&lt;/h4&gt;
&lt;p&gt;产生场景：消费者执行完业务后 ，还没有回执就宕机了，结果判断为消息没有确认，还在队列中，再投递给消费者。因此就出现了重复消费的问题；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;幂等&lt;/strong&gt;是一个数学概念，用函数表达式来描述是这样的：&lt;code&gt;f(x) = f(f(x))&lt;/code&gt; 。在程序开发中，则是指同一个业务，执行一次或多次对业务状态的影响是一致的。&lt;/p&gt;
&lt;p&gt;有些业务天生就是幂等的，而有些不是，要根据业务场景区分
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-04_20-39-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;常见的幂等：查询、删除&lt;/p&gt;
&lt;p&gt;常见的非幂等：更新操作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;唯一消息id&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方案一&lt;/strong&gt;，是给每个消息都设置一个唯一id，利用id区分是否是重复消息：&lt;/p&gt;
&lt;p&gt;1️⃣每一条消息都生成一个唯一的id，与消息一起投递给消费者。&lt;/p&gt;
&lt;p&gt;2️⃣消费者接收到消息后处理自己的业务，业务处理成功后将消息ID保存到数据库&lt;/p&gt;
&lt;p&gt;3️⃣如果下次又收到相同消息，去数据库查询判断是否存在，存在则为重复消息放弃处理。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SpringAMQP&lt;/code&gt;的&lt;code&gt;MessageConverter&lt;/code&gt;自带了&lt;code&gt;MessageID&lt;/code&gt;的功能，我们只要开启这个功能即可。&lt;/p&gt;
&lt;p&gt;在消息的发送者，配置如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
    public MessageConverter messageConverter(){
        // 1.定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        // 2.配置自动创建消息id，用于识别不同消息，也可以在业务中基于ID判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在消费者，接收消息时，应使用&lt;code&gt;Message&lt;/code&gt;对象接收&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @RabbitListener(queues = &quot;simple.queue&quot;)
    public void listenSimpleQueue(Message message) {
        log.info(&quot;监听到simple.queue的ID:{}&quot;, message.getMessageProperties().getMessageId());
        log.info(&quot;监听到simple.queue的消息:{}&quot;, new String(message.getBody()));
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;06-04 21:00:06:868  INFO 3344 --- [ntContainer#3-1] c.i.consumer.mq.SpringRabbitListener     : 监听到simple.queue的ID:480f70f0-d658-4447-824b-022c3b8970e3
06-04 21:00:06:869  INFO 3344 --- [ntContainer#3-1] c.i.consumer.mq.SpringRabbitListener     : 监听到simple.queue的消息:&quot;Hello Spring AMQP!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;业务判断&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方案二&lt;/strong&gt;，是结合业务逻辑，基于业务本身做判断。以我们的余额支付业务为例：&lt;/p&gt;
&lt;p&gt;业务判断就是基于业务本身的逻辑或状态来判断是否是重复的请求或消息，不同的业务场景判断的思路也不一样。&lt;/p&gt;
&lt;p&gt;例如我们当前案例中，处理消息的业务逻辑是把订单状态从未支付修改为已支付。因此我们就可以在执行业务时判断订单状态是否是未支付，如果不是则证明订单已经被处理过，无需重复处理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-04_21-05-27.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;改造代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class PayStatusListener {

    private final IOrderService orderService;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = &quot;trade.pay.success.queue&quot;, durable = &quot;true&quot;),
            exchange = @Exchange(name = &quot;pay.direct&quot;),
            key = &quot;pay.success&quot;
    ))
    public void listenPaySuccess(Long orderId) {
        Order order = orderService.getById(orderId);
        // 判断订单状态，是否为未支付
        if (order == null || order.getStatus() != 1) {
            // 不做处理
            return;
        }
        // 标记订单状态为已支付
        orderService.markOrderPaySuccess(orderId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如何保证支付服务与交易服务之间的订单状态一致性？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先，支付服务会正在用户支付成功以后利用MQ消息通知交易服务，完成订单状态同步。&lt;/p&gt;
&lt;p&gt;其次，为了保证MQ消息的可靠性，我们采用了生产者确认机制、消费者确认、消费者失败重试等策略，确保消息投递和处理的可靠性。同时也开启了MQ的持久化，避免因服务宕机导致消息丢失。&lt;/p&gt;
&lt;p&gt;最后，我们还在交易服务更新订单状态时做了业务幂等判断，避免因消息重复消费导致订单状态异常。&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;如果交易服务消息处理失败，有没有什么兜底方案？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以在交易服务设置定时任务，定期查询订单支付状态。这样即便MQ通知失败，还可以利用定时任务作为兜底方案，确保订单支付状态的最终一致性。&lt;/p&gt;
&lt;h3&gt;延迟消息&lt;/h3&gt;
&lt;p&gt;延迟消息：发送者发送消息时指定一个时间，消费者不会立刻收到消息，而是在指定时间之后才收到消息。&lt;/p&gt;
&lt;p&gt;延迟任务：设置在一定时间之后才执行的任务&lt;/p&gt;
&lt;p&gt;在RabbitMQ中实现延迟消息也有两种方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;死信交换机+TTL  (Time To Live，简写TTL)&lt;/li&gt;
&lt;li&gt;延迟消息插件&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;死信交换机&lt;/h4&gt;
&lt;p&gt;当一个队列中的消息满足下列情况之一时，就会成为&lt;code&gt;死信（dead letter）&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消费者使用&lt;code&gt;basic.reject&lt;/code&gt;或 &lt;code&gt;basic.nack&lt;/code&gt;声明消费失败，并且消息的&lt;code&gt;requeue&lt;/code&gt;参数设置为&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;消息是一个过期消息（达到了队列或消息本身设置的过期时间），超时无人消费&lt;/li&gt;
&lt;li&gt;要投递的队列消息堆积满了，最早的消息可能成为死信&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果队列通过&lt;code&gt;dead-letter-exchange&lt;/code&gt;属性指定了一个交换机，那么该队列中的死信就会投递到这个交换机中。这个交换机称为&lt;code&gt;死信交换机（Dead Letter Exchange，简称DLX）&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;绑定关系如下
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-04_21-36-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;消费者监听：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(&quot;dlx.queue&quot;),
            exchange = @Exchange(name = &quot;dlx.direct&quot;, type = ExchangeTypes.DIRECT),
            key = {&quot;hi&quot;}
    ))
    public void listenDlxQueue(String msg) {
        log.info(&quot;消费者2监听到dlx.queue的消息:{}&quot;, msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置我们的关系绑定，&lt;strong&gt;普通交换机与普通队列是不能有消费者的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;config&lt;/code&gt;包下创建&lt;code&gt;NormalConfig&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class NormalConfig {

    @Bean
    public DirectExchange normalExchange() {
        return ExchangeBuilder.directExchange(&quot;normal.direct&quot;).build();
    }

    @Bean
    public Queue normalQueue() {
        return QueueBuilder
                .durable(&quot;normal.queue&quot;)
                .deadLetterExchange(&quot;dlx.direct&quot;)  // 指定死信交换机
                .build();
    }

    @Bean
    public Binding normalExchangeBinding(Queue normalQueue, DirectExchange normalExchange) {
        return BindingBuilder.bind(normalQueue).to(normalExchange).with(&quot;hi&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们只需要向&lt;code&gt;normal.direct&lt;/code&gt;中发消息，该消息就会到达&lt;code&gt;normal.queue&lt;/code&gt;，到达后，由于&lt;code&gt;normal.queue&lt;/code&gt;没有设置消费者，那么这个消息就会成为死信(过期以后)，
成为死信后就会投递到&lt;code&gt;dlx.direct&lt;/code&gt;，从而到达我们刚刚的消费者(listenDlxQueue)；&lt;/p&gt;
&lt;p&gt;发送者：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testSendDelayMessage() {
        rabbitTemplate.convertAndSend(&quot;normal.direct&quot;, &quot;hi&quot;, &quot;hello lalala&quot;, message -&amp;gt; {
            message.getMessageProperties().setExpiration(&quot;10000&quot;);
            return message;
        });
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;死信交换机有什么作用呢？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;收集那些因处理失败而被拒绝的消息&lt;/li&gt;
&lt;li&gt;收集那些因队列满了而被拒绝的消息&lt;/li&gt;
&lt;li&gt;收集因TTL（有效期）到期的消息&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;:::warning
这里的RoutingKey必须一致。死信在转移到死信队列时，他的Routing key也会保存下来。但是如果配置了x-dead-letter-routing-key这个参数的话，routingkey就会被替换为配置的这个值。&lt;/p&gt;
&lt;p&gt;另外，死信在转移到死信队列的过程中，是没有经过消息发送者确认的，所以并不能保证消息的安全性。也就是说，publisher发送了一条消息，但最终consumer在10秒后才收到消息。我们成功实现了延迟消息。
:::&lt;/p&gt;
&lt;h4&gt;延迟消息插件&lt;/h4&gt;
&lt;p&gt;这个插件可以将普通交换机改造为支持延迟消息功能的交换机，当消息投递到交换机后可以暂存一定时间，到期后再投递到队列&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/wechat_2025-06-09_212414_912.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;官方文档说明：&lt;a href=&quot;https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq&quot;&gt;文档&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;插件下载地址：&lt;a href=&quot;https://github.com/rabbitmq/rabbitmq-delayed-message-exchange&quot;&gt;下载地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;由于我们安装的MQ是&lt;code&gt;3.8&lt;/code&gt;版本，因此这里下载&lt;code&gt;3.8.17&lt;/code&gt;版本&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;docker安装&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为我们是基于Docker安装，所以需要先查看RabbitMQ的插件目录对应的数据卷。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker volume inspect mq-plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &quot;CreatedAt&quot;: &quot;2024-06-19T09:22:59+08:00&quot;,
        &quot;Driver&quot;: &quot;local&quot;,
        &quot;Labels&quot;: null,
        &quot;Mountpoint&quot;: &quot;/var/lib/docker/volumes/mq-plugins/_data&quot;,
        &quot;Name&quot;: &quot;mq-plugins&quot;,
        &quot;Options&quot;: null,
        &quot;Scope&quot;: &quot;local&quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;插件目录被挂载到了&lt;code&gt;/var/lib/docker/volumes/mq-plugins/_data&lt;/code&gt;这个目录，我们上传插件到该目录下。&lt;/p&gt;
&lt;p&gt;因为之前部署mq时，已经挂载好了，现在只需上传该插件即可&lt;/p&gt;
&lt;p&gt;接下来执行命令，安装插件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;使用步骤如下&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;消费者&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;两种方式：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-09_21-26-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RabbitListener(bindings = @QueueBinding(
            value = @Queue(&quot;delay.queue&quot;),
            exchange = @Exchange(name = &quot;delay.direct&quot;, delayed = &quot;true&quot;, type = ExchangeTypes.DIRECT),
            key = {&quot;hi&quot;}
    ))
    public void listenDelayQueue(String msg) {
        log.info(&quot;消费者监听到delay.queue的消息:{}&quot;, msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;生产者&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-09_21-27-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    void testSendDelayMessageByPlugins() {
        rabbitTemplate.convertAndSend(&quot;delay.direct&quot;, &quot;hi&quot;, &quot;hello lalala&quot;, message -&amp;gt; {
            message.getMessageProperties().setDelay(10000);
            return message;
        });
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
注意：&lt;/p&gt;
&lt;p&gt;延迟消息插件内部会维护一个本地数据库表，同时使用&lt;code&gt;Elang Timers功能实现计时&lt;/code&gt;。如果消息的延迟时间设置较长，可能会导致堆积的延迟消息非常多，会带来较大的CPU开销，同时延迟消息的时间会存在误差。&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;不建议设置延迟时间过长的延迟消息&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;延时消息计时是由CPU完成的，依赖cpu去完成，所以耗费cpu资源。如果延迟时间设置过长，如一天，那么业务这一天内会产生大量 的延迟消息，带来很大的开销。
:::&lt;/p&gt;
&lt;h4&gt;取消超时订单&lt;/h4&gt;
&lt;p&gt;用户下单完成后，发送15分钟延迟消息，在15分钟后接收消息，检查支付状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已支付：更新订单状态为已支付&lt;/li&gt;
&lt;li&gt;未支付：更新订单状态为关闭订单，恢复商品库存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-09_21-45-23.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-06-09_21-45-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;代码业务改造&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;trade-service&lt;/code&gt;服务中建&lt;code&gt;constans&lt;/code&gt;包，建&lt;code&gt;MQConstans&lt;/code&gt;接口，定义常量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface MQConstans {
    String DELAY_EXCHANGE_NAME = &quot;trade.delay.direct&quot;;
    String DELAY_ORDER_QUEUE_NAME = &quot;trade.delay.order.queue&quot;;
    String DELAY_ORDER_KEY_NAME = &quot;delay.order.query&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;listener&lt;/code&gt;包下的&lt;code&gt;OrderDelayMessageListener&lt;/code&gt;
消费者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class OrderDelayMessageListener {

    private final IOrderService orderService;
    private final PayClient payClient;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = MQConstans.DELAY_ORDER_QUEUE_NAME),
            exchange = @Exchange(name = MQConstans.DELAY_EXCHANGE_NAME, delayed = &quot;true&quot;),
            key = MQConstans.DELAY_ORDER_KEY_NAME
    ))
    public void listenOrderDelayMessage(Long orderId) {
        // 1.查询订单
        Order order = orderService.getById(orderId);
        // 2.检测订单状态，判断是否己支付
        if (order == null &amp;amp;&amp;amp; order.getStatus() != 1) {
            // 订单不存在 或 已支付
            return;
        }
        // 3. 未支付，需要查询支付流水状态
        PayOrderDTO payOrderDTO = payClient.queryPayOrderByBizOrderNo(orderId);
        // 4.判断是否支付
        if (payOrderDTO != null &amp;amp;&amp;amp; payOrderDTO.getStatus() == 3) {
            // 4.1.已支付，标记订单状态为已支付
            orderService.markOrderPaySuccess(orderId);
        } else {
            // 4.2.未支付，取消订单，回复库存
            orderService.cancelOrder(orderId);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rabbitTemplate.convertAndSend(MQConstans.DELAY_EXCHANGE_NAME,
        MQConstans.DELAY_ORDER_KEY_NAME,
        order.getId(),
        message -&amp;gt; {
            message.getMessageProperties().setDelay(10000);
            return message;
        });
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Spring Cloud</title><link>https://zzyang.top/posts/spring-cloud/</link><guid isPermaLink="true">https://zzyang.top/posts/spring-cloud/</guid><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;SpringCloud&lt;/h1&gt;
&lt;h3&gt;&lt;strong&gt;单体架构&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;单体架构:将业务的所有功能集中在一个项目中开发，打成一个包部署&lt;/p&gt;
&lt;p&gt;优点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;架构简单&lt;/li&gt;
&lt;li&gt;部署成本低&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;团队协作成本高&lt;/li&gt;
&lt;li&gt;系统发布效率低&lt;/li&gt;
&lt;li&gt;系统可用性差&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;总结:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;单体架构适合开发功能相对简单，规模较小的项目。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;分布式系统 (Distributed System)&lt;/h3&gt;
&lt;p&gt;分布式系统是一个广泛的概念，包含了许多不同的架构模式，微服务架构只是其中一种。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;微服务 (Microservices)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;可以独立运行和独立部署的服务&lt;/p&gt;
&lt;p&gt;微服务是一种软件架构风格，它是以专注于单一职责的很多小型项目为基础，组合出复杂的大型应用。每个服务运行在自己的进程中；&lt;/p&gt;
&lt;p&gt;是分布式系统理念在应用设计层面的具体实践。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;粒度小&lt;/li&gt;
&lt;li&gt;团队自治&lt;/li&gt;
&lt;li&gt;服务自治&lt;/li&gt;
&lt;li&gt;独立部署&lt;/li&gt;
&lt;li&gt;单一职责&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;SpringCloud&lt;/h3&gt;
&lt;p&gt;Spring Cloud是一个特定的技术框架，用于实现微服务架构。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SpringCloud&lt;/code&gt;是目前国内使用最广泛的微服务框架。&lt;/p&gt;
&lt;p&gt;官网地址：https://spring.io/projects/spring-cloud&lt;/p&gt;
&lt;p&gt;基于Spring Boot的微服务开发工具集，&lt;code&gt;SpringCloud&lt;/code&gt;集成了各种微服务功能组件，并基于SpringBoot实现了这些组件的自动装配，从而提供了良好的开箱即用体验：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s1.imagehub.cc/images/2025/04/28/1d011c0b8f705d58ed7f03f49b542884.png&quot; alt=&quot;image&quot; /&gt;
这三者是层层递进的关系：&lt;code&gt;分布式系统&lt;/code&gt;是基础理论，&lt;code&gt;微服务&lt;/code&gt;是基于分布式系统的架构风格，&lt;code&gt;Spring Cloud&lt;/code&gt;是实现微服务的技术框架。&lt;/p&gt;
&lt;h2&gt;服务拆分&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;什么时候拆分?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创业型项目:先采用单体架构，快速开发，快速试错。随着规模扩大，逐渐拆分。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定的大型项目:资金充足，目标明确，可以直接选择微服务架构，避免后续拆分的麻烦&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;怎么拆分?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从拆分目标来说，要做到:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高内聚:每个微服务的职责要尽量单一，包含的业务相互关联度高、完整度高。&lt;/li&gt;
&lt;li&gt;低耦合:每个微服务的功能要相对独立，尽量减少对其它微服务的依赖，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从拆分方式来说，一般包含两种方式:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;纵向拆分:按照业务模块来拆分&lt;/li&gt;
&lt;li&gt;横向拆分:抽取公共服务，提高复用性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工程结构有两种:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;独立Project&lt;/li&gt;
&lt;li&gt;Maven聚合&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;服务调用&lt;/h2&gt;
&lt;p&gt;把原本本地方法调用，改造成跨微服务的远程调用（RPC，即Remote Produce Call）。&lt;/p&gt;
&lt;h3&gt;RestTemplate&lt;/h3&gt;
&lt;p&gt;Spring给我们提供了一个RestTemplate的API，可以方便的实现Http请求的发送。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用步骤&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@RequiredArgsConstructor


private final RestTemplate restTemplate; // final修饰成员变量，意味着必须初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@RequiredArgsConstructor&lt;/code&gt;会将类的每一个&lt;code&gt;final&lt;/code&gt;字段或者&lt;code&gt;@NonNull&lt;/code&gt;标记字段生成一个构造方法&lt;/p&gt;
&lt;p&gt;调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ResponseEntity&amp;lt;List&amp;lt;ItemDTO&amp;gt;&amp;gt; response = restTemplate.exchange(&quot;http://localhost:8081/items?ids={ids}&quot;,
        HttpMethod.GET,
        null,
        new ParameterizedTypeReference&amp;lt;List&amp;lt;ItemDTO&amp;gt;&amp;gt;() {},
        Map.of(&quot;ids&quot;, CollUtil.join(itemIds, &quot;,&quot;)));
if (!response.getStatusCode().is2xxSuccessful()) {
    return;
}
List&amp;lt;ItemDTO&amp;gt; items = response.getBody();
if (CollUtils.isEmpty(items)) {
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求路径&lt;/li&gt;
&lt;li&gt;请求方式&lt;/li&gt;
&lt;li&gt;请求实体&lt;/li&gt;
&lt;li&gt;返回值类型&lt;/li&gt;
&lt;li&gt;请求参数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java发送http请求可以使用Spring提供的RestTemplate，使用的基本步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注册RestTemplate到Spring容器&lt;/li&gt;
&lt;li&gt;调用RestTemplate的API发送请求，常见方法有：
&lt;ul&gt;
&lt;li&gt;getForObject：发送Get请求并返回指定类型对象&lt;/li&gt;
&lt;li&gt;PostForObject：发送Post请求并返回指定类型对象&lt;/li&gt;
&lt;li&gt;put：发送PUT请求&lt;/li&gt;
&lt;li&gt;delete：发送Delete请求&lt;/li&gt;
&lt;li&gt;exchange：发送任意类型请求，返回ResponseEntity&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;服务注册和发现&lt;/h2&gt;
&lt;p&gt;刚刚手动发送Http请求的方式存在一些问题。&lt;/p&gt;
&lt;p&gt;假如商品微服务被调用较多，为了应对更高的并发，我们进行了多实例部署&lt;/p&gt;
&lt;p&gt;此时，每个&lt;code&gt;item-service&lt;/code&gt;的实例其IP或端口不同，问题来了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;item-service&lt;/code&gt;这么多实例，&lt;code&gt;cart-service&lt;/code&gt;如何知道每一个实例的地址？&lt;/li&gt;
&lt;li&gt;http请求要写url地址，&lt;code&gt;cart-service&lt;/code&gt;服务到底该调用哪个实例呢？&lt;/li&gt;
&lt;li&gt;如果在运行过程中，某一个&lt;code&gt;item-service&lt;/code&gt;实例宕机，&lt;code&gt;cart-service&lt;/code&gt;依然在调用该怎么办？&lt;/li&gt;
&lt;li&gt;如果并发太高，&lt;code&gt;item-service&lt;/code&gt;临时多部署了N台实例，&lt;code&gt;cart-service&lt;/code&gt;如何知道新实例的地址？&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;注册中心原理&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在微服务远程调用的过程中，包括两个角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务提供者：提供接口供其它微服务访问，比如item-service&lt;/li&gt;
&lt;li&gt;服务消费者：调用其它微服务提供的接口，比如cart-service
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-07_20-04-22.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;服务治理中的三个角色分别是什么?&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;服务提供者:暴露服务接口，供其它服务调用&lt;/li&gt;
&lt;li&gt;服务消费者:调用其它服务提供的接口&lt;/li&gt;
&lt;li&gt;注册中心:记录并监控微服务各实例状态，推送服务变更信息&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;消费者如何知道提供者的地址?&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;服务提供者会在启动时注册自己信息到注册中心，消费者可以从注册中心订阅和拉取服务信息&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;消费者如何得知服务状态变更?&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;服务提供者通过心跳机制向注册中心报告自己的健康状态，当心跳异常时注册中心会将异常服务剔除，并通知订阅了该服务的消费者&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;当提供者有多个实例时，消费者该选择哪一个?&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;消费者可以通过负载均衡算法，从多个实例中选择一个&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Nacos注册中心&lt;/h3&gt;
&lt;p&gt;目前开源的注册中心框架有很多，国内比较常见的有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Eureka&lt;/code&gt;：&lt;code&gt;Netflix&lt;/code&gt;公司出品，目前被集成在&lt;code&gt;SpringCloud&lt;/code&gt;当中，一般用于Java应用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Nacos&lt;/code&gt;：&lt;code&gt;Alibaba&lt;/code&gt;公司出品，目前被集成在&lt;code&gt;SpringCloudAlibaba&lt;/code&gt;中，一般用于Java应用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Consul&lt;/code&gt;：&lt;code&gt;HashiCorp&lt;/code&gt;公司出品，目前集成在&lt;code&gt;SpringCloud&lt;/code&gt;中，不限制微服务语言&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上几种注册中心都遵循SpringCloud中的API规范，因此在业务开发使用上没有太大差异。由于Nacos是国内产品，中文文档比较丰富，而且同时具备配置管理功能&lt;/p&gt;
&lt;p&gt;Nacos是目前国内企业中占比最多的注册中心组件。它是阿里巴巴的产品，目前已经加入&lt;code&gt;SpringCloudAlibaba&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;Nacos博客地址：&lt;a href=&quot;https://nacos.io/blog/nacos-gvr7dx_awbbpb_gg16sv97bgirkixe/?spm=5238cd80.2ef5001f.0.0.3f613b7cTCVaJH&quot;&gt;博客&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;搭建&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;执行sql文件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;文件地址：&lt;a href=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/sql/nacos.sql&quot;&gt;地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;表结构如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/sql/Snipaste_2025-05-07_20-44-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;docker中安装nacos&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上传nacos.tar镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker load -i nacos.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;编写并上传&lt;code&gt;custom.env&lt;/code&gt;文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=192.168.146.131
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=123
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&amp;amp;connectTimeout=1000&amp;amp;socketTimeout=3000&amp;amp;autoReconnect=true&amp;amp;useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;serverTimezone=Asia/Shanghai
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;执行&lt;code&gt;nacos&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;访问 http://192.168.146.131:8848/nacos/&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首次访问会跳转到登录页，账号密码都是&lt;code&gt;nacos&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;服务注册&lt;/h3&gt;
&lt;p&gt;注册到Nacos，步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引入依赖&lt;/li&gt;
&lt;li&gt;配置Nacos地址&lt;/li&gt;
&lt;li&gt;重启&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;步骤&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--nacos 服务注册发现--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-discovery&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置Nacos&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;application.yml中添加nacos地址配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: item-service # 服务名称
  cloud:
    nacos:
      server-addr: 192.168.146.131:8848 # nacos地址
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;重启项目&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-07_21-24-42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;服务发现&lt;/h3&gt;
&lt;p&gt;服务的消费者要去nacos订阅服务，这个过程就是服务发现，步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引入依赖&lt;/li&gt;
&lt;li&gt;配置Nacos地址&lt;/li&gt;
&lt;li&gt;发现并调用服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消费者需要连接nacos以拉取和订阅服务，因此服务发现的前两步与服务注册是一样，后面再加上服务调用即可：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;还是先添加依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--nacos 服务注册发现--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-discovery&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;添加yml文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;spring:
  cloud:
    nacos:
      server-addr: 192.168.150.101:8848
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;发现并调用服务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来，服务调用者cart-service就可以去订阅item-service服务了。不过item-service有多个实例，而真正发起调用时只需要知道一个实例的地址。
因此，服务调用者必须利用负载均衡的算法，从多个实例中挑选一个去访问。常见的负载均衡算法有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;随机&lt;/li&gt;
&lt;li&gt;轮询&lt;/li&gt;
&lt;li&gt;IP的hash&lt;/li&gt;
&lt;li&gt;最近最少访问&lt;/li&gt;
&lt;li&gt;...
这里我们可以选择最简单的随机负载均衡。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务发现需要用到一个工具，DiscoveryClient，SpringCloud已经帮我们自动装配，我们可以直接注入使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequiredArgsConstructor
public class


private final DiscoveryClient discoveryClient;



// 1.根据服务名称，拉取服务的实例列表
List&amp;lt;ServiceInstance&amp;gt; instances = discoveryClient.getInstances(&quot;item-service&quot;);
if (CollUtils.isEmpty(instances)) {
    return;
}
// 2.负载均衡，挑选一个实例
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
// 3.获取实例的IP和端口
ResponseEntity&amp;lt;List&amp;lt;ItemDTO&amp;gt;&amp;gt; response = restTemplate.exchange(instance.getUri() + &quot;/items?ids={ids}&quot;,
        HttpMethod.GET,
        null,
        new ParameterizedTypeReference&amp;lt;List&amp;lt;ItemDTO&amp;gt;&amp;gt;() {
        },
        Map.of(&quot;ids&quot;, CollUtil.join(itemIds, &quot;,&quot;)));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OpenFeign&lt;/h2&gt;
&lt;p&gt;OpenFeign是一个声明式的http客户端，是SpringCloud在Eureka公司开源的Feign基础上改造而来。官方地址：https://github.com/OpenFeign/feign&lt;/p&gt;
&lt;p&gt;其作用就是基于SpringMVC的常见注解，帮我们优雅的实现http请求的发送。&lt;/p&gt;
&lt;h3&gt;快速入门&lt;/h3&gt;
&lt;p&gt;OpenFeign已经被SpringCloud自动装配，实现起来非常简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入依赖，包括OpenFeign和负载均衡组件SpringCloudLoadBalancer&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;!--openFeign--&amp;gt;
  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
  &amp;lt;!--负载均衡器--&amp;gt;
  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-cloud-starter-loadbalancer&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;通过@EnableFeignClients注解，启用OpenFeign功能&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@MapperScan(&quot;com.hmall.cart.mapper&quot;)
@SpringBootApplication
@EnableFeignClients
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建client文件夹ItemClient接口&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@FeignClient(&quot;item-service&quot;)
public interface ItemClient {

    @GetMapping(&quot;/items&quot;)
    List&amp;lt;ItemDTO&amp;gt; queryItemByIds(@RequestParam(&quot;ids&quot;) Collection ids);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;注入并调用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@RequiredArgsConstructor

private final ItemClient itemClient;


Set&amp;lt;Long&amp;gt; itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());

List&amp;lt;ItemDTO&amp;gt; items = itemClient.queryItemByIds(itemIds);

if (CollUtils.isEmpty(items)) {
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作，是不是看起来优雅多了。&lt;/p&gt;
&lt;p&gt;而且，这里我们不再需要RestTemplate了，还省去了RestTemplate的注册。&lt;/p&gt;
&lt;p&gt;:::warning
注意：负载均衡早期用的是&lt;strong&gt;springCloud&lt;/strong&gt;里的&lt;code&gt;Ribbon&lt;/code&gt;，现在新版本都是用&lt;code&gt;loadbalancer&lt;/code&gt;
:::&lt;/p&gt;
&lt;h3&gt;连接池&lt;/h3&gt;
&lt;p&gt;OpenFeiqn对Http请求做了优雅的伪装，不过其底层发起http请求，依赖于其它的框架。这些框架可以自己选择，包括以下三种:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HttpURLconnection:默认实现，不支持连接池&lt;/li&gt;
&lt;li&gt;Apache HttpClient:支持连接池&lt;/li&gt;
&lt;li&gt;OKHttp:支持连接池
具体源码可以参考FeignBlockingLoadBalancerClient类中的delegate成员变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用步骤&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--OK http 的依赖 --&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;io.github.openfeign&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;feign-okhttp&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;开启连接池功能&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;feign:
  okhttp:
    enabled: true # 开启OKHttp功能
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启服务，连接池就生效了。&lt;/p&gt;
&lt;h3&gt;实践&lt;/h3&gt;
&lt;p&gt;一个服务中的FeignClient需要被其他多个服务调用，我们就需要在每个不同服务中定义多个相同接口，这不是重复编码吗？ 有什么办法能加避免重复编码呢？&lt;/p&gt;
&lt;p&gt;避免重复编码的办法就是抽取。不过这里有两种抽取思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;思路1：抽取到微服务之外的公共module&lt;/li&gt;
&lt;li&gt;思路2：每个微服务自己抽取一个module&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-09_21-34-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;方案1抽取更加简单，工程结构也比较清晰，但缺点是整个项目耦合度偏高。&lt;/p&gt;
&lt;p&gt;方案2抽取相对麻烦，工程结构相对更复杂，但服务之间耦合度降低。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方案2步骤&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;抽取Feign客户端，新建一个moudule，&lt;code&gt;xxxx-api&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;该模块依赖如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;!--open feign--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- load balancer--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-loadbalancer&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- swagger 注解依赖 --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.swagger&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;swagger-annotations&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.6.6&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把公共的客户端拷贝到该&lt;code&gt;xxxxx-api&lt;/code&gt;模块&lt;/p&gt;
&lt;p&gt;现在，任何微服务要调用&lt;code&gt;公共的FeignClient&lt;/code&gt;中的接口，只需要引入&lt;code&gt;xxxx-api&lt;/code&gt;模块依赖即可，无需自己编写Feign客户端了。&lt;/p&gt;
&lt;p&gt;在其他服务中引入该api模块&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;!--feign模块--&amp;gt;
  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;com.heima&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;hm-api&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;1.0.0&amp;lt;/version&amp;gt;
  &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启项目，发现报错了：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-09_21-46-03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里因为ItemClient现在定义到了com.hmall.api.client包下，而cart-service的启动类定义在com.hmall.cart包下，扫描不到ItemClient，所以报错了。&lt;/p&gt;
&lt;p&gt;解决办法很简单，在cart-service的启动类上添加声明即可，两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方式1：声明扫描包：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@EnableFeignClients(basePackages = &quot;com.hmall.api.client&quot;)
public class CartApplication {}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;方式2：声明要用的FeignClient&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@EnableFeignClients(clients = {ItemClient.class})
public class CartApplication {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;日志配置&lt;/h3&gt;
&lt;p&gt;OpenFeign只会在FeignClient所在包的日志级别为DEBUG时，才会输出日志。而且其日志级别有4级:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NONE:不记录任何日志信息，这是默认值。&lt;/li&gt;
&lt;li&gt;BASIC:仅记录请求的方法，URL以及响应状态码和执行时间&lt;/li&gt;
&lt;li&gt;HEADERS:在BASIC的基础上，额外记录了请求和响应的头信息&lt;/li&gt;
&lt;li&gt;FULL:记录所有请求和响应的明细，包括头信息、请求体、元数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于Feign默认的日志级别就是NONE，所以默认我们看不到请求日志&lt;/p&gt;
&lt;p&gt;在hm-api模块下新建一个配置类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
import feign.Logger;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.FULL;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在其他服务Module模块启动类中配置&lt;/p&gt;
&lt;p&gt;接下来，要让日志级别生效，还需要配置这个类。有两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部生效：在某个FeignClient中配置，只对当前FeignClient生效&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@FeignClient(value = &quot;item-service&quot;, configuration = DefaultFeignConfig.class)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;全局生效：在@EnableFeignClients中配置，针对所有FeignClient生效。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;22:18:52:730 DEBUG 9844 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds] &amp;lt;--- HTTP/1.1 200  (1280ms)
22:18:52:730 DEBUG 9844 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds] connection: keep-alive
22:18:52:730 DEBUG 9844 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds] content-type: application/json
22:18:52:730 DEBUG 9844 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds] date: Fri, 09 May 2025 14:18:52 GMT
22:18:52:730 DEBUG 9844 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds] keep-alive: timeout=60
22:18:52:730 DEBUG 9844 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds] transfer-encoding: chunked
22:18:52:730 DEBUG 9844 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds]
22:18:52:732 DEBUG 9844 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds] [{&quot;id&quot;:&quot;100000006163&quot;,&quot;name&quot;:&quot;巴布豆(BOBDOG)柔薄悦动婴儿拉拉裤XXL码80片(15kg以上)&quot;,&quot;price&quot;:67100,&quot;stock&quot;:10000,&quot;image&quot;:&quot;https://m.360buyimg.com/mobilecms/s720x720_jfs/t23998/350/2363990466/222391/a6e9581d/5b7cba5bN0c18fb4f.jpg!q70.jpg.webp&quot;,&quot;category&quot;:&quot;拉拉裤&quot;,&quot;brand&quot;:&quot;巴布豆&quot;,&quot;spec&quot;:&quot;{}&quot;,&quot;sold&quot;:11,&quot;commentCount&quot;:33343434,&quot;isAD&quot;:false,&quot;status&quot;:2}]
22:18:52:732 DEBUG 9844 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds] &amp;lt;--- END HTTP (371-byte body)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如何利用OpenFeign实现远程调用?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;引入OpeFeign和SpringCloudLoadBalancer依赖&lt;/li&gt;
&lt;li&gt;利用@EnableFeignClients注解开启OpenFeiqn功能&lt;/li&gt;
&lt;li&gt;编写FeignClient&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;如何配置OpenFeign的连接池?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;引入http客户端依赖，例如OKHttp、HttpClient&lt;/li&gt;
&lt;li&gt;配置yaml文件，打开OpenFeign连接池开关&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;OpenFeign使用的最佳实践方式是什么?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;由服务提供者编写独立module，将FeignClient及DTO抽取&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;如何配置OpenFeign输出日志的级别?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;声明类型为Logger.Level的Bean&lt;/li&gt;
&lt;li&gt;在@FeignClient或@EnableFeignclient注解上使用&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;网关路由&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;网关&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;就是网络的关口，负责请求的路由、转发、身份校验。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网关可以做安全控制，也就是登录身份校验，校验通过才放行&lt;/li&gt;
&lt;li&gt;通过认证后，网关再根据请求判断应该访问哪个微服务，将请求转发过去&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-12_20-16-42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在SpringCloud当中，提供了两种网关实现方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Netflix Zuul：早期实现，目前已经淘汰&lt;/li&gt;
&lt;li&gt;SpringCloudGateway：基于Spring的WebFlux技术，完全支持响应式编程，吞吐能力更强
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-12_20-18-38.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先，我们要在根目录下创建一个新的module，命名为hm-gateway，作为网关微服务：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入依赖&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;dependencies&amp;gt;
    &amp;lt;!--common--&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;com.heima&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;hm-common&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.0.0&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;!--网关--&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-cloud-starter-gateway&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;!--nacos discovery--&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-discovery&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;!--负载均衡--&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-cloud-starter-loadbalancer&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&amp;lt;build&amp;gt;
    &amp;lt;finalName&amp;gt;${project.artifactId}&amp;lt;/finalName&amp;gt;
    &amp;lt;plugins&amp;gt;
        &amp;lt;plugin&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
        &amp;lt;/plugin&amp;gt;
    &amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;启动类&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置路由&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来，在hm-gateway模块的resources目录新建一个application.yaml文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.150.101:8848
    gateway:
      routes:
        - id: item # 路由规则id，自定义，唯一
          uri: lb://item-service # 路由的目标服务，lb代表负载均衡，会从注册中心拉取服务列表
          predicates: # 路由断言，判断当前请求是否符合当前规则，符合则路由到目标服务
            - Path=/items/**,/search/** # 这里是以请求路径作为判断规则
        - id: cart
          uri: lb://cart-service
          predicates:
            - Path=/carts/**
        - id: user
          uri: lb://user-service
          predicates:
            - Path=/users/**,/addresses/**
        - id: trade
          uri: lb://trade-service
          predicates:
            - Path=/orders/**
        - id: pay
          uri: lb://pay-service
          predicates:
            - Path=/pay-orders/**

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由属性&lt;/h3&gt;
&lt;p&gt;网关路由对应的Java类型是&lt;code&gt;RouteDefinition&lt;/code&gt;，其中常见的属性有&lt;/p&gt;
&lt;p&gt;四个属性含义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;：路由的唯一标示&lt;/li&gt;
&lt;li&gt;&lt;code&gt;predicates&lt;/code&gt;：路由断言，其实就是匹配条件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filters&lt;/code&gt;：路由过滤条件，后面讲&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uri&lt;/code&gt;：路由目标地址，&lt;code&gt;lb://&lt;/code&gt;代表负载均衡，从注册中心获取目标微服务的实例列表，并且负载均衡选择一个访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们重点关注predicates，也就是路由断言。SpringCloudGateway中支持的断言类型有很多：&lt;/p&gt;
&lt;p&gt;Spring提供了12种基本的&lt;code&gt;RoutePredicateFactory&lt;/code&gt;实现:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;After&lt;/td&gt;
&lt;td&gt;是某个时间点后的请求&lt;/td&gt;
&lt;td&gt;- After=2037-01-20T17:42:47.789-07:00[America/Denver]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before&lt;/td&gt;
&lt;td&gt;是某个时间点之前的请求&lt;/td&gt;
&lt;td&gt;- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Between&lt;/td&gt;
&lt;td&gt;是某两个时间点之前的请求&lt;/td&gt;
&lt;td&gt;- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookie&lt;/td&gt;
&lt;td&gt;请求必须包含某些cookie&lt;/td&gt;
&lt;td&gt;- Cookie=chocolate, ch.p&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header&lt;/td&gt;
&lt;td&gt;请求必须包含某些header&lt;/td&gt;
&lt;td&gt;- Header=X-Request-Id, \d+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;请求必须是访问某个host（域名）&lt;/td&gt;
&lt;td&gt;- Host=&lt;strong&gt;.somehost.org,&lt;/strong&gt;.anotherhost.org&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Method&lt;/td&gt;
&lt;td&gt;请求方式必须是指定方式&lt;/td&gt;
&lt;td&gt;- Method=GET,POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path&lt;/td&gt;
&lt;td&gt;请求路径必须符合指定规则&lt;/td&gt;
&lt;td&gt;- Path=/red/{segment},/blue/**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query&lt;/td&gt;
&lt;td&gt;请求参数必须包含指定参数&lt;/td&gt;
&lt;td&gt;- Query=name, Jack或者- Query=name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RemoteAddr&lt;/td&gt;
&lt;td&gt;请求者的ip必须是指定范围&lt;/td&gt;
&lt;td&gt;- RemoteAddr=192.168.1.1/24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;weight&lt;/td&gt;
&lt;td&gt;权重处理&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;路由过滤器&lt;/h4&gt;
&lt;p&gt;网关中提供了33种路由过滤器，每种过滤器都有独特的作用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-12_21-34-18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试1&lt;/strong&gt;
在gateway中配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.146.131:8848
    gateway:
      routes:
        - id: item-service
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/**
          filters:
            - AddRequestHeader=truth, 123456
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public PageDTO&amp;lt;ItemDTO&amp;gt; queryItemByPage(PageQuery query, @RequestHeader(value = &quot;truth&quot;, required = false) String truth) {
        System.out.println(&quot;truth&quot; + truth);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试2&lt;/strong&gt;
默认过滤器，对所有路由都生效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    gateway:
      routes:
        - id: item-service
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/**
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/addresses/**,/users/**
        - id: cart-service
          uri: lb://cart-service
          predicates:
            - Path=/carts/**
        - id: pay-service
          uri: lb://pay-service
          predicates:
            - Path=/pay-orders/**
        - id: trade-service
          uri: lb://trade-service
          predicates:
            - Path=/orders/**
      default-filters:
        - AddRequestHeader=truth, 123456
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;网关过滤器&lt;/h3&gt;
&lt;p&gt;登录校验必须在请求转发到微服务之前做，否则就失去了意义。而网关的请求转发是Gateway内部代码实现的，要想在请求转发之前做登录校验，就必须了解Gateway内部工作的基本原理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-12_21-55-02.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如图所示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端请求进入网关后由&lt;code&gt;HandlerMapping&lt;/code&gt;对请求做判断，找到与当前请求匹配的路由规则（Route），然后将请求交给&lt;code&gt;WebHandler&lt;/code&gt;去处理。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebHandler&lt;/code&gt;则会加载当前路由下需要执行的过滤器链（Filter chain），然后按照顺序逐一执行过滤器（后面称为Filter）。&lt;/li&gt;
&lt;li&gt;图中&lt;code&gt;Filter&lt;/code&gt;被虚线分为左右两部分，是因为&lt;code&gt;Filter&lt;/code&gt;内部的逻辑分为&lt;code&gt;pre&lt;/code&gt;和&lt;code&gt;post&lt;/code&gt;两部分，分别会在请求路由到微服务之前和之后被执行。&lt;/li&gt;
&lt;li&gt;只有所有&lt;code&gt;Filter&lt;/code&gt;的&lt;code&gt;pre&lt;/code&gt;逻辑都依次顺序执行通过后，请求才会被路由到微服务。&lt;/li&gt;
&lt;li&gt;微服务返回结果后，再倒序执行&lt;code&gt;Filter&lt;/code&gt;的&lt;code&gt;post&lt;/code&gt;逻辑。&lt;/li&gt;
&lt;li&gt;最终把响应结果返回。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终请求转发是有一个名为&lt;code&gt;NettyRoutingFilter&lt;/code&gt;的过滤器来执行的，而且这个过滤器是整个过滤器链中顺序最靠后的一个。&lt;strong&gt;如果我们能够定义一个过滤器，在其中实现登录校验逻辑，并且将过滤器执行顺序定义到&lt;code&gt;NettyRoutingFilter&lt;/code&gt;之前&lt;/strong&gt;，这就符合我们的需求了！&lt;/p&gt;
&lt;p&gt;那么，该如何实现一个网关过滤器呢？&lt;/p&gt;
&lt;p&gt;网关过滤器链中的过滤器有两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GatewayFilter&lt;/code&gt;：路由过滤器，作用范围比较灵活，可以是任意指定的路由&lt;code&gt;Route&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GlobalFilter&lt;/code&gt;：全局过滤器，作用范围是所有路由，不可配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;自定义过滤器&lt;/h3&gt;
&lt;p&gt;无论是GatewayFilter还是GlobalFilter都支持自定义，只不过编码方式、使用方式略有差别。&lt;/p&gt;
&lt;p&gt;网关过滤器有两种，分别是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GatewayFilter&lt;/code&gt;:路由过滤器，作用于任意指定的路由;默认不生效，要配置到路由后生效。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GlobalFilter&lt;/code&gt;:全局过滤器，作用范围是所有路由;声明后自动生效&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;GlobalFilter&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-14_20-22-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gateway&lt;/code&gt;包下创建&lt;code&gt;filters&lt;/code&gt;,再创建&lt;code&gt;MyGlobalFilter&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 模拟登录校验
        ServerHttpRequest exchangeRequest = exchange.getRequest();
        HttpHeaders headers = exchangeRequest.getHeaders();
        System.out.println(&quot;headers:&quot; + headers);
        // 放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
    // 过滤器执行顺序，值越小，优先级越高
        return 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现登录校验&lt;/h3&gt;
&lt;p&gt;需求:在网关中基于过滤器实现登录校验功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final AuthProperties authProperties;
    private final JwtTool jwtTool;
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        if (isExculed(request.getPath().toString())) {
            return chain.filter(exchange);
        }
        String token = null;
        List&amp;lt;String&amp;gt; headers = request.getHeaders().get(&quot;authorization&quot;);
        if (headers != null &amp;amp;&amp;amp; !headers.isEmpty()) {
            token = headers.get(0);
        }
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        // 传递用户信息
        log.info(&quot;userId:{}&quot;, userId);

        return chain.filter(exchange);
    }

    private boolean isExculed(String string) {
        for (String excludePath : authProperties.getExcludePaths()) {
            if (antPathMatcher.match(excludePath, string)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;hm:
  jwt:
    location: classpath:hmall.jks
    alias: hmall
    password: hmall123
    tokenTTL: 30m
  auth:
    excludePaths:
      - /search/**
      - /users/login
      - /items/**
      - /hi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;utils&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class JwtTool {
    private final JWTSigner jwtSigner;

    public JwtTool(KeyPair keyPair) {
        this.jwtSigner = JWTSignerUtil.createSigner(&quot;rs256&quot;, keyPair);
    }

    /**
     * 创建 access-token
     *
     * @param
     * @return access-token
     */
    public String createToken(Long userId, Duration ttl) {
        // 1.生成jws
        return JWT.create()
                .setPayload(&quot;user&quot;, userId)
                .setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis()))
                .setSigner(jwtSigner)
                .sign();
    }

    /**
     * 解析token
     *
     * @param token token
     * @return 解析刷新token得到的用户信息
     */
    public Long parseToken(String token) {
        // 1.校验token是否为空
        if (token == null) {
            throw new UnauthorizedException(&quot;未登录&quot;);
        }
        // 2.校验并解析jwt
        JWT jwt;
        try {
            jwt = JWT.of(token).setSigner(jwtSigner);
        } catch (Exception e) {
            throw new UnauthorizedException(&quot;无效的token&quot;, e);
        }
        // 2.校验jwt是否有效
        if (!jwt.verify()) {
            // 验证失败
            throw new UnauthorizedException(&quot;无效的token&quot;);
        }
        // 3.校验是否过期
        try {
            JWTValidator.of(jwt).validateDate();
        } catch (ValidateException e) {
            throw new UnauthorizedException(&quot;token已经过期&quot;);
        }
        // 4.数据格式校验
        Object userPayload = jwt.getPayload(&quot;user&quot;);
        if (userPayload == null) {
            // 数据为空
            throw new UnauthorizedException(&quot;无效的token&quot;);
        }

        // 5.数据解析
        try {
           return Long.valueOf(userPayload.toString());
        } catch (RuntimeException e) {
            // 数据格式有误
            throw new UnauthorizedException(&quot;无效的token&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Data
@Component
@ConfigurationProperties(prefix = &quot;hm.auth&quot;)
public class AuthProperties {
    private List&amp;lt;String&amp;gt; includePaths;
    private List&amp;lt;String&amp;gt; excludePaths;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Data
@ConfigurationProperties(prefix = &quot;hm.jwt&quot;)
public class JwtProperties {
    private Resource location;
    private String password;
    private String alias;
    private Duration tokenTTL = Duration.ofMinutes(10);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public KeyPair keyPair(JwtProperties properties){
        // 获取秘钥工厂
        KeyStoreKeyFactory keyStoreKeyFactory =
                new KeyStoreKeyFactory(
                        properties.getLocation(),
                        properties.getPassword().toCharArray());
        //读取钥匙对
        return keyStoreKeyFactory.getKeyPair(
                properties.getAlias(),
                properties.getPassword().toCharArray());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体作用如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AuthProperties&lt;/code&gt;：配置登录校验需要拦截的路径，因为不是所有的路径都需要登录才能访问&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JwtProperties&lt;/code&gt;：定义与JWT工具有关的属性，比如秘钥文件位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SecurityConfig&lt;/code&gt;：工具的自动装配&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JwtTool&lt;/code&gt;：JWT工具，其中包含了校验和解析token的功能&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hmall.jks&lt;/code&gt;：秘钥文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::info
&lt;code&gt;@ConfigurationProperties(prefix = &quot;hm.auth&quot;)&lt;/code&gt; 是 Spring Boot 提供的一个注解，用于将配置文件（如 &lt;code&gt;application.properties&lt;/code&gt; 或 &lt;code&gt;application.yml&lt;/code&gt;）中的属性值绑定到 Java 对象中。
&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AntPathMatcher&lt;/code&gt; 是 Spring 框架提供的一个工具类，用于匹配路径模式（&lt;code&gt;Path Patterns&lt;/code&gt;）。在权限控制中验证用户是否有权访问某个路径。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;antPathMatcher.match(&quot;/user/*.json&quot;, &quot;/user/profile.json&quot;); // true
antPathMatcher.match(&quot;/**/*.html&quot;, &quot;/pages/home/index.html&quot;); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;网关传递用户&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;我们修改登录校验拦截器的处理逻辑，保存用户信息到请求头中：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final AuthProperties authProperties;
    private final JwtTool jwtTool;
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        if (isExculed(request.getPath().toString())) {
            return chain.filter(exchange);
        }
        String token = null;
        List&amp;lt;String&amp;gt; headers = request.getHeaders().get(&quot;authorization&quot;);
        if (headers != null &amp;amp;&amp;amp; !headers.isEmpty()) {
            token = headers.get(0);
        }
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        // 传递用户信息
        String userInfo = userId.toString();
        ServerWebExchange serverWebExchange = exchange.mutate()  // mutate就是对下游请求做更改
                .request(builder -&amp;gt; builder.header(&quot;user-info&quot;, userInfo))
                .build();
        log.info(&quot;userId:{}&quot;, userId);

        return chain.filter(serverWebExchange);
    }

    private boolean isExculed(String string) {
        for (String excludePath : authProperties.getExcludePaths()) {
            if (antPathMatcher.match(excludePath, string)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在hm-common中编写&lt;code&gt;SpringMVC&lt;/code&gt;拦截器，获取登录用户&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需求:由于每个微服务都可能有获取登录用户的需求，因此我们直接在hm-common模块定义拦截器
这样微服务只需要引入依赖即可生效，无需重复编写。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserInfoInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取登录用户信息
        String userInfo = request.getHeader(&quot;user-info&quot;);
        // 是否获取了用户，如果有存入ThreadLocal
        if (StrUtil.isNotBlank(userInfo)) {
            UserContext.setUser(Long.valueOf(userInfo));
        }
        // 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContext.removeUser();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过，需要注意的是，这个配置类默认是不会生效的，因为它所在的包是&lt;code&gt;com.hmall.common.config&lt;/code&gt;，与其它微服务的扫描包不一致，无法被扫描到，因此无法生效。
基于&lt;code&gt;SpringBoot&lt;/code&gt;的自动装配原理，我们要将其添加到&lt;code&gt;resources&lt;/code&gt;目录下的&lt;code&gt;META-INF/spring.factories&lt;/code&gt;文件中：&lt;/p&gt;
&lt;p&gt;common的&lt;code&gt;resources&lt;/code&gt;下的&lt;code&gt;META-INF&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.JsonConfig,\
  com.hmall.common.config.MvcConfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;common&lt;/code&gt;的&lt;code&gt;utils&lt;/code&gt;包下的&lt;code&gt;UserContext&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserContext {
    private static final ThreadLocal&amp;lt;Long&amp;gt; tl = new ThreadLocal&amp;lt;&amp;gt;();

    /**
     * 保存当前登录用户信息到ThreadLocal
     * @param userId 用户id
     */
    public static void setUser(Long userId) {
        tl.set(userId);
    }

    /**
     * 获取当前登录用户信息
     * @return 用户id
     */
    public static Long getUser() {
        return tl.get();
    }

    /**
     * 移除当前登录用户信息
     */
    public static void removeUser(){
        tl.remove();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;code&gt;@ConditionalOnClass&lt;/code&gt; 的作用是检查类路径中是否存在指定的类。如果存在，则加载被标注的配置或组件；否则，忽略该配置。&lt;/p&gt;
&lt;p&gt;在微服务或多模块项目中，可以根据运行时的类路径动态加载不同的配置。我们希望他在网关中不要加载，因为网关是基于&lt;code&gt;WebFlux&lt;/code&gt;加载会报错
:::&lt;/p&gt;
&lt;h3&gt;OpenFeign传递用户&lt;/h3&gt;
&lt;p&gt;微服务之间传递用户信息&lt;/p&gt;
&lt;p&gt;前端发起的请求都会经过网关再到微服务，由于我们之前编写的过滤器和拦截器功能，微服务可以轻松获取登录用户信息。&lt;/p&gt;
&lt;p&gt;但有些业务是比较复杂的，请求到达微服务后还需要调用其它多个微服务。比如下单业务，流程如下：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-14_21-52-23.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下单的过程中，需要调用商品服务扣减库存，调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是，&lt;strong&gt;订单服务调用购物车时并没有传递用户信息&lt;/strong&gt;，购物车服务无法知道当前用户是谁！&lt;/p&gt;
&lt;p&gt;由于微服务获取用户信息是通过拦截器在请求头中读取，因此要想实现微服务之间的用户信息传递，就&lt;strong&gt;必须在微服务发起调用时把用户信息存入请求头&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;OpenFeign中提供了一个拦截器接口，所有由OpenFeign发起的请求都会先调用拦截器处理请求：&lt;/p&gt;
&lt;p&gt;由于FeignClient全部都是在hm-api模块，因此我们在hm-api模块的com.hmall.api.config.DefaultFeignConfig中编写这个拦截器：&lt;/p&gt;
&lt;p&gt;在hm-api中的config的&lt;code&gt;DefaultFeignConfig&lt;/code&gt;中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public RequestInterceptor userInfoRequestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long userId = UserContext.getUser();
                if (userId != null) {
                    requestTemplate.header(&quot;user-info&quot;, userId.toString());
                }
            }
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动类上配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableFeignClients(basePackages = &quot;com.hmall.api.client&quot;, defaultConfiguration = DefaultFeignConfig.class)
public class TradeApplication {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好了，现在微服务之间通过OpenFeign调用时也会传递登录用户信息了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-14_22-07-04.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;配置管理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;微服务重复配置过多，维护成本高&lt;/li&gt;
&lt;li&gt;业务配置经常变动，每次修改都要重启服务&lt;/li&gt;
&lt;li&gt;网关路由配置写死，如果变更要重启网关&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;配置共享&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;添加配置到&lt;code&gt;Nacos&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;添加一些共享配置到&lt;code&gt;Nacos&lt;/code&gt;中，包括:&lt;code&gt;Jdbc&lt;/code&gt;、&lt;code&gt;MybatisPlus&lt;/code&gt;、日志、&lt;code&gt;Swagger&lt;/code&gt;、&lt;code&gt;OpenFeign&lt;/code&gt;等配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://${hm.db.host}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&amp;amp;characterEncoding=UTF-8&amp;amp;autoReconnect=true&amp;amp;serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: ${hm.db.un:root}
    password: ${hm.db.pw:123}
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  global-config:
    db-config:
      update-strategy: not_null
      id-type: auto
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;logging:
  level:
    com.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: &quot;logs/${spring.application.name}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;knife4j:
  enable: true
  openapi:
    title: ${hm.swagger.title:黑马商城接口文档}
    description: ${hm.swagger.desc:黑马商城接口文档}
    email: zxyang3636@163.com
    concat: zzyang
    url: https://www.zzyang.top
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:  # Swagger扫描到Controller，会把Controller接口信息作为接口文档信息
          - ${hm.swagger.package}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;拉取共享配置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-15_19-56-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;读取Nacos配置是SpringCloud上下文（ApplicationContext）初始化时处理的，发生在项目的引导阶段。然后才会初始化SpringBoot上下文，去读取application.yaml。
也就是说引导阶段，application.yaml文件尚未读取，根本不知道nacos 地址，该如何去加载nacos中的配置文件呢？&lt;/p&gt;
&lt;p&gt;SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件，如果我们将nacos地址配置到bootstrap.yaml中，那么在项目引导阶段就可以读取nacos中的配置了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引入依赖&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;!--nacos配置管理--&amp;gt;
  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-config&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
  &amp;lt;!--读取bootstrap文件--&amp;gt;
  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-cloud-starter-bootstrap&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建&lt;code&gt;bootstrap.yaml&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在cart-service中的resources目录新建一个bootstrap.yaml文件：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;内容如下：&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: cart-service # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.146.131:8848 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yaml # 共享mybatis配置
          - dataId: shared-log.yaml # 共享日志配置
          - dataId: shared-swagger.yaml # 共享日志配置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;application.yaml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8082
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持
hm:
  swagger:
    title: 购物车服务接口文档
    package: com.hmall.cart.controller
  db:
    database: hm-cart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启服务，发现所有配置都生效了。&lt;/p&gt;
&lt;h3&gt;配置热更新&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/wechat_2025-05-15_202238_944.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新建配置文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Data
@Component
@ConfigurationProperties(prefix = &quot;hm.cart&quot;)
public class CartProperties {
    private Integer maxItems;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注入&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl&amp;lt;CartMapper, Cart&amp;gt; implements ICartService {}

private final CartProperties cartProperties;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;改造代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  private void checkCartsFull(Long userId) {
        int count = lambdaQuery().eq(Cart::getUserId, userId).count().intValue();
        if (count &amp;gt;= cartProperties.getMaxItems()) {
            throw new BizIllegalException(StrUtil.format(&quot;用户购物车课程不能超过{}&quot;, cartProperties.getMaxItems()));
        }
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;nacos中创建&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意文件的dataId格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[服务名]-[spring.active.profile].[后缀名]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文件名称由三部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务名：我们是购物车服务，所以是cart-service&lt;/li&gt;
&lt;li&gt;spring.active.profile：就是spring boot中的spring.active.profile，可以省略，则所有profile共享该配置&lt;/li&gt;
&lt;li&gt;后缀名：例如yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们直接使用&lt;code&gt;cart-service.yaml&lt;/code&gt;这个名称，则不管是dev还是local环境都可以共享该配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hm:
  cart:
    maxItems: 1 # 购物车商品数量上限
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后续修改&lt;code&gt;maxItems&lt;/code&gt;，无需重启服务，配置热更新就生效了！&lt;/p&gt;
&lt;h3&gt;动态路由&lt;/h3&gt;
&lt;p&gt;监听Nacos配置变更可以参考官方文档： https://nacos.io/zh-cn/docs/sdk.html&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;网关中引入依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;!--nacos配置管理--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-config&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!--读取bootstrap文件--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-bootstrap&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;网关的bootstrap.yaml文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.146.131:8848
      config:
        file-extension: yaml
        shared-configs:
          - data-id: shared-log.yaml
  profiles:
    active: dev
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8080

hm:
  jwt:
    location: classpath:hmall.jks
    alias: hmall
    password: hmall123
    tokenTTL: 30m
  auth:
    excludePaths:
      - /search/**
      - /users/login
      - /items/**
      - /hi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;网关中，新建&lt;code&gt;routers/DynamicRouteLoader.java&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {

    private final NacosConfigManager nacosConfigManager;
    private final RouteDefinitionWriter writer;

    private final String dataId = &quot;gateway-routes.json&quot;;
    private final String group = &quot;DEFAULT_GROUP&quot;;

    private final Set&amp;lt;String&amp;gt; routeIds = new HashSet&amp;lt;&amp;gt;();

    @PostConstruct
    public void initRouteConfigListener() throws NacosException {
        // 项目启动时，先拉取一次配置，并添加配置监听器
        String configInfo = nacosConfigManager.getConfigService()
                .getConfigAndSignListener(dataId, group, 5000, new Listener() {
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }

                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        // 2.监听到配置变更，更新路由表
                        updateConfigInfo(configInfo);
                    }
                });
        // 3.第一次读取到配置，也需要更新到路由表
        updateConfigInfo(configInfo);
    }

    public void updateConfigInfo(String configInfo) {
        log.debug(&quot;监听到路由配置信息：{}&quot;, configInfo);
        // 1.解析配置信息，转为RouteDefinition
        List&amp;lt;RouteDefinition&amp;gt; routeDefinitionList = JSONUtil.toList(configInfo, RouteDefinition.class);
        // 删除旧的路由表
        for (String routeId : routeIds) {
            writer.delete(Mono.just(routeId));
        }
        routeIds.clear();
        // 2.更新路由表
        for (RouteDefinition routeDefinition : routeDefinitionList) {
            // 更新路由表
            writer.save(Mono.just(routeDefinition)).subscribe();
            // 记录路由id，便于下一次更新时删除
            routeIds.add(routeDefinition.getId());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nacos配置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-15_22-16-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gateway-routes.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &quot;id&quot;: &quot;item&quot;,
        &quot;predicates&quot;: [{
            &quot;name&quot;: &quot;Path&quot;,
            &quot;args&quot;: {&quot;_genkey_0&quot;:&quot;/items/**&quot;, &quot;_genkey_1&quot;:&quot;/search/**&quot;}
        }],
        &quot;filters&quot;: [],
        &quot;uri&quot;: &quot;lb://item-service&quot;
    },
    {
        &quot;id&quot;: &quot;cart&quot;,
        &quot;predicates&quot;: [{
            &quot;name&quot;: &quot;Path&quot;,
            &quot;args&quot;: {&quot;_genkey_0&quot;:&quot;/carts/**&quot;}
        }],
        &quot;filters&quot;: [],
        &quot;uri&quot;: &quot;lb://cart-service&quot;
    },
    {
        &quot;id&quot;: &quot;user&quot;,
        &quot;predicates&quot;: [{
            &quot;name&quot;: &quot;Path&quot;,
            &quot;args&quot;: {&quot;_genkey_0&quot;:&quot;/users/**&quot;, &quot;_genkey_1&quot;:&quot;/addresses/**&quot;}
        }],
        &quot;filters&quot;: [],
        &quot;uri&quot;: &quot;lb://user-service&quot;
    },
    {
        &quot;id&quot;: &quot;trade&quot;,
        &quot;predicates&quot;: [{
            &quot;name&quot;: &quot;Path&quot;,
            &quot;args&quot;: {&quot;_genkey_0&quot;:&quot;/orders/**&quot;}
        }],
        &quot;filters&quot;: [],
        &quot;uri&quot;: &quot;lb://trade-service&quot;
    },
    {
        &quot;id&quot;: &quot;pay&quot;,
        &quot;predicates&quot;: [{
            &quot;name&quot;: &quot;Path&quot;,
            &quot;args&quot;: {&quot;_genkey_0&quot;:&quot;/pay-orders/**&quot;}
        }],
        &quot;filters&quot;: [],
        &quot;uri&quot;: &quot;lb://pay-service&quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;微服务保护&lt;/h2&gt;
&lt;h3&gt;雪崩问题&lt;/h3&gt;
&lt;p&gt;微服务调用链路中的某个服务故障，引起整个链路中的所有微服务都不可用，这就是雪崩。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-19_21-23-31.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-19_21-23-44.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;雪崩问题产生的原因是什么?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;微服务相互调用，服务提供者出现故障或阻塞。&lt;/li&gt;
&lt;li&gt;服务调用者没有做好异常处理，导致自身故障。&lt;/li&gt;
&lt;li&gt;调用链中的所有服务级联失败，导致整个集群故障&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;解决问题的思路有哪些?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;尽量避免服务出现故障或阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅保证代码的健壮性;&lt;/p&gt;
&lt;p&gt;✅保证网络畅通;&lt;/p&gt;
&lt;p&gt;✅能应对较高的并发请求;&lt;/p&gt;
&lt;h3&gt;服务保护方案&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;请求限流&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;请求限流：限制访问微服务的请求的并发量，避免服务因流量激增出现故障，
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/wechat_2025-05-19_213425_995.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;线程隔离&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;线程隔离:也叫做舱壁模式，模拟船舱隔板的防水原理。通过限定每个业务能使用的线程数量而将故障业务隔离，避免
故障扩散。&lt;/p&gt;
&lt;p&gt;线程隔离的思想来自轮船的舱壁模式：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-19_21-51-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;轮船的船舱会被隔板分割为N个相互隔离的密闭舱，假如轮船触礁进水，只有损坏的部分密闭舱会进水，而其他舱由于相互隔离，并不会进水。这样就把进水控制在部分船体，避免了整个船舱进水而沉没。&lt;/p&gt;
&lt;p&gt;为了避免某个接口故障或压力过大导致整个服务不可用，我们可以限定每个接口可以使用的资源范围，也就是将其“隔离”起来。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;服务熔断&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;服务熔断:由断路器统计请求的异常比例或慢调用比例，如果超出阈值则会熔断该业务，则拦截该接口的请求。熔断期间，所有请求快速失败，全都走&lt;code&gt;fallback&lt;/code&gt;逻辑&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;解决雪崩问题的常见方案有哪些?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;请求限流:限制流量在服务可以处理的范围，避免因突发流量而故障&lt;/li&gt;
&lt;li&gt;线程隔离:控制业务可用的线程数量，将故障隔离在一定范围&lt;/li&gt;
&lt;li&gt;服务熔断:将异常比例过高的接口断开，拒绝所有请求，直接走fallback&lt;/li&gt;
&lt;li&gt;失败处理:定义fallback逻辑，让业务失败时不再抛出异常，而是返回默认数据或友好提示&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-19_21-52-02.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Sentinel&lt;/h3&gt;
&lt;p&gt;Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址： https://sentinelguard.io/zh-cn/index.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;安装运行Sentinel&lt;/h4&gt;
&lt;p&gt;将&lt;code&gt;jar包&lt;/code&gt;放在任意非中文、不包含特殊字符的目录下，重命名为sentinel-dashboard.jar：&lt;/p&gt;
&lt;p&gt;然后运行如下命令启动控制台：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问&lt;code&gt;http://localhost:8090&lt;/code&gt;页面，就可以看到&lt;code&gt;sentinel&lt;/code&gt;的控制台了：&lt;/p&gt;
&lt;p&gt;需要输入账号和密码，默认都是：&lt;code&gt;sentinel&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;登录后，即可看到控制台，默认会监控sentinel-dashboard服务本身&lt;/p&gt;
&lt;p&gt;:::info
sentinel安装地址：https://github.com/alibaba/Sentinel/releases&lt;/p&gt;
&lt;p&gt;启动时可配置参数可参考官方文档：https://github.com/alibaba/Sentinel/wiki/%E5%90%AF%E5%8A%A8%E9%85%8D%E7%BD%AE%E9%A1%B9
:::&lt;/p&gt;
&lt;h4&gt;微服务整合&lt;/h4&gt;
&lt;p&gt;引入sentinel依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--sentinel--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-sentinel&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;application.yaml&lt;/code&gt;文件，添加下面内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启&lt;code&gt;cart-service&lt;/code&gt;，然后访问查询购物车接口，&lt;code&gt;sentinel&lt;/code&gt;的客户端就会将服务访问的信息提交到sentinel-dashboard控制台。并展示出统计信息&lt;/p&gt;
&lt;p&gt;&lt;em&gt;簇点链路&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;所谓簇点链路，就是单机调用链路，是一次请求进入服务后经过的每一个被Sentinel监控的资源。默认情况下，Sentinel会监控SpringMVC的每一个Endpoint（接口）。
因此，我们看到/carts这个接口路径就是其中一个簇点，我们可以对其进行限流、熔断、隔离等保护措施。&lt;/p&gt;
&lt;p&gt;不过，需要注意的是，我们的SpringMVC接口是按照Restful风格设计，因此购物车的查询、删除、修改等接口全部都是/carts路径：&lt;/p&gt;
&lt;p&gt;默认情况下Sentinel会把路径作为簇点资源的名称，无法区分路径相同但请求方式不同的接口，查询、删除、修改等都被识别为一个簇点资源，这显然是不合适的。&lt;/p&gt;
&lt;p&gt;所以我们可以选择打开Sentinel的请求方式前缀，把请求方式 + 请求路径作为簇点资源名：&lt;/p&gt;
&lt;p&gt;首先，在cart-service的&lt;code&gt;application.yml&lt;/code&gt;中添加下面的配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090
      http-method-specify: true # 开启请求方式前缀
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;请求限流&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-19_22-27-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;线程隔离&lt;/h3&gt;
&lt;p&gt;限流可以降低服务器压力，尽量减少因并发流量引起的服务故障的概率，但并不能完全避免服务故障。一旦某个服务出现故障，我们必须隔离对这个服务的调用，避免发生雪崩。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-20_20-51-15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::warning
需要注意的是，默认情况下SpringBoot项目的tomcat最大线程数是200，允许的最大连接是8492，单机测试很难打满。
:::&lt;/p&gt;
&lt;p&gt;如果测试的话可以配置以下内容,我们需要配置一下cart-service模块的application.yml文件，修改tomcat连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8082
  tomcat:
    threads:
      max: 50 # 允许的最大线程数
    accept-count: 50 # 最大排队等待数量
    max-connections: 100 # 允许的最大连接
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;配置线程隔离&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在簇点链路中点击&lt;code&gt;流控&lt;/code&gt;按钮&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置线程隔离⬇️
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-20_21-04-53.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意，这里勾选的是并发线程数限制，也就是说这个查询功能最多使用5个线程，而不是5QPS。如果查询商品的接口每秒处理2个请求，则5个线程的实际QPS在10左右，而超出的请求自然会被拒绝。&lt;/p&gt;
&lt;p&gt;此时如果我们通过页面访问购物车的其它接口，例如添加购物车、修改购物车商品数量，发现不受影响；&lt;/p&gt;
&lt;p&gt;响应时间非常短，这就证明线程隔离起到了作用，尽管查询购物车这个接口并发很高，但是它能使用的线程资源被限制了，因此不会影响到其它接口。&lt;/p&gt;
&lt;h3&gt;Fallback&lt;/h3&gt;
&lt;p&gt;Fallback实现逻辑&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将FeiqnClient作为Sentinel的簇点资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FeignClient的Fallback有两种配置方式:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;方式一:FallbackClass，无法对远程调用的异常做处理&lt;/li&gt;
&lt;li&gt;方式二:FallbackFactory，可以对远程调用的异常做处理，通常都会选择这种&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;步骤&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;创建文件夹&lt;code&gt;com.hmall.api.client.fallback&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory&amp;lt;ItemClient&amp;gt; {


    @Override
    public ItemClient create(Throwable cause) {

        return new ItemClient() {
            @Override
            public List&amp;lt;ItemDTO&amp;gt; queryItemByIds(Collection&amp;lt;Long&amp;gt; ids) {
                log.error(&quot;查询商品失败&quot;, cause);
                return CollUtils.emptyList();
            }

            @Override
            public void deductStock(List&amp;lt;OrderDetailDTO&amp;gt; items) {
                log.error(&quot;扣减商品库存失败&quot;, cause);
                throw new RuntimeException(cause);
            }
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注册为bean&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public RequestInterceptor userInfoRequestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long userId = UserContext.getUser();
                if (userId != null) {
                    requestTemplate.header(&quot;user-info&quot;, userId.toString());
                }
            }
        };
    }

    @Bean
    public ItemClientFallbackFactory itemClientFallbackFactory() {
        return new ItemClientFallbackFactory();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在ItemClient注解&lt;code&gt;FeignClient&lt;/code&gt;上配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@FeignClient(value = &quot;item-service&quot;, fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {

    @GetMapping(&quot;/items&quot;)
    List&amp;lt;ItemDTO&amp;gt; queryItemByIds(@RequestParam(&quot;ids&quot;) Collection&amp;lt;Long&amp;gt; ids);

    @PutMapping(&quot;/items/stock/deduct&quot;)
    void deductStock(@RequestBody List&amp;lt;OrderDetailDTO&amp;gt; items);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改调用者的&lt;code&gt;application.yaml&lt;/code&gt;，也就是cart-service的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;feign:
  okhttp:
    enabled: true # 开启OKHttp功能
  sentinel:
    enabled: true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;服务熔断&lt;/h3&gt;
&lt;p&gt;熔断是解决雪崩问题的重要手段。思路是由&lt;code&gt;断路器&lt;/code&gt;统计服务调用的异常比例、慢请求比例，如果超出阈值则会&lt;code&gt;熔断&lt;/code&gt;该服务。即拦截访问该服务的一切请求;而当服务恢复时，断路器会放行访问该服务的请求&lt;/p&gt;
&lt;p&gt;Sentinel中的断路器不仅可以统计某个接口的&lt;code&gt;慢请求比例&lt;/code&gt;，还可以统计&lt;code&gt;异常请求比例&lt;/code&gt;。当这些比例超出阈值时，就会&lt;code&gt;熔断&lt;/code&gt;该接口，即拦截访问该接口的一切请求，降级处理；当该接口恢复正常时，再放行对于该接口的请求。
断路器的工作状态切换有一个状态机来控制：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/wechat_2025-05-20_223221_264.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;状态机包括三个状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;closed&lt;/code&gt;：关闭状态，断路器放行所有请求，并开始统计异常比例、慢请求比例。超过阈值则切换到&lt;code&gt;open&lt;/code&gt;状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;open&lt;/code&gt;：打开状态，服务调用被熔断，访问被熔断服务的请求会被拒绝，快速失败，直接走降级逻辑。Open状态持续一段时间后会进入&lt;code&gt;half-open&lt;/code&gt;状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;half-open&lt;/code&gt;：半开状态，放行一次请求，根据执行结果来判断接下来的操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求成功：则切换到&lt;code&gt;closed&lt;/code&gt;状态&lt;/li&gt;
&lt;li&gt;请求失败：则切换到&lt;code&gt;open&lt;/code&gt;状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以在控制台通过点击簇点后的&lt;code&gt;熔断&lt;/code&gt;按钮来配置熔断策略：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-20_22-34-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种是按照慢调用比例来做熔断，上述配置的含义是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RT超过200毫秒的请求调用就是慢调用&lt;/li&gt;
&lt;li&gt;统计最近1000ms内的最少5次请求，如果慢调用比例不低于0.5，则触发熔断&lt;/li&gt;
&lt;li&gt;熔断持续时长20s&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分布式事务&lt;/h2&gt;
&lt;p&gt;在分布式系统中，如果一个业务需要多个服务合作完成，而且每一个服务都有事务，多个事务必须同时成功或失败，这样的事务就是&lt;strong&gt;分布式事务&lt;/strong&gt;。其中的每个服务的事务就是一个&lt;strong&gt;分支事务&lt;/strong&gt;。整个业务称为&lt;strong&gt;全局事务&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-21_20-22-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于订单、购物车、商品分别在三个不同的微服务，而每个微服务都有自己独立的数据库，因此下单过程中就会跨多个数据库完成业务。而每个微服务都会执行自己的本地事务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;交易服务：下单事务&lt;/li&gt;
&lt;li&gt;购物车服务：清理购物车事务&lt;/li&gt;
&lt;li&gt;库存服务：扣减库存事务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个业务中，各个本地事务是有关联的。因此每个微服务的本地事务，也可以称为分支事务。多个有关联的分支事务一起就组成了全局事务。我们必须保证整个全局事务同时成功或失败。&lt;/p&gt;
&lt;p&gt;我们知道每一个分支事务就是传统的单体事务，都可以满足ACID特性，但全局事务跨越多个服务、多个数据库，是否还能满足呢？&lt;/p&gt;
&lt;p&gt;事务并未遵循ACID的原则，归其原因就是参与事务的多个子业务在不同的微服务，跨越了不同的数据库。虽然每个单独的业务都能在本地遵循ACID，但是它们互相之间没有感知，不知道有人失败了，无法保证最终结果的统一，也就无法遵循ACID的事务特性了。
这就是分布式事务问题，出现以下情况之一就可能产生分布式事务问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务跨多个服务实现&lt;/li&gt;
&lt;li&gt;业务跨多个数据源实现&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;初识Seata&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Seata&lt;/code&gt;是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务，为用户打造一站式的分布式解决方案。&lt;/p&gt;
&lt;p&gt;官网地址：http://seata.io/   其中的文档、播客中提供了大量的使用说明、源码分析。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-21_20-36-08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其实分布式事务产生的一个重要原因，就是参与事务的多个分支事务互相无感知，不知道彼此的执行状态。因此解决分布式事务的思想非常简单：&lt;/p&gt;
&lt;p&gt;就是找一个统一的事务协调者，与多个分支事务通信，检测每个分支事务的执行状态，保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。&lt;/p&gt;
&lt;p&gt;在Seata的事务管理中有三个重要的角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TC (Transaction Coordinator) - 事务协调者&lt;/strong&gt;：维护全局和分支事务的状态，协调全局事务提交或回滚。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TM (Transaction Manager) - 事务管理器&lt;/strong&gt;：定义全局事务的范围、开始全局事务、提交或回滚全局事务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RM (Resource Manager) - 资源管理器&lt;/strong&gt;：管理分支事务，与TC交谈以注册分支事务和报告分支事务的状态，并驱动分支事务提交或回滚。
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-21_20-38-01.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，TM和RM可以理解为Seata的客户端部分，引入到参与事务的微服务依赖中即可。将来TM和RM就会协助微服务，实现本地分支事务与TC之间交互，实现事务的提交或回滚。&lt;/p&gt;
&lt;p&gt;而TC服务则是事务协调中心，是一个独立的微服务，需要单独部署。&lt;/p&gt;
&lt;h3&gt;部署TC服务&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;执行sql文件 文件地址：&lt;a href=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/sql/seata-tc.sql&quot;&gt;seata-tc&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;会得到这样一个数据库和表⬇️
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-21_20-49-41.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;上传我们的&lt;code&gt;seata服务&lt;/code&gt; 和 &lt;code&gt;seata镜像&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们将整个seata文件夹拷贝到虚拟机的&lt;code&gt;/root&lt;/code&gt;目录：&lt;/p&gt;
&lt;p&gt;必须要确保我们的服务都在同一网络下&lt;/p&gt;
&lt;p&gt;加载镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker load -i seata-1.5.2.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查是否在同一网络下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network ls

docker inspect nacos

docker inspect mysql

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没在同一网络下执行该命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network connect hm-net nacos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行docker命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.146.131 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SEATA_IP&lt;/code&gt;参数的端口号换成自己的&lt;/p&gt;
&lt;p&gt;:::tip
&lt;code&gt;7099&lt;/code&gt;端口是web控制台&lt;/p&gt;
&lt;p&gt;&lt;code&gt;8099&lt;/code&gt;端口是给我们微服务连接使用的
:::&lt;/p&gt;
&lt;p&gt;查看nacos的服务列表，即可看到Seata服务&lt;/p&gt;
&lt;p&gt;访问该地址 http://192.168.146.131:7099/#/login   进入控制台&lt;/p&gt;
&lt;p&gt;seata服务中的application.yaml中配置的账号密码是 &lt;code&gt;admin&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;微服务集成Seata&lt;/h3&gt;
&lt;p&gt;为了方便各个微服务集成seata，我们需要把seata配置共享到nacos，因此trade-service模块不仅仅要引入seata依赖，还要引入nacos依赖:&lt;/p&gt;
&lt;p&gt;1.引入依赖&lt;/p&gt;
&lt;p&gt;只要是服务的参与者，必须都要引入该依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--统一配置管理--&amp;gt;
  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-config&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
  &amp;lt;!--读取bootstrap文件--&amp;gt;
  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-cloud-starter-bootstrap&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
  &amp;lt;!--seata--&amp;gt;
  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-seata&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.改造配置&lt;/p&gt;
&lt;p&gt;首先在nacos上添加一个共享的seata配置，命名为&lt;code&gt;shared-seata.yaml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;seata:
  registry: # TC服务注册中心的配置，微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 192.168.150.101:8848 # nacos地址
      namespace: &quot;&quot; # namespace，默认为空
      group: DEFAULT_GROUP # 分组，默认是DEFAULT_GROUP
      application: seata-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: hmall # 事务组名称
  service:
    vgroup-mapping: # 事务组与tc集群的映射关系
      hmall: &quot;default&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，改造trade-service模块，添加&lt;code&gt;bootstrap.yaml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: trade-service # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.146.131 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yaml # 共享mybatis配置
          - dataId: shared-log.yaml # 共享日志配置
          - dataId: shared-swagger.yaml # 共享日志配置
          - dataId: shared-seata.yaml # 共享seata配置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到这里加载了共享的seata配置。&lt;/p&gt;
&lt;p&gt;然后改造application.yaml文件，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8085
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持
  sentinel:
    enabled: true # 开启Feign对Sentinel的整合
hm:
  swagger:
    title: 交易服务接口文档
    package: com.hmall.trade.controller
  db:
    database: hm-trade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看seata日志，即可看到有哪些服务注册上来了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;13:41:25.411  INFO --- [ettyServerNIOWorker_1_1_2] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId=&apos;cart-service&apos;, transactionServiceGroup=&apos;hmall&apos;},channel:[id: 0x5ab2a41f, L:/172.19.0.4:8099 - R:/192.168.146.1:62804],client version:1.5.2
13:41:27.848  INFO --- [rverHandlerThread_1_1_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds=&apos;jdbc:mysql://192.168.146.131:3306/hm-cart&apos;, applicationId=&apos;cart-service&apos;, transactionServiceGroup=&apos;hmall&apos;},channel:[id: 0xd6dae7c4, L:/172.19.0.4:8099 - R:/192.168.146.1:62830],client version:1.5.2
13:50:49.572  INFO --- [ettyServerNIOWorker_1_1_2] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId=&apos;item-service&apos;, transactionServiceGroup=&apos;hmall&apos;},channel:[id: 0xf8d4148f, L:/172.19.0.4:8099 - R:/192.168.146.1:64349],client version:1.5.2
13:50:50.541  INFO --- [rverHandlerThread_1_2_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds=&apos;jdbc:mysql://192.168.146.131:3306/hm-item&apos;, applicationId=&apos;item-service&apos;, transactionServiceGroup=&apos;hmall&apos;},channel:[id: 0xa4b01da3, L:/172.19.0.4:8099 - R:/192.168.146.1:64355],client version:1.5.2
13:52:40.084  INFO --- [ettyServerNIOWorker_1_1_2] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId=&apos;trade-service&apos;, transactionServiceGroup=&apos;hmall&apos;},channel:[id: 0xd939830d, L:/172.19.0.4:8099 - R:/192.168.146.1:64878],client version:1.5.2
13:52:42.143  INFO --- [rverHandlerThread_1_3_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds=&apos;jdbc:mysql://192.168.146.131:3306/hm-trade&apos;, applicationId=&apos;trade-service&apos;, transactionServiceGroup=&apos;hmall&apos;},channel:[id: 0x4d6e0e40, L:/172.19.0.4:8099 - R:/192.168.146.1:64888],client version:1.5.2
[root@localhost ~]#
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;XA模式&lt;/h3&gt;
&lt;p&gt;XA 规范 是 X/Open 组织定义的分布式事务处理（DTP，Distributed Transaction Processing）标准，XA 规范 描述了全局的TM与局部的RM之间的接口，几乎所有主流的关系型数据库都对 XA 规范 提供了支持。Seata的XA模式如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-21_22-11-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一阶段的工作：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;RM注册分支事务到TC&lt;/li&gt;
&lt;li&gt;RM执行分支业务sql但不提交&lt;/li&gt;
&lt;li&gt;RM报告执行状态到TC&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;二阶段的工作：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;TC检测各分支事务执行状态
&lt;ul&gt;
&lt;li&gt;如果都成功，通知所有RM提交事务&lt;/li&gt;
&lt;li&gt;如果有失败，通知所有RM回滚事务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RM接收TC指令，提交或回滚事务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;总结&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;XA模式的优点是什么?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事务的强一致性，满足ACID原则。&lt;/li&gt;
&lt;li&gt;常用数据库都支持，实现简单，并且没有代码侵入&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;XA模式的缺点是什么?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为一阶段需要锁定数据库资源，等待二阶段结束才释放，性能较差&lt;/li&gt;
&lt;li&gt;依赖关系型数据库实现事务&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;实现XA模式&lt;/h4&gt;
&lt;p&gt;Seata的starter已经完成了XA模式的自动装配，实现非常简单，步骤如下&lt;/p&gt;
&lt;p&gt;1.修改application.yml文件(每个参与事务的微服务)，开启XA模式:&lt;/p&gt;
&lt;p&gt;我们已经在nacos中配置了，所以直接去nacos中修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;seata:
  data-source-proxy-mode: XA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;seata:
  registry: # TC服务注册中心的配置，微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 192.168.146.131:8848 # nacos地址
      namespace: &quot;&quot; # namespace，默认为空
      group: DEFAULT_GROUP # 分组，默认是DEFAULT_GROUP
      application: seata-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: hmall # 事务组名称
  service:
    vgroup-mapping: # 事务组与tc集群的映射关系
      hmall: &quot;default&quot;
  data-source-proxy-mode: XA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其次，我们要利用@GlobalTransactional标记分布式事务的入口方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
@GlobalTransactional
public Long createOrder(OrderFormDTO orderFormDTO) {



@Override
@Transactional
public void deductStock(List&amp;lt;OrderDetailDTO&amp;gt; items) {


@Override
@Transactional
public void removeByItemIds(Collection&amp;lt;Long&amp;gt; itemIds) {
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AT模式&lt;/h3&gt;
&lt;p&gt;Seata主推的是AT模式，AT模式同样是分阶段提交的事务模型，不过缺弥补了XA模型中资源锁定周期过长的缺陷。&lt;/p&gt;
&lt;p&gt;流程图：
&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/Snipaste_2025-05-22_20-06-29.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;阶段一RM的工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注册分支事务&lt;/li&gt;
&lt;li&gt;记录undo-log（数据快照）&lt;/li&gt;
&lt;li&gt;执行业务sql并提交&lt;/li&gt;
&lt;li&gt;报告事务状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;阶段二提交时RM的工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除undo-log即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;阶段二回滚时RM的工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据undo-log恢复数据到更新前&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;简述AT模式与XA模式最大的区别是什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;XA模式一阶段不提交事务，锁定资源；AT模式一阶段直接提交，不锁定资源。&lt;/li&gt;
&lt;li&gt;XA模式依赖数据库机制实现回滚；AT模式利用数据快照实现数据回滚。&lt;/li&gt;
&lt;li&gt;XA模式强一致；AT模式最终一致&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;AT模式的使用&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在每个服务的数据库中，执行sql生成一张表
sql地址：&lt;a href=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/sql/seata-at.sql&quot;&gt;seata-at.sql&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在nacos中修改配置文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;seata:
  data-source-proxy-mode: AT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里默认不填即是AT模式&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://gitee.com/zz_yang/hmall&quot;&gt;地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;reference:&lt;a href=&quot;https://gitee.com/huyi612/hmall&quot;&gt;reference&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>通讯技术</title><link>https://zzyang.top/posts/xiaoxi-socket/</link><guid isPermaLink="true">https://zzyang.top/posts/xiaoxi-socket/</guid><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;WebSocket 实现简易聊天室&lt;/h1&gt;
&lt;h2&gt;消息推送的常见方式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;1.轮询 浏览器以指定的时间间隔向服务器发出HTTP请求，服务器实时遮回数据给浏览器

2.长轮询 浏览器发出ajax请求，服务器端接收到请求后，会阻塞请求直到有数据或者超时才返回

3.SSE（server-sent event）服务器发送事件
	SSE在服务器和客户端之间打开一个单向通道
	服务端响应的不再是一次性的数据包，而是text/event-stream类型的数据流信息
	服务器有数据变更时将数据流式传输到客户端

4.WebSocket Websocket是一种在基于TCP连接上进行全双工通信的协议
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
全双工: 全双工(Full Duplex):允许数据在两个方向上同时传输&lt;/p&gt;
&lt;p&gt;半双工: 半双工(Half Duplex):允许数据在两个方向上传输，但是同一个时间段内只允许一个方向上传输。
:::&lt;/p&gt;
&lt;h2&gt;客户端&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;客户端方法&lt;/strong&gt;
&lt;img src=&quot;../../assets/img//XMAUTa8Ix7Sri2D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码概览&lt;/strong&gt;
&lt;img src=&quot;../../assets/img//x4iEm7ypW6i76OB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;服务端&lt;/h2&gt;
&lt;p&gt;Tomcat的7.0.5 版本开始支持WebSocket,并且实现了Java WebSocket规范。&lt;/p&gt;
&lt;p&gt;Java Websocket应用&amp;lt;span class=&quot;marker-evy&quot;&amp;gt;由一系列的Endpoint组成&amp;lt;/span&amp;gt;。Endpoint 是一个java对象，代表Websocket链接的一端，对于服务端，我们可以视为处理具体WebSocket消息的接口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何定义Endpoint?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;我们可以通过两种方式定义Endpoint:&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第一种是编程式, 即继承类javax.websocket.Endpoint并实现其方法
第二种是注解式, 即定义一个POJO, 并添加@ServerEndpoint相关注解
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生命周期方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Endpoint实例在Websocket握手时创建，并在客户端与服务端链接过程中有效，最后在链接关闭时结束。
在Endpoint接口中明确定义了与其生命周期相关的方法， 规范实现者确保生命周期的各个阶段调用实例的相关方法。
生命周期方法如下:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img//cEeZOCyjvyqHb5e.png&quot; alt=&quot;&quot; /&gt;
:::danger
⬆ 没有onError，应为onMessage   @OnMessage&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;服务端如何接收客户端发送的数据？&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1.编程式	通过添加 MessageHandler 消息处理器来接收消息
2.注解式	在定义Endpoint时，通过@OnMessage注解指定接收消息的方法
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;服务端如何推送数据给客户端？&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;发送消息则由 RemoteEndpoint 完成，其实例由Session 维护发送消息有2种方式发送消息
方式1：通过session.getBasicRemote 获取同步消息发送的实例，然后调用其 sendXxx()方法发送消息
方式2：通过session.getAsyncRemote 获取异步消息发送实例，然后调用其 sendXxx()方法发送消息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;代码概览&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img//8eF07sLPVUcTehL.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;SpringBoot整合Websocket&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;引入依赖&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--websocket--&amp;gt;
&amp;lt;dependency&amp;gt;
	&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;spring-boot-starter-websocket&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配置类&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;编写配置类，扫描添加有@ServerEndpoint注解的Bean
该配置类会自动扫描带有@ServerEndpoint注解的
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;编写配置类，用于获取HttpSession对象
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 获取HttpSession对象
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        // 将httpSession对象保存起来
        sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;定义Bean，在@serverEndpoint中引入配置器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ServerEndpoint(value = &quot;/chat&quot;, configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndPoint {

    private static final Map&amp;lt;String, Session&amp;gt; onlineUsers = new ConcurrentHashMap&amp;lt;&amp;gt;();

    private HttpSession httpSession;

    /**
     * 建立websocket连接后，被调用
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        // 1.将session进行保存
        this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        String userName = (String) this.httpSession.getAttribute(&quot;userName&quot;);
        onlineUsers.put(userName, session);
        // 2. 广播消息，需要将登录的所有用户推送给所有用户
        String message = MessageUtil.getMessage(true, null, getAllUserName());
        broadcastAllUsers(message);
    }

    /**
     * 浏览器发送消息到服务端，该方法被调用
     *
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        // 将消息推送给指定用户
        Message msgObj = JSON.parseObject(message, Message.class);
        // 获取消息接收方
        String toName = msgObj.getToName();
        String msg = msgObj.getMessage();
        // 接收方 session对象
        Session session = onlineUsers.get(toName);
        String sendMsg = MessageUtil.getMessage(false, (String) this.httpSession.getAttribute(&quot;userName&quot;), msg);
        try {
            session.getBasicRemote().sendText(sendMsg);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * 断开websocket连接时被调用
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {
        // 1.从onlineUsers中剔除当前用户的session对象
        onlineUsers.remove((String) this.httpSession.getAttribute(&quot;userName&quot;));
        // 2.通知其他所有用户，当前用户下线了
        String message = MessageUtil.getMessage(true, null, getAllUserName());
        broadcastAllUsers(message);
    }

    private Set getAllUserName() {
        Set&amp;lt;String&amp;gt; strings = onlineUsers.keySet();
        return strings;
    }

    private void broadcastAllUsers(String message) {
        onlineUsers.forEach((k, v) -&amp;gt; {
            // 获取所有用户的session对象
            Session session = v;
            // 发送消息
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        });
    }


}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在onOpen中的&amp;lt;span class=&quot;marker-evy&quot;&amp;gt;EndpointConfig&amp;lt;/span&amp;gt; 与 GetHttpSessionConfig类中modifyHandshake方法的&amp;lt;span class=&quot;marker-evy&quot;&amp;gt;ServerEndpointConfig&amp;lt;/span&amp;gt;这俩其实是一个对象&lt;/p&gt;
&lt;p&gt;保存HttpSession对象时
&lt;img src=&quot;../../assets/img//ji2De3gXKkEoI3e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;用到的代码片段&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class MessageUtil {

    public static String getMessage(boolean isSystemMessage, String fromName, Object message) {
        ResultMessage resultMessage = new ResultMessage();
        resultMessage.setMessage(message);
        resultMessage.setSystem(isSystemMessage);
        if (!ObjectUtils.isEmpty(fromName)) {
            resultMessage.setFromName(fromName);
        }
        return JSON.toJSONString(resultMessage);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@AllArgsConstructor
@NoArgsConstructor
@Data
public class Message {
    private String toName;
    private String message;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultMessage {
    private boolean system;
    private Object message;
    private String fromName;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;消息格式&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img//z5utPGrgh40LVSE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;SSE(Server-Sent Events)&lt;/h1&gt;
&lt;h2&gt;什么是 SSE&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;服务器发送事件(Server-Sent Events，简称SSE)
SSE，就是浏览器向服务器发送一个HTTP请求，然后服务器不断单向地向浏览器推送“信息”(message)。
这种信息在格式上很简单，就是“信息”加上前缀“data:”，然后以“\n\n”结尾。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SSE 应用场景&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;服务器向浏览器“发送”数据，比如，每当收到新的电子邮件，服务器就向浏览器发送一个“通知”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img//UQ51t4Il.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;SSE 与 Websocket&lt;/h2&gt;
&lt;p&gt;SSE 与 WebSocket 有相似功能，都是用来建立浏览器与服务器之间的通信渠道。两者的区别在于:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WebSocket 是全双工通道，可以双向通信，功能更强;SSE 是单向通道，只能服务器向浏览器端发送。&lt;/li&gt;
&lt;li&gt;SSE 是一个轻量级协议，相对简单; WebSocket 是一种较重的协议，相对复杂。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;实现方式 1&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;前端&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img//JRdzSgdW.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;后端&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/messageObtain&quot;)
@Slf4j
public class MessageNoticeController {

    @PostMapping(&quot;/getStreamData&quot;)
    public String getStreamData(HttpServletResponse response) {
        response.setContentType(&quot;text/event-stream&quot;);
        response.setCharacterEncoding(&quot;utf-8&quot;);
        String str = &quot;&quot;;
        while (true) {
            str = &quot;data:&quot; + new Date() + &quot;\n\n&quot;;
            PrintWriter writer = null;
            try {
                Thread.sleep(1000);
                writer = response.getWriter();
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            }
            writer.write(str);
//            log.info(str);
            writer.flush();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::: tip
响应是主要的&lt;/p&gt;
&lt;p&gt;response.setContentType(&quot;text/event-stream&quot;);
:::&lt;/p&gt;
&lt;h2&gt;实现方式 2&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;前端&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;SSE Messages&amp;lt;/h1&amp;gt;
    &amp;lt;ul&amp;gt;
      &amp;lt;li v-for=&quot;message in messages&quot; :key=&quot;message.id&quot;&amp;gt;{{ message.data }}&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { onMounted, ref } from &apos;vue&apos;;

interface SseMessage {
  id: string;
  data: string;
}

const messages = ref&amp;lt;SseMessage[]&amp;gt;([]);

onMounted(() =&amp;gt; {
  const eventSource = new EventSource(&apos;http://localhost:8080/api/sse/events&apos;);

  eventSource.onmessage = (event) =&amp;gt; {
    const parsedData: SseMessage = JSON.parse(event.data);
    messages.value = [...messages.value, parsedData];
  };

  eventSource.onerror = () =&amp;gt; {
    console.error(&apos;EventSource failed.&apos;);
    eventSource.close();
  };
});
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
/* Add some styles here */
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;后端&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.demo.controller;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@RequestMapping(&quot;/api/sse&quot;)
public class SseController {

    private final ExecutorService executorService = Executors.newCachedThreadPool();

    @GetMapping(value = &quot;/events&quot;, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter handleSse() {
        // 设置超时时间为1小时
        SseEmitter emitter = new SseEmitter(3600000L);

        // 处理客户端断开连接回调
        emitter.onCompletion(() -&amp;gt; {
            System.out.println(&quot;Connection completed&quot;);
        });

        // 设置超时回调
        emitter.onTimeout(() -&amp;gt; {
            System.out.println(&quot;Connection timed out&quot;);
            emitter.complete();
        });
         // 设置错误回调
        emitter.onError((ex) -&amp;gt; {
            System.err.println(&quot;SSE 连接出错: &quot; + ex.getMessage());
            emitter.completeWithError(ex);
        });

        // 模拟每两秒发送一次消息到客户端，总共发送十次
        executorService.execute(() -&amp;gt; {
            try {
                for (int i = 0; i &amp;lt; 10; i++) {
                    SseEmitter.SseEventBuilder event = SseEmitter.event()
                            .data(&quot;SSE message &quot; + i)
                            .id(&quot;&quot; + i)
                            .name(&quot;sseEvent&quot;);

                    emitter.send(event);
                    Thread.sleep(2000);
                }
                emitter.complete();
            } catch (IOException | InterruptedException e) {
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现方式 3&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package com.example.ssedemo.controller;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@RequestMapping(&quot;/sse&quot;)
public class SseController {

    private final ExecutorService executor = Executors.newCachedThreadPool();

    @GetMapping(value = &quot;/stream&quot;, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter streamEvents() {
        // 创建一个 SseEmitter 实例，设置超时时间为 0（永不超时）
        SseEmitter emitter = new SseEmitter(0L);

        // 异步发送事件
        executor.execute(() -&amp;gt; {
            try {
                for (int i = 1; i &amp;lt;= 10; i++) {
                    // 模拟每秒发送一次数据
                    Thread.sleep(1000);
                    emitter.send(SseEmitter.event()
                            .id(String.valueOf(i))
                            .name(&quot;message&quot;)
                            .data(&quot;Event &quot; + i));
                }
                // 完成事件流
                emitter.complete();
            } catch (IOException | InterruptedException e) {
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;Server-Sent Events (SSE) Demo&amp;lt;/h1&amp;gt;
    &amp;lt;button @click=&quot;startSSE&quot;&amp;gt;Start SSE&amp;lt;/button&amp;gt;
    &amp;lt;ul&amp;gt;
      &amp;lt;li v-for=&quot;(event, index) in events&quot; :key=&quot;index&quot;&amp;gt;{{ event }}&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &apos;vue&apos;;

// 定义响应式变量
const events = ref&amp;lt;string[]&amp;gt;([]);
let eventSource: EventSource | null = null;

// 启动 SSE 连接
const startSSE = () =&amp;gt; {
  if (eventSource) {
    eventSource.close(); // 如果已经有连接，先关闭
  }

  // 创建 EventSource 连接到后端 SSE 端点
  eventSource = new EventSource(&apos;http://localhost:8080/sse/stream&apos;);

  // 监听消息事件
  eventSource.addEventListener(&apos;message&apos;, (event: MessageEvent) =&amp;gt; {
    console.log(&apos;Received event:&apos;, event.data);
    events.value.push(event.data); // 将接收到的消息添加到列表中
  });

  // 监听错误事件
  eventSource.onerror = (error: Event) =&amp;gt; {
    console.error(&apos;EventSource failed:&apos;, error);
    eventSource?.close(); // 关闭连接
  };
};
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 5px 0;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Java 线程池</title><link>https://zzyang.top/posts/java-threadpool/</link><guid isPermaLink="true">https://zzyang.top/posts/java-threadpool/</guid><pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;线程池&lt;/h1&gt;
&lt;h2&gt;为什么要使用线程池？&lt;/h2&gt;
&lt;p&gt;在没有使用线程池之前，是这样执行任务的&lt;/p&gt;
&lt;p&gt;一个线程只能执行一个任务，不能连续执行任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Task implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Task task = new Task();
        Thread thread = new Thread(task);
        thread.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/230480sdaf.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这暴露了一个问题，线程不能复用，重复创建和销毁线程耗时耗资源，若能复用就好了，复用的好处就是省时省资源&lt;/p&gt;
&lt;p&gt;看线程池如何执行⤵️&lt;/p&gt;
&lt;p&gt;创建只有一个线程的线程池，这个线程池中只有一个线程，重点是它里面的线程可以复用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Runnable task1 = new Task();
        Runnable task2 = new Task();
        Runnable task3 = new Task();

        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        threadPool.execute(task1);
        threadPool.execute(task2);
        threadPool.execute(task3);

        threadPool.shutdown();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明一个线程执行了3个任务
&lt;img src=&quot;../../assets/img/30294805.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;线程池的好处&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗&lt;/li&gt;
&lt;li&gt;提高响应速度:当有任务时，任务可以不需要等到线程创建就能立即执行&lt;/li&gt;
&lt;li&gt;提高线程的可管理性:线程是稀缺资源，如果无限制的创建，不仅会消耗系统资源，还会降低系统的稳定性，线程池可以进行统一的分配，调优和监控&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;什么是线程池&lt;/h2&gt;
&lt;p&gt;线程池是一种基于池化思想管理线程的工具&lt;/p&gt;
&lt;p&gt;在没有线程池之前，当有任务需要执行时，我们会创建一个线程，然后将任务传递给线程，并且一个线程只能执行一个任务，如果还有任务，我们就只能再创建一个线程去执行它，当任务执行完时，线程就销毁了，重复创建和销毁线程是一件很耗时耗资源的事，如果线程能复用，那么就减少很多不必要的消耗，于是线程池就孕育而生了，事先将线程创建好，当有任务需要执行时，提交给线程池，线程池分配线程去执行，有再多的任务也不怕线程池中的线程能复用，执行完一个任务，再接着执行其他任务，当所有任务都执行完时，我们可以选择关闭线程池，也可以选择等待接收任务。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h2&gt;原生方式创建线程池&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;线程池核心UML类图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1414142354364363.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ThreadPoolExecutor&lt;/code&gt;是线程池核心类，它一共有四个构造方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/fjowieqgongfwopej.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最复杂的说起，它一共有7个参数⤵️&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ThreadPoolExecutor(int corePoolSize,
                        int maximumPoolSize,
                        long keepAliveTime,
                        TimeUnit unit,
                        BlockingQueue&amp;lt;Runnable&amp;gt; workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数含义：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;corePoolSize&lt;/td&gt;
&lt;td&gt;核心线程数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maximumPoolSize&lt;/td&gt;
&lt;td&gt;最大线程数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepAliveTime&lt;/td&gt;
&lt;td&gt;空闲线程存活时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unit&lt;/td&gt;
&lt;td&gt;时间单位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;workQueue&lt;/td&gt;
&lt;td&gt;任务队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;threadFactory&lt;/td&gt;
&lt;td&gt;线程工厂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;handler&lt;/td&gt;
&lt;td&gt;拒绝任务策略&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;参数解析：&lt;/strong&gt;
&lt;img src=&quot;../../assets/img/wqeoirugnoasidfj.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;核心线程的好处：只要线程池不关闭，就不会被销毁&lt;/p&gt;
&lt;p&gt;最大线程数：表示线程池中最多允许有25个线程&lt;/p&gt;
&lt;p&gt;除去核心线程之外的线程是非核心线程，非核心线程没有执行任务的话，是要被清理的，在被清理之前能存活多久取决于第三个和第四个参数；
当任务都执行完以后，所有线程都成了空闲线程，还是要分核心与非核心线程，再过十秒，若非核心线程没有工作，就要被销毁，剩下的都是核心线程&lt;/p&gt;
&lt;p&gt;workQueue是任务队列，线程池中的线程们也都是在这领取的任务，我一般用的是 &lt;code&gt;LinkedBlockingQueue&lt;/code&gt;链式阻塞队列，基于链表的阻塞队列；还有一个常用的是&lt;code&gt;ArrayBlockingQueue&lt;/code&gt;数组阻塞队列，这是一个基于数组的阻塞队列&lt;/p&gt;
&lt;p&gt;threadFactory：线程工厂，指定线程该如何生产，它是一个接口,实现它里面的 newThread 方法,可以自定义线程的相关设置; 例如：可以指定线程名称，还可以指定是否为后台线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface ThreadFactory {

    /**
    *  构造一个新线程，可以指定线程名称、优先级等等
    *
    * @param r 新线程执行的任务
    * @return 构造的新线程，如果创建线程的请求被拒绝，则为 nulL
    */
    Thread newThread(Runnable r);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/4368029840918.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你不想自定义线程工厂，那么可以使用，Executors类中的默认线程工程
&lt;img src=&quot;../../assets/img/23-49-230486867.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;handler：任务拒绝策略，什么情况下我们提交给线程池的任务会被拒绝呢，要满足以下四种情况&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程池中线程已满&lt;/li&gt;
&lt;li&gt;无法继续扩容&lt;/li&gt;
&lt;li&gt;没有空闲线程，所有线程都在执行任务&lt;/li&gt;
&lt;li&gt;任务队列已满，无法再存入新任务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同时满足这四种情况时，我们提交给线程池的任务才会被拒绝&lt;/p&gt;
&lt;p&gt;线程池拒绝我们的方式也有四种：
&lt;img src=&quot;../../assets/img/230948U08409234.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;p&gt;自定义线程工厂&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CustomThreadFactory implements ThreadFactory {
    private final AtomicInteger i = new AtomicInteger(1);

    @Override
    public Thread newThread(Runnable r) {
        // 创建线程，执行任务
        Thread thread = new Thread(r);
        // 设置线程名称
        thread.setName(&quot;线程&quot; + i.getAndIncrement() + &quot;号&quot;);
        // 返回线程
        return thread;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Task implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        // 创建任务
        Runnable task1 = new Task();
        Runnable task2 = new Task();
        Runnable task3 = new Task();
        // 创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 25, 10L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue&amp;lt;&amp;gt;(),
                new CustomThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        // 提交任务
        threadPoolExecutor.execute(task1);
        threadPoolExecutor.execute(task2);
        threadPoolExecutor.execute(task3);
        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/230586006707982731.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;这 3 种创建线程池的方式有风险&lt;/h2&gt;
&lt;p&gt;分别是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;固定大小线程池 &lt;code&gt;FixedThreadPool&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;单个线程的线程池 &lt;code&gt;SingleThreadExecuton&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;可缓存的线程池 &lt;code&gt;CachedThreadPool&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这三种创建方式都在Executors 工具类中
&lt;img src=&quot;../../assets/img/124987597912395646gggg.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所有已new开头的都能创建线程池
&lt;img src=&quot;../../assets/img/sadlfjweoinbszc.png&quot; alt=&quot;&quot; /&gt;
一共有12个这样的方法，去掉重载方法后，就剩下6个
&lt;img src=&quot;../../assets/img/sadfkjl4598982.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;FixedThreadPool&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/weqoru23948.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;FixedThreadPool，它内部采用 &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;来创建线程池，核心线程数和最大线程数一样，意味着它里面全是核心线程；空闲线程存活时间为0毫秒，这样空闲线程就不会被销毁，任务队列采用的是 &lt;code&gt;LinkedBlockingQueue&lt;/code&gt;;&lt;/p&gt;
&lt;p&gt;此队列有资源耗尽的风险，因为&lt;code&gt;LinkedBlockingQueue&lt;/code&gt;的容量为Interger的最大值，Interger的最大值是2&amp;lt;sup&amp;gt;31&amp;lt;/sup&amp;gt;，意味着任务队列中的任务数量可高达 21亿之多，内存随时都有可能爆掉，不推荐使用 &lt;code&gt;FixedThreadPool&lt;/code&gt;;
&lt;img src=&quot;../../assets/img/230498gjwlwjg.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在《阿里巴巴Java开发手册》的第一章第7小节，第4条中这样写道⤵️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/asdl;fkj23947.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;创建固定大小的线程池有两个方法⤵️
&lt;img src=&quot;../../assets/img/0239481975069172.png&quot; alt=&quot;&quot; /&gt;
演示一个参数的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Task implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        // 创建任务
        Runnable task1 = new Task();
        Runnable task2 = new Task();
        Runnable task3 = new Task();
        // 创建线程池
        ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(10);
        // 提交任务
        threadPoolExecutor.execute(task1);
        threadPoolExecutor.execute(task2);
        threadPoolExecutor.execute(task3);
        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;运行结果&lt;/em&gt;
&lt;img src=&quot;../../assets/img/1239085710238.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;SingleThreadExecutor&lt;/h3&gt;
&lt;p&gt;它内部也采用 &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;来创建线程池，核心线程数和最大线程数一样，意味着它里面全也都是核心线程，只不过只有一个，空闲线程存活时间为0毫秒，它里面的空闲线程也不会被销毁，任务队列采用的是 &lt;code&gt;LinkedBlockingQueue&lt;/code&gt;，风险和FixedThreadPool一样的问题，不推荐使用 &lt;code&gt;SingleThreadExecutor&lt;/code&gt;
&lt;img src=&quot;../../assets/img/12398591237591298.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;方法:
&lt;img src=&quot;../../assets/img/1894012227162210304.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        // 创建任务
        Runnable task1 = new Task();
        Runnable task2 = new Task();
        Runnable task3 = new Task();
        // 创建线程池
        ExecutorService threadPoolExecutor = Executors.newSingleThreadExecutor();
        // 提交任务
        threadPoolExecutor.execute(task1);
        threadPoolExecutor.execute(task2);
        threadPoolExecutor.execute(task3);
        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序输出三个一样的线程名称，说明线程池中的确只有一个线程
&lt;img src=&quot;../../assets/img/1894013511424540672.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;CachedThreadPool&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894015233911947264.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可缓存的线程池，可缓存的意思是，它里面除了核心线程以外，还有非核心线程，他内部也采用了&lt;code&gt;ThreadPoolExecutor&lt;/code&gt;来创建线程池，核心线程数是0，意味着可缓存的线程池里面全都是非核心线程，最大线程数是 Integer的最大值，风险不言而喻，弊端就不再赘述了，和前面两个线程池的问题一样，不推荐使用 &lt;code&gt;CachedThreadPool&lt;/code&gt;，空闲线程存活时间为 60 秒，空闲线程 60 秒内没工作就会被销毁，任务队列采用的是 &lt;code&gt;SynchronousQueue&lt;/code&gt;，这是一个同步队列。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894014879807832064.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;创建可缓存的线程池有两个方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894016177919754240.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        // 创建任务
        Runnable task1 = new Task();
        Runnable task2 = new Task();
        Runnable task3 = new Task();
        // 创建线程池
        ExecutorService threadPoolExecutor = Executors.newCachedThreadPool();
        // 提交任务
        threadPoolExecutor.execute(task1);
        threadPoolExecutor.execute(task2);
        threadPoolExecutor.execute(task3);
        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序输出三个不一样的线程名称&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894016802153824256.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FixedThreadPool&lt;/code&gt;:固定大小的线程池&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SingleThreadExecutor&lt;/code&gt;:单个线程的线程池&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CachedThreadPool&lt;/code&gt;:可缓存的线程池&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FixedThreadPool&lt;/code&gt;和&lt;code&gt;SingleThreadExecutor&lt;/code&gt; 允许的请求队列长度为 &lt;code&gt;Integer.MAX_VALUE&lt;/code&gt;，可能会堆积大量的请求，从而导致00M。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CachedThreadPool&lt;/code&gt; 允许的创建线程数量为&lt;code&gt;Integer.MAX_VALUE&lt;/code&gt;可能会创建大量的线程，从而导致 00M。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;execute与submit&lt;/h2&gt;
&lt;h3&gt;execute&lt;/h3&gt;
&lt;p&gt;execute位于Executor接口中，作用是向线程池中提交Runnable任务，Runnable是无返回值的任务，也就是它执行完是没有结果返回给你的，所以，execute 只适合提交无返回值的任务，如果你的任务是有返回结果的，那么你就要创建Callable任务，它是一个有返回值的任务，Callable任务执行完，会将任务执行结果封装到 Future 对象中，然后返回给调用者，调用者再通过 Future 对象获取结果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894019796459061248.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Task implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Runnable task = new Task();

        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        threadPool.execute(task);

        threadPool.shutdown();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序只输出了一个线程名称&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894020551345700864.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;submit&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894021498507624448.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;该方法位于 ExecutorService 接口中，一共有三个 submit 方法，他们的作用稍有不同，我将三个方法的作用分别列举出来&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894021699037298688.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它们的返回值类型都是 Future 类型，而且都带泛型，任务执行结果就封装在 Future 对象里面，Future是一个接口，该接口定义了与任务执行结果相关的功能&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894022094186872832.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是 Future 的 UML类图，他一共有5个可用的方法，这5个方法的作用如图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894022346788831232.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894022599864745984.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;回到submit本身&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894022966388195328.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一个方法，它的作用是提交 Runnable 任多，submit 方法也可以提交 Runnable 任务，方法返回一个 Future 对象，都无返回值了，为什么还有返回Future对象呢？因为 Future 除了获取任务执行结果以外，还可以观察任务是否执行完毕，以及取消任务，等等操作，所以，Future 对象你可以选择接收，你也可以选择不接收，我们来演示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Runnable task = new Task();

        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 提交任务
        Future&amp;lt;?&amp;gt; future = threadPool.submit(task);
        try {
            // 获取任务执行结果
            Object result = future.get();
            // 输出任务执行结果
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            // 关闭线程池
            threadPool.shutdown();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;null是我们获取的任务执行结果，因为我们提交的是无返回值任务，所以结果为null，如果我们非要给无返回值任务一个结果，是否可以？那也是可以的，这就是我们要介绍第二个 submit 方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894024547917627392.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第二个方法，它的作用就是提交一个 Runnable 任务给线程池，并且还可以附带一个执行结果，别的任务都是执行完才知道结果，这个 submit 方法是执行任务之前，都已经知道了任务执行结果，所以，它只适用于，执行任务的同时还要附带一个参数的场景，该方法依然是返回一个 Futrue 对象，这个 Futrue 对象里面装的结果，就是我们刚刚传递的第二个参数；示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Runnable task = new Task();

        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 提交任务
        Future&amp;lt;String&amp;gt; future = threadPool.submit(task, &quot;任务完成&quot;);
        try {
            // 获取任务执行结果
            String result = future.get();
            // 输出任务执行结果
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            // 关闭线程池
            threadPool.shutdown();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894026032231481344.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再介绍最后一个 submit 方法，它的作用是提交 Callable 任务，也就是有返回值的任务，方法是返回一个 Futrue 对象，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ResultTask implements Callable&amp;lt;Integer&amp;gt; {
    @Override
    public Integer call() throws Exception {
        return 1 + 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        ResultTask task = new ResultTask();

        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 提交任务
        Future&amp;lt;Integer&amp;gt; future = threadPool.submit(task);
        try {
            // 获取任务执行结果
            Integer result = future.get();
            // 输出任务执行结果
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            // 关闭线程池
            threadPool.shutdown();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;符合预期
&lt;img src=&quot;../../assets/img/1894027235220783104.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;execute与submit 的区别&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894027602381766656.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/img/1894027910621167616.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Java SE Interview</title><link>https://zzyang.top/posts/javase-mianshi/</link><guid isPermaLink="true">https://zzyang.top/posts/javase-mianshi/</guid><pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;面试指北&lt;/h1&gt;
&lt;h2&gt;Java 基础&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;==&lt;strong&gt;和&lt;/strong&gt;equals&lt;/strong&gt;的区别是什么？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;==既可以比较基本数据类型,也可以比较引用数据类型&lt;/li&gt;
&lt;li&gt;==在比较基本数据类型时, 比较的是值, 在比较引用数据类型时, 比较对象的地址&lt;/li&gt;
&lt;li&gt;equals 只能比较引用数据类型, 比较对象中的内容是否相同, 在没有重写的情况下也是比较对象的地址&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;String str=&quot;i&quot; 与 String str=new String(&quot;i&quot;) 一样吗？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;String str=&quot;i&quot;会将其分配到常量池中，常量池中没有重复的元素，如果常量池中存在 i，就将 i 的地址赋给变量，如果没有就创建一个再赋给变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符串字面量，通常存储在 JVM 的字符串常量池中。常量池中没有重复的元素，如果字符串常量池中已经存在相同的字符串（例如，之前已经使用过 &quot;i&quot;），则不会创建新的对象，而是直接引用池中的现有对象。如果没有就创建一个再赋给变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;String str=new String(“i”)会将对象分配到堆中，即使内存一样，还是会重新创建一个新的对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;接口和抽象类有什么区别？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;声明方法的存在而不去实现它的类叫抽象类。接口是抽象类的变体，接口中的所有方法都是抽象的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接口可以被多个类实现，而一个类只能继承一个抽象类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接口中的成员变量默认为 public static final 类型，而抽象类中的成员变量可以是 public、protected、private 等各种类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接口不能包含抽象构造方法和抽象静态方法，而抽象类可以有构造方法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接口中的方法只有声明，没有具体实现，实现接口的类需要提供具体的实现。
抽象类中的方法可以有具体的实现，子类可以直接继承并使用。抽象类的子类必须为父类中的所有抽象方法提供实现，否则它自己也是抽象类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jdk1.8 之后，接口可以使用 default 关键字，实现类是默认实现的，那个实现类需要使用，再具体实现就可以&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;抽象类与接口都不能被实例化，但是可以指向具体的子类的实例&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;String 有哪些常用方法？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;equals&lt;/li&gt;
&lt;li&gt;contains&lt;/li&gt;
&lt;li&gt;trim&lt;/li&gt;
&lt;li&gt;getBytes&lt;/li&gt;
&lt;li&gt;toUpperCase&lt;/li&gt;
&lt;li&gt;toLowerCase&lt;/li&gt;
&lt;li&gt;replace&lt;/li&gt;
&lt;li&gt;indexOf&lt;/li&gt;
&lt;li&gt;substring&lt;/li&gt;
&lt;li&gt;startWith&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;什么是反射？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;反射就是动态加载对象，并对对象进行剖析，通过 class、constructor、field、method 四个方法，获取一个类的各个组成部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;对于任意一个类，都能够知道这个类的所有属性和方法；对于任意一个对象，都能够调用它的任意一个方法，这种动态获取信息以及动态调用对象方法的功能称为 Java 反射机制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;反射获取实例对象的方式&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;使用 Class 类的静态方法 forName()&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用 Class.forName()&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过的类的 class 属性获得该类的 Class 实例&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;类名.Class&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;利用对象调用 getClass()方法获得对象的 Class 实例&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对象.getClass()&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基本数据类型封装，可以用 TYPE 属性获得对应数据类型的 Class 实例&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;封装类.TYPE&lt;/p&gt;
&lt;h3&gt;JDK 代理与 CGLIB 代理的区别&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 JDK 代理的委托类对象必须基于接口，目标类必须实现至少一个接口；使用 CGLib 代理的委托对象不需要实现接口&lt;/li&gt;
&lt;li&gt;CGLib 通过生成目标类的子类来实现代理， 有一些限制不能代理 final 类、不能代理 final 方法&lt;/li&gt;
&lt;li&gt;CGLib 第一次加载较慢，因为需要生成子类&lt;/li&gt;
&lt;li&gt;JDK 代理是 Java 原生支持不需要依赖, CGLib 通过继承方式代理 需要依赖 jar 包
:::info
Spring 会根据情况自动选择代理方式：
&lt;ol&gt;
&lt;li&gt;如果目标类实现了接口，默认使用 JDK 动态代理&lt;/li&gt;
&lt;li&gt;如果目标类没有实现接口，使用 CGLib
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;JDK 代理&lt;/strong&gt;
:::tip
JDK 内置的 Proxy 动态代理通过反射机制在运行时动态地创建代理类。被代理的类必须实现接口，未实现接口则没办法完成动态代理。JDK 代理的核心是&lt;code&gt;Proxy&lt;/code&gt;类和&lt;code&gt;InvocationHandler&lt;/code&gt;接口。&lt;code&gt;Proxy&lt;/code&gt;类通过调用其静态方法&lt;code&gt;newProxyInstance()&lt;/code&gt;来返回代理对象，而&lt;code&gt;InvocationHandler&lt;/code&gt;接口中的&lt;code&gt;invoke&lt;/code&gt;方法则使用反射在目标对象上调用方法并传入参数。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CGLIB 代理&lt;/strong&gt;
:::tip
CGLIB 在运行时动态生成被代理类的子类，并重写父类中的方法来实现代理功能。cglib 可以在运行时动态生成字节码，cglib 继承被代理的类，重写方法，织入通知，动态生成字节码并运行，通过 &lt;code&gt;MethodInterceptor&lt;/code&gt; 接口来实现方法调用的拦截，重写 intercept，在调用方法的前后织入横切内容。&lt;/p&gt;
&lt;p&gt;使用 cglib 可以实现动态代理，即使被代理的类没有实现接口，但被代理的类必须不是 final 类。
:::
可以看看这篇文章
&lt;a href=&quot;https://www.cnblogs.com/best/p/5679656.html#_label0&quot;&gt;为什么需要代理模式&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;throw 和 throws 的区别？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;throw&lt;/strong&gt;：
throw 语句用在方法体内，表示抛出异常，由方法体内的语句处理。&lt;/li&gt;
&lt;li&gt;throw 是具体向外抛出异常的动作，所以它抛出的是一个异常实例对象(任何 &lt;code&gt;Throwable&lt;/code&gt; 类型的实例)，执行 throw 一定是抛出了某种异常。该语句后面的代码将不成执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;throws&lt;/strong&gt;：
throws 语句是用在方法声明后面，表示如果抛出异常，由该方法的调用者来进行异常的处理。&lt;/li&gt;
&lt;li&gt;throws 主要是声明这个方法会抛出某种类型的异常(只能声明 &lt;code&gt;Exception&lt;/code&gt; 类型及其子类的异常)，让它的使用者要知道需要捕获的异常的类型。throws 表示出现异常的一种可能性，并不一定会发生这种异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;final、finally、finalize 有什么区别？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;final 可以修饰类，变量，方法，修饰的类不能被继承，修饰的变量不能重新赋值，修饰的方法不能被重写&lt;/li&gt;
&lt;li&gt;finally 是异常处理语句结构的一部分，表示总是执行。常用于一些流的关闭。&lt;/li&gt;
&lt;li&gt;finalize 是 Object 类的一个方法，当对象被垃圾回收器（GC）回收之前，会调用此方法。Java 9 后标记为废弃（@Deprecated），不推荐使用。有更好的替代方案（AutoCloseable，try-with-resources ，Cleaner）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常见的异常类有哪些？&lt;/h3&gt;
&lt;p&gt;运行时异常：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ArithmeticException（算术异常）&lt;/li&gt;
&lt;li&gt;ClassCastException （类转换异常）&lt;/li&gt;
&lt;li&gt;IllegalArgumentException （非法参数异常）&lt;/li&gt;
&lt;li&gt;IndexOutOfBoundsException （下标越界异常）&lt;/li&gt;
&lt;li&gt;NullPointerException （空指针异常）&lt;/li&gt;
&lt;li&gt;ArrayStoreException：试图将错误类型的对象存储到一个对象数组时抛出的异常；&lt;/li&gt;
&lt;li&gt;SecurityException （安全异常）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;受检异常(编译时异常)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQLException 提供有关数据库访问错误或其他错误的信息的异常。&lt;/li&gt;
&lt;li&gt;IOException 表示发生了某种 I / O 异常的信号。此类是由失败或中断的 I / O 操作产生的&lt;/li&gt;
&lt;li&gt;FileNotFoundException 当试图打开指定路径名表示的文件失败时，抛出此异常。&lt;/li&gt;
&lt;li&gt;ClassNotFoundException 找不到具有指定名称的类的定义。&lt;/li&gt;
&lt;li&gt;NoSuchMethodException：无法找到某一方法时，抛出；&lt;/li&gt;
&lt;li&gt;EOFException 当输入过程中意外到达文件或流的末尾时，抛出此异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
受检异常在编译时需要被处理，‌ 即要么使用 try-catch 块捕获，‌ 要么在方法签名中使用 throws 关键字声明可能抛出的异常
:::&lt;/p&gt;
&lt;h3&gt;String、StringBuffer、StringBuilder 区别？&lt;/h3&gt;
&lt;p&gt;第一个：可变性
String 内部的 value 值是 final 修饰的，所以它是不可变类。所以每次修改 String 的值，都会产生一个新的对象。
StringBuffer 和 StringBuilder 是可变类，字符串的变更不会产生新的对象。&lt;/p&gt;
&lt;p&gt;第二个：线程安全性
String 是不可变类，所以它是线程安全的。
StringBuffer 是线程安全的，因为它每个操作方法都加了 synchronized 同步关键字。
StringBuilder 不是线程安全的。
所以在多线程环境下对字符串进行操作，应该使用 StringBuffer，否则使用 StringBuilder&lt;/p&gt;
&lt;p&gt;第三个：性能方面
String 的性能是最的低的，因为不可变意味着在做字符串拼接和修改的时候，需要重新创建新的对象以及分配内存。
其次是 StringBuffer 要比 String 性能高，因为它的可变性使得字符串可以直接被修改最后是
StringBuilder，它比 StringBuffer 的性能高，因为 StringBuffer 加了同步锁。&lt;/p&gt;
&lt;p&gt;第四个：存储方面
String 存储在字符串常量池里面
StringBuffer 和 StringBuilder 存储在堆内存空间。&lt;/p&gt;
&lt;h3&gt;在 Java 中，什么时候用重载，什么时候用重写？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;重载是多态的集中体现，要以统一的方式处理不同类型数据的时候，可以用重载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是建立在继承关系上的，子类在继承父类的基础上，增加新的功能，可以用重写&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;重写、重载规则？&lt;/h3&gt;
&lt;p&gt;方法重载的规则？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法名一致，参数列表中参数的&lt;strong&gt;顺序&lt;/strong&gt;，&lt;strong&gt;类型&lt;/strong&gt;，&lt;strong&gt;个数&lt;/strong&gt;不同。&lt;/li&gt;
&lt;li&gt;重载与方法的&lt;strong&gt;返回值&lt;/strong&gt;(返回类型)无关，存在于父类和子类，同类中。&lt;/li&gt;
&lt;li&gt;可以抛出不同的&lt;strong&gt;异常&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;可以有不同&lt;strong&gt;修饰符&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法重写的规则？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数列表、方法名、返回值类型必须完全一致，&lt;strong&gt;构造方法&lt;/strong&gt;不能被重写；&lt;/li&gt;
&lt;li&gt;声明为 &lt;strong&gt;final&lt;/strong&gt; 的方法不能被重写；&lt;/li&gt;
&lt;li&gt;声明为 &lt;strong&gt;static&lt;/strong&gt; 的方法不存在重写(重写和多态联合才有意义);&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问权限&lt;/strong&gt;不能比父类更低;&lt;/li&gt;
&lt;li&gt;重写之后的方法不能抛出更宽泛的&lt;strong&gt;异常&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;子类无法重写父类的私有&lt;strong&gt;private&lt;/strong&gt;方法&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实例化对象有哪几种方式&lt;/h3&gt;
&lt;p&gt;new&lt;/p&gt;
&lt;p&gt;clone()&lt;/p&gt;
&lt;p&gt;通过反射机制创建&lt;/p&gt;
&lt;p&gt;序列化反序列化 将一个对象实例化后，进行序列化，再反序列化，也可以获得一个对象&lt;/p&gt;
&lt;h3&gt;Java 集合容器都有哪些？&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20260310223633539.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Collection
├── List（有序、可重复）
│   ├── ArrayList（基于动态数组，随机访问快，线程不安全）
│   ├── LinkedList（基于双向链表，插入/删除快，线程不安全）
│   └── Vector（线程安全的动态数组，性能较低，已基本被 ArrayList + Collections.synchronizedList 替代）
│       └── Stack（继承自 Vector，LIFO 栈，官方不推荐使用）
│
├── Set（无序、元素唯一）
│   ├── HashSet（基于 HashMap 实现，无序，允许 null）
│   │   └── LinkedHashSet（维护插入顺序或访问顺序）
│   └── TreeSet（基于 TreeMap/红黑树，元素自然排序或自定义排序，不允许 null）
│
└── Queue（队列，FIFO 或优先级）
    ├── PriorityQueue（基于堆，按优先级出队，不允许 null）
    └── Deque（双端队列）
        ├── ArrayDeque（基于循环数组，高效，推荐替代 Stack）
        └── LinkedList（也实现了 Deque 接口）

Map（键值对，非 Collection 子接口，但属于集合框架核心）
├── HashMap（哈希表实现，允许 null 键/值，线程不安全）
│   └── LinkedHashMap（维护插入顺序或访问顺序）
├── Hashtable（线程安全，不允许 null，已过时）
├── TreeMap（基于红黑树，按键排序，不允许 null 键）
└── ConcurrentHashMap（高并发场景下的线程安全 Map，分段锁 / CAS 优化）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;List 与 Set 区别&lt;/h3&gt;
&lt;p&gt;List,Set 都是继承自 Collection 接口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;List 特点：元素有放入顺序，元素可重复
List: 和数组类似，List 可以动态增长，查找元素效率高，插入删除元素效率低，因为会引起其他元素位置改变。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set 特点：元素无放入顺序，元素不可重复，重复元素会覆盖掉，（元素虽然无放入顺序，但是元素在 set 中的位置是有该元素的 HashCode 决定的，其位置其实是固定的，加入 Set 的 Object 必须定义 equals()方法，Set 检索元素效率低下，删除和插入效率高，插入和删除不会引起元素位置改变。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外 list 支持 for 循环，也就是通过下标来遍历，也可以用迭代器，但是 set 只能用迭代，因为他无序，无法用下标来取得想要的值。&lt;/p&gt;
&lt;h3&gt;说出 ArrayList，Vector， LinkedList 的存储性能和特性？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ArrayList&lt;/code&gt; 和 &lt;code&gt;Vector&lt;/code&gt; 都是使用数组方式存储数据，此数组元素数大于实际存储的数据以便增加和插入元素，它们都允许直接按序号索引元素，但是插入元素要涉及数组元素移动等内存操作，所以索引数据快而插入数据慢，Vector 由于使用了 &lt;code&gt;synchronized&lt;/code&gt; 方法（线程安全），通常性能上较 ArrayList 差&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而 LinkedList 使用双向链表实现存储，按序号索引数据需要进行前向或后向遍历，但是插入数据时只需要记录本项的前后项即可，所以插入速度较快。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ArrayList 和 LinkedList 都是不同步的，也就是不保证线程安全；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Arraylist 底层使用的是 Object 数组；LinkedList 底层使用的是双向链表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否支持快速随机访问：LinkedList 不支持高效的随机元素访问，而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 &lt;code&gt;get(int index)&lt;/code&gt;方法)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Arraylist 适合随机访问，但插入/删除效率低；LinkedList 基于链表实现，适合频繁的插入/删除操作，但访问元素效率较低。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;HashTable 和 HashMap 的区别？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HashTable&lt;/code&gt; 线程安全，&lt;code&gt;HashMap&lt;/code&gt; 非线程安全。&lt;code&gt;HashTable&lt;/code&gt;由于其线程安全性，性能略低于 &lt;code&gt;HashMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;HashTable 使用数组加链表、HashMap 采用了数组+链表+红黑树&lt;/li&gt;
&lt;li&gt;HashMap 初始容量是 16、HashTable 初始容量是 11。&lt;/li&gt;
&lt;li&gt;HashTable 不允许 null 值(key 和 value 都不可以)，HashMap 允许 null 值(key 和 value 都可以)。&lt;/li&gt;
&lt;li&gt;两者的遍历方式大同小异，HashTable 仅仅比 HashMap 多一个 elements 方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ArrayList 源码分析？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ArrayList&lt;/code&gt; 是一种变长的集合类，基于定长数组实现，在构造 ArrayList 时，如果没有指定容量，那么内部会构造一个空数组，使用默认构造方法初始化出来的容量是 10，如果指定了容量，那么就构造出对应容量大小的数组,
在添加元素时，会先判断数组容量是否足够，如果不够则会扩容，扩容按原长度的 1.5 倍扩容，容量足够后，再把元素添加到数组中
在添加元素时，如果指定了下标，先检查下标是否越界，然后再确认数组容量是否足够，不够则扩容，然后再把新元索添加到 指定位置，如果该位置后面有元素则后移;&lt;/p&gt;
&lt;p&gt;由于 ArrayList 底层基于数组实现，所以其可以保证在 O(1) 复杂度下完成随机查找操作。&lt;/p&gt;
&lt;p&gt;ArrayList 是非线程安全类，并发环境下，多个线程同时访问同一个 ArrayList 集合时，如果两个或两个以上的线程修改了 ArrayList 集合，会引发不可预知的异常或错误，则必须手动保证该集合的同步性&lt;/p&gt;
&lt;p&gt;删除和插入需要复制数组，性能差（可以使用 LinkindList），顺序添加很方便&lt;/p&gt;
&lt;h3&gt;你为什么重写 equals 时必须重写 hashCode 方法？&lt;/h3&gt;
&lt;p&gt;主要影响在基于哈希的集合类（HashMap, HashSet 等）&lt;/p&gt;
&lt;p&gt;集合类（如 HashMap 和 HashSet）依赖 hashCode 和 equals 来实现高效的存储和查找功能：&lt;/p&gt;
&lt;p&gt;HashMap 的工作流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当插入一个键值对时，HashMap 会根据键的 hashCode 值计算出存储位置（桶的位置），确定数组下标。&lt;/li&gt;
&lt;li&gt;如果该位置已经有其他键存在，则通过 equals 方法比较这些键是否相等。&lt;/li&gt;
&lt;li&gt;如果相等，则覆盖旧值；如果不相等，则将新键值对添加到链表或红黑树中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;问题是如果你重写了 equals 方法但没有重写 hashCode 方法，可能会导致以下问题：
两个对象通过 equals 方法被认为是相等的，但它们的 hashCode 值不同，在 HashMap 中会被当作不同的 key。
这会导致 HashMap 或 HashSet 无法正确找到这些对象，因为它们被存储在了不同的桶中。集合类将无法正确工作。&lt;/p&gt;
&lt;p&gt;总的来说目的就是：保证同一个对象。如果重写了 equals 方法，而没有重写 hashcode 方法，会出现 equals 相等的对象，hashcode 不相等的情况，重写 hashcode 方法就是为了避免这种情况的出现。保证了在使用 Hash 相关的集合类时能够正常工作。&lt;/p&gt;
&lt;p&gt;hashCode() 的作用是获取哈希码，也称为散列码；它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。如果两个对象相等，则 hashcode 一定也是相同的，如果两个对象相等,对两个对象分别调用 equals 方法都返回 true&lt;/p&gt;
&lt;p&gt;如果两个对象有相同的 hashcode 值，它们也不一定是相等的。因此，equals 方法被覆盖过，则 hashCode 方法也必须被覆盖。hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode()，则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据).
&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;HashSet 的底层实现是什么？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;HashSet&lt;/code&gt; 的实现是依赖于 HashMap 的，HashSet 的值都是存储在 HashMap 中的。在 HashSet 的构造法中会初始化一个 HashMap 对象，HashSet 不允许值重复。
因此，HashSet 的值是作为 HashMap 的 key 存储在 HashMap 中的，当存储的值已经存在时返回 false。利用 &lt;code&gt;HashMap&lt;/code&gt; 的键来保证 HashSet 中元素的唯一性
&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;HashSet 和 TreeSet 有什么区别？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;HashSet 是由一个 hash 表来实现的，因此，它的元素是无序的。add()，remove()，contains()方法的时间复杂度是 O(1)。&lt;/li&gt;
&lt;li&gt;TreeSet 是由一个树形的结构来实现的，它里面的元素是有序的。因此，add()，remove()，contains()方法的时间复杂度是 O(log n)。
&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;说一下 HashMap 的实现原理？&lt;/h3&gt;
&lt;p&gt;首先，HashMap 基于 map 接口，元素以键值对方式存储，允许有 null 值，HashMap 是线程不安全的。&lt;/p&gt;
&lt;p&gt;JDK1.7 中采用数组+链表的存储形式。
HashMap 采取 Entry 数组来存储 key-value，每一个键值对组成了一个 Entry 实体，Entry 类实际上是一个单向的链表结构，它具有 next 指针，指向下一个 Entry 实体，以此来解决 Hash 冲突的问题。&lt;/p&gt;
&lt;p&gt;HashMap 实现一个内部类 Entry，重要的属性有 hash、key、value、next。&lt;/p&gt;
&lt;p&gt;JDK1.8 中采用数组+链表+红黑树的存储形式。当链表长度超过阈值（8）时，数组长度超过 64 时，将链表转换为红黑树。在性能上进一步得到提升。 链表的查询速度不如红黑树快，红黑树查询速度快。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;扩容机制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;扩容发生在&lt;code&gt;HashMap&lt;/code&gt;的元素数量超过了当前容量与负载因子（load factor）的乘积时，&lt;code&gt;HashMap&lt;/code&gt;的默认负载因子是 0.75，这意味着当&lt;code&gt;HashMap&lt;/code&gt;的元素数量达到容量的 75%时，就会触发扩容。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HashMap&lt;/code&gt;的扩容机制是其设计中非常关键的部分，主要目的是为了保持哈希表的性能，避免过多的哈希冲突。&lt;code&gt;HashMap&lt;/code&gt;在 Java 中默认的初始容量是 16，这是一个 2 的幂次方。每次扩容时，新的容量会是当前容量的两倍，而且新的容量仍然是 2 的幂次方。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为啥是 2 的幂次方？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用位运算代替取模运算，提高了效率。&lt;code&gt;HashMap&lt;/code&gt;会使用位与运算（&amp;amp;）来代替取模运算（%），因为位与运算比取模运算快得多。&lt;/p&gt;
&lt;p&gt;2 的幂次方的容量可以保证哈希值的低位尽可能多地参与计算，从而更均匀地分布键值对，减少哈希冲突。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;如何去掉 list 集合中重复的元素&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;双重 for 循环去重&lt;/p&gt;
&lt;p&gt;使用两个 for 循环遍历集合所有元素，然后进行判断是否有相同元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HashSet 去重&lt;/p&gt;
&lt;p&gt;HashSet 可以去重，把 List 集合所有元素存入 HashSet 对象，接着把 List 集合元素全部清空，最后把 HashSet 对象元素全部添加至 List 集合中，这样就可以保证不出现重复元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java8 中 Stream 提供了对 List 做简单去重的处理，通过调用 distinct 方法，可以实现对
类型 Integer、Long、Char 等基本类型以及 String 类型的去重。需要注意的是，无法对自定义对象进行去重处理&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;Comparable 和 Comparator 接口的区别？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Comparable 接口只包含一个 compareTo()方法。这个方法用于比较当前对象与指定对象的顺序。返回值：负整数：当前对象小于指定对象。零：当前对象等于指定对象。正整数：当前对象大于指定对象。&lt;/li&gt;
&lt;li&gt;Comparator 接口包含 compare()和 equals()两个方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Comparable(java.lang)在要比较的类上实现实现该接口。&lt;/li&gt;
&lt;li&gt;Comparator(java.util)在要比较的类外部实现该接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Comparable 用法是在比较的类上实现该接口，重写 compareTo 方法，使用 &lt;code&gt;Collections.sort(list)&lt;/code&gt;传入需要比较的对象集合&lt;/li&gt;
&lt;li&gt;Comparator 用法是可以在外部直接使用，使用&lt;code&gt;Collections.sort(list, comparator)&lt;/code&gt;传入需要比较的集合与 Comparator 比较规则对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Comparable 只能定义一种排序规则,当一个类需要定义其自然排序规则时，可以实现 Comparable 接口。需要修改类的源码&lt;/li&gt;
&lt;li&gt;Comparator 可以定义多种排序规则，而无需修改类的源码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;自然顺序是指按照元素的自然顺序（如数字 0 到 9，或字符&apos;a&apos;到&apos;z&apos;）进行排序。&lt;/p&gt;
&lt;p&gt;对于 String 类，默认按字典顺序排序。
对于 Integer 类，默认按数值大小排序。&lt;/p&gt;
&lt;p&gt;当一个类实现了 Comparable 接口时，它通过 compareTo 方法定义了自身的自然排序规则。这个规则是固定的，不能动态更改。
:::&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;集合数据排序的常用方法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;Collections.sort&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;sort()方法有两个重载方法：一个参数和两个参数。如果只传入待排序的集合，则默认按照自然排序（升序）对集合中的数据进行排序；&lt;/p&gt;
&lt;p&gt;如果传入待排序的集合和一个比较器(Comparator)，则按照该比较器的排序规则对集合中的数据进行排序&lt;/p&gt;
&lt;p&gt;先对要排序的类实现 Comparable 接口，类中重写 compareTo()方法。在 main 函数中，调用 Collections 工具类.sort 方法，将要排序的集合传进去&lt;/p&gt;
&lt;p&gt;在 Collections.sort(list,Comparator) 直接传 Comparator 匿名函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;List.sort()&lt;/code&gt;使用 List 接口自带的 sort 方法，在 sort 中实现 Comparator 方法&lt;/p&gt;
&lt;p&gt;sort()方法只有一种使用方式，必须传入一个比较器作为参数才能使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Stream.sorted()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;sorted()方法有两个重载方法：有参和无参。如果不传参数，则默认按照自然排序（升序）对集合中的数据进行排序；&lt;/p&gt;
&lt;p&gt;如果传入一个比较器作为参数，则按照该比较器的排序规则对集合中的数据进行排序&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 自然序排序一个list
list.stream().sorted()

// 自然序逆序元素，使用Comparator 提供的reverseOrder() 方法
list.stream().sorted(Comparator.reverseOrder())
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Arrays.sort()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;sort()方法有多个重载方法，其中入参分为两种：一个参数和两个参数。 该方法只能对数组进行排序，不能直接对集合排序，因此要将待排序的集合转成数组&lt;code&gt;Collections.toArray()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果只传入待排序的数组，则默认按照自然排序（升序）对数组中的数据进行排序；&lt;/p&gt;
&lt;p&gt;如果传入待排序的数组和一个比较器，则按照该比较器的排序规则对数组中的数据进行排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你可以使用有序集合，如 TreeSet 或 TreeMap，你也可以使用有顺序的的集合，如 list&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;集合实现交集并集&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;实现交集（Intersection）指两个集合中都包含的元素。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;要实现两个集合的交集，你可以遍历一个集合，并检查元素是否存在于另一个集合中。如果存在，则将其添加到新的集合中。这里使用&lt;code&gt;retainAll&lt;/code&gt;方法来实现交集。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.HashSet;
import java.util.Set;

public class IntersectionExample {
    public static void main(String[] args) {
        Set&amp;lt;Integer&amp;gt; set1 = new HashSet&amp;lt;&amp;gt;();
        Set&amp;lt;Integer&amp;gt; set2 = new HashSet&amp;lt;&amp;gt;();

        // 添加元素到 set1
        set1.add(1);
        set1.add(2);
        set1.add(3);
        set1.add(4);

        // 添加元素到 set2
        set2.add(3);
        set2.add(4);
        set2.add(5);
        set2.add(6);

        // 使用 retainAll 方法计算交集
        set1.retainAll(set2);

        // 输出交集
        System.out.println(&quot;交集: &quot; + set1);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实现并集（Union）两个集合都有的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;要实现两个集合的并集，你可以将一个集合的内容复制到另一个集合中，这样第二个集合将包含两个集合的所有元素。这里使用&lt;code&gt;addAll&lt;/code&gt;方法来实现并集。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.HashSet;
import java.util.Set;

public class UnionExample {
    public static void main(String[] args) {
        Set&amp;lt;Integer&amp;gt; set1 = new HashSet&amp;lt;&amp;gt;();
        Set&amp;lt;Integer&amp;gt; set2 = new HashSet&amp;lt;&amp;gt;();

        // 添加元素到 set1
        set1.add(1);
        set1.add(2);
        set1.add(3);
        set1.add(4);

        // 添加元素到 set2
        set2.add(3);
        set2.add(4);
        set2.add(5);
        set2.add(6);

        // 使用 addAll 方法计算并集
        set2.addAll(set1);

        // 输出并集
        System.out.println(&quot;并集: &quot; + set2);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;HashMap 遍历方式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Map&amp;lt;String, Integer&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
map.put(&quot;one&quot;, 1);
map.put(&quot;two&quot;, 2);
map.put(&quot;three&quot;, 3);

map.forEach((k, v) -&amp;gt; {
    System.out.println(k + &quot; &quot; + v);
});

map.entrySet().stream().forEach(entry -&amp;gt; {
    System.out.println(entry.getKey() + &quot; &quot; + entry.getValue());
});

Iterator&amp;lt;Map.Entry&amp;lt;String, Integer&amp;gt;&amp;gt; iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry&amp;lt;String, Integer&amp;gt; next = iterator.next();
    System.out.println(next.getKey() + &quot; &quot; + next.getValue());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt; Map&amp;lt;String, Integer&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
map.put(&quot;Alice&quot;, 25);
map.put(&quot;Bob&quot;, 30);
map.put(&quot;Charlie&quot;, 35);

// 使用 entrySet 遍历键值对
for (Map.Entry&amp;lt;String, Integer&amp;gt; entry : map.entrySet()) {
    System.out.println(&quot;Key: &quot; + entry.getKey() + &quot;, Value: &quot; + entry.getValue());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;成员变量和局部变量的区别有哪些？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;从&lt;strong&gt;语法&lt;/strong&gt;形式上，看成员变量是属于类的，而局部变量是在方法中定义的变量或是方法的参数；成员变量可以被 public,private,static 等修饰符所修饰，而局部变量不能被访问控制修饰符及 static 所修饰；成员变量和局部变量都能被 final 所修饰；&lt;/li&gt;
&lt;li&gt;从变量在内存中的&lt;strong&gt;存储&lt;/strong&gt;方式来看，成员变量是对象的一部分，而对象存在于堆内存，局部变量存在于栈内存&lt;/li&gt;
&lt;li&gt;从变量在内存中的&lt;strong&gt;生存时间&lt;/strong&gt;上看，成员变量是对象的一部分，它随着对象的创建而存在，而局部变量随着方法的调用而自动消失。&lt;/li&gt;
&lt;li&gt;成员变量如果没有被&lt;strong&gt;赋初值&lt;/strong&gt;，则会自动以类型的默认值而赋值(一种情况例外被 final 修饰但没有被 static 修饰的成员变量必须显示地赋值); 而局部变量则不会自动赋值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;是否可以在 static 环境中访问非 static 变量？&lt;/h3&gt;
&lt;p&gt;static 变量在 Java 中是属于类的，它在所有的实例中的值是一样的。当类被 Java 虚拟机载入的时候，会对 static 变量进行初始化。(而非 static 变量（实例变量）是属于对象的，它们在对象被创建时才被分配内存) 如果你的代码尝试不用实例来访问非 static 的变量，编译器会报错，因为这些变量还没有被创建出来，还没有跟任何实例关联上。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h2&gt;并发编程&lt;/h2&gt;
&lt;h3&gt;什么是线程安全？如何保证线程安全？&lt;/h3&gt;
&lt;p&gt;在多线程环境下，线程安全是指多个线程访问共享数据时，不会出现数据错误或不一致的情况。要保证线程安全，可以采用同步机制，比如使用 synchronized 关键字或 Lock 接口来保护共享数据的访问，或者使用线程安全的数据结构，比如 ConcurrentHashMap、CopyOnWriteArrayList。&lt;/p&gt;
&lt;p&gt;或使用线程局部变量（ThreadLocal）&lt;/p&gt;
&lt;p&gt;或使用原子类（Atomic Classes）如 AtomicInteger、AtomicLong 等&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;说说线程有几种创建方式？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;继承 Thread&lt;/li&gt;
&lt;li&gt;实现 Runnable&lt;/li&gt;
&lt;li&gt;实现 Callable&lt;/li&gt;
&lt;li&gt;使用线程池&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义一个类继承 Thread。
重写 run() 方法，编写线程需要执行的任务。
创建该类的实例并调用 start() 方法启动线程。&lt;/p&gt;
&lt;p&gt;不推荐使用，因为 Java 是单继承语言，继承 Thread 后无法再继承其他类。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(&quot;线程运行：&quot; + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义一个类实现 Runnable 接口。
实现 run() 方法，编写线程需要执行的任务。
将该类的实例传递给 Thread 的构造器，并调用 start() 方法启动线程。&lt;/p&gt;
&lt;p&gt;更灵活，避免了单继承的限制。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(&quot;线程运行：&quot; + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start(); // 启动线程
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义一个类实现 &lt;code&gt;Callable&amp;lt;V&amp;gt;&lt;/code&gt; 接口，指定泛型类型 V 表示返回值类型。&lt;/p&gt;
&lt;p&gt;实现 call() 方法，编写线程需要执行的任务并返回结果。&lt;/p&gt;
&lt;p&gt;使用 FutureTask 包装 Callable 对象，并将 FutureTask 传递给 Thread 的构造器。&lt;/p&gt;
&lt;p&gt;调用 start() 方法启动线程，并通过 FutureTask.get() 获取返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable&amp;lt;Integer&amp;gt; {
    @Override
    public Integer call() throws Exception {
        System.out.println(&quot;线程运行：&quot; + Thread.currentThread().getName());
        return 42; // 返回结果
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        FutureTask&amp;lt;Integer&amp;gt; futureTask = new FutureTask&amp;lt;&amp;gt;(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start(); // 启动线程

        // 获取线程返回值
        Integer result = futureTask.get();
        System.out.println(&quot;线程返回值：&quot; + result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用 Executors 工具类创建线程池。
提交任务（Runnable 或 Callable）到线程池中。
线程池会自动分配线程执行任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2); // 创建固定大小的线程池

        // 提交任务
        executor.submit(() -&amp;gt; {
            System.out.println(&quot;线程运行：&quot; + Thread.currentThread().getName());
        });

        executor.shutdown(); // 关闭线程池
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;进程与线程&lt;/h3&gt;
&lt;p&gt;进程是系统分配和调度资源的基本单位，每个进程都有自己的独立内存空间。&lt;/p&gt;
&lt;p&gt;线程是进程中的一个执行单元，是进程中的一个可调度的实体。一个进程可以包含多个线程，这些线程共享进程的资源，如内存空间。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;并行与并发&lt;/h3&gt;
&lt;p&gt;并行是指多个任务在同一时刻同时执行。&lt;/p&gt;
&lt;p&gt;并发是指多个任务在一段时间内交替执行。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;线程有几种状态？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NEW&lt;/strong&gt;新建状态：线程对象已经被创建，但是尚未启动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RUNNABLE 就绪状态&lt;/strong&gt;(不停的抢 cpu 执行权):线程正在运行，或者准备好运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BLOCKED 阻塞状态&lt;/strong&gt;：无法获得锁对象，线程等待获得一个排他锁才能继续运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WAITING 等待状态&lt;/strong&gt;：线程等待另一个线程的动作（例如调用 notify() 方法）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TIMED_WAITING 计时等待&lt;/strong&gt;：线程等待某个条件发生，或者等待的时间到了就会自动(返回)恢复。比如 sleep()方法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TERMINATED 结束状态&lt;/strong&gt;：线程已经终止。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;谈谈你对 AQS 的理解&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AbstractQueuedSynchronizer&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;AQS 是多线程同步器，它是 J.U.C 包中多个组件的底层实现，是许多同步工具类的基础，如 Lock、CountDownLatch、Semaphore 等都用到了 AQS.&lt;/p&gt;
&lt;p&gt;从本质上来说，AQS 提供了两种锁机制，分别是排它锁和共享锁。&lt;/p&gt;
&lt;p&gt;排它锁，就是存在多线程竞争同一共享资源时，同一时刻只允许一个线程访问该共享资源，也就是多个线程中只能有一个线程获得锁资源，比如 Lock 中的 ReentrantLock，重入锁实现就是用到了 AQS 中的排它锁功能。&lt;/p&gt;
&lt;p&gt;共享锁也称为读锁，就是在同一时刻允许多个线程同时获得锁资源，比如 CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;请谈谈 AQS 是怎么回事儿？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AQS&lt;/code&gt; 它是 &lt;code&gt;J.U.C&lt;/code&gt; 这个包里面非常核心的一个抽象类，它为多线程访问共享资源提供了一个队列同步器。
在 J.U.C 这个包里面，很多组件都依赖 AQS 实现线程的同步和唤醒，比如 &lt;code&gt;Lock&lt;/code&gt;、&lt;code&gt;Semaphore&lt;/code&gt;、&lt;code&gt;CountDownLatch&lt;/code&gt; 等等。
AQS 内部由两个核心部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个 &lt;code&gt;volatile&lt;/code&gt; 修饰的 state 变量，作为一个竞态条件&lt;/li&gt;
&lt;li&gt;用双向链表结构维护的 FIFO 线程等待队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它的具体工作原理是，多个线程通过对这个 state 共享变量进行修改来实现竞态条件，
竞争失败的线程加入到 &lt;code&gt;FIFO&lt;/code&gt; 队列并且阻塞，抢占到竞态资源的线程释放之后，后续的线程按照 FIFO 顺序实现有序唤醒。&lt;/p&gt;
&lt;p&gt;AQS 里面提供了两种资源共享方式，一种是独占资源，同一个时刻只能有一个线程获得竞态资源。
比如 ReentrantLock 就是使用这种方式实现排他锁。
另一种是共享资源，同一个时刻，多个线程可以同时获得竞态资源。
&lt;code&gt;CountDownLatch&lt;/code&gt; 或者 &lt;code&gt;Semaphore&lt;/code&gt; 就是使用共享资源的方式，实现同时唤醒多个线程。
&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;lock 和 synchronized 区别&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;从功能角度来看：Lock 和 Synchronized 都是 Java 中用来解决线程安全问题的工具。&lt;/li&gt;
&lt;li&gt;从特性来看:Synchronized 是 Java 中的同步关键字; Lock 是 J.U.C 包中提供的接口，这个接口有很多实现类，其中就包括 ReentrantLock 重入锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;锁的力度&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Synchronized 可以通过两种方式来控制锁的粒度,一种是把 synchronized 关键字修饰在方法层面，另一种是修饰在代码块上，并且我们可以通过 Synchronized(锁定特定的对象)加锁对象的声明周期来控制锁的作用范围，比如锁对象是静态对象或者类对象，那么这个锁就是全局锁。
如果锁对象是普通实例对象，那这个锁的范围取决于这个实例的声明周期。&lt;/p&gt;
&lt;p&gt;Lock 锁的粒度是通过它里面提供的 lock()和 unlock()方法决定的，包裹在这两个方法之间的代码能够保证线程安全性。而锁的作用域取决于 Lock 实例的生命周期。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;灵活性与非阻塞竞争锁&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Lock 比 Synchronized 的灵活性更高，Lock 可以自主决定什么时候加锁，什么时候释放锁，只需要调用 lock()和 unlock()这两个方法就行，同时 Lock 还提供了非阻塞的竞争锁方法 tryLock()方法，这个方法通过返回 true/false 来告诉当前线程是否已经有其他线程正在使用锁。&lt;/p&gt;
&lt;p&gt;Synchronized 由于是关键字，所以它无法实现非阻塞竞争锁的方法，另外，Synchronized 锁的释放是被动的，就是当 Synchronized 同步代码块执行完以后或者代码出现异常时才会释放。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;公平锁与非公平锁&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Lock 提供了公平锁和非公平锁的机制，&lt;/p&gt;
&lt;p&gt;公平锁是指线程竞争锁资源时，如果已经有其他线程正在排队等待锁释放，那么当前竞争锁资源的线程无法插队。&lt;/p&gt;
&lt;p&gt;而非公平锁，就是不管是否有线程在排队等待锁，它都会尝试去竞争一次锁。&lt;/p&gt;
&lt;p&gt;Synchronized 只提供了一种非公平锁的实现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从&lt;strong&gt;性能方面&lt;/strong&gt;来看，Synchronized 和 Lock 在性能方面相差不大，在实现上会有一些区别，Synchronized 引入了偏向锁、轻量级锁、重量级锁以及锁升级的方式来优化加锁的性能，而 Lock 中则用到了自旋锁的方式来实现性能优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;怎么理解线程安全？说说你对原子性、可见性、有序性的理解？&lt;/h3&gt;
&lt;p&gt;在多线程环境下，线程安全是指多个线程访问共享数据时，不会出现数据错误或不一致的情况。&lt;/p&gt;
&lt;p&gt;原子性、有序性、可见性是并发编程中非常重要的基础概念，JMM 的很多技术都是围绕着这三大特性展开。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子性&lt;/strong&gt;：原子性指的是一个操作是不可分割、不可中断的，要么全部执行并且执行的过程不会被任何因素打断，要么就全不执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可见性&lt;/strong&gt;：可见性指的是一个线程修改了某一个共享变量的值时，其它线程能够立即知道这个修改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有序性&lt;/strong&gt;：有序性指的是对于一个线程的执行代码，从前往后依次执行，单线程下可以认为程序是有序的，但是并发时有可能会发生指令重排。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;原子性、可见性、有序性都应该怎么保证呢？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;原子性：JMM 只能保证基本的原子性，如果要保证一个代码块的原子性，需要使用 synchronized。&lt;/li&gt;
&lt;li&gt;可见性：Java 是利用 volatile 关键字来保证可见性的，除此之外，final 和 synchronized 也能保证可见性。&lt;/li&gt;
&lt;li&gt;有序性：synchronized 或者 volatile 都可以保证多线程之间操作的有序性。
&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;能谈一下 CAS 机制吗？悲观锁？乐观锁？&lt;/h3&gt;
&lt;p&gt;CAS 是 &lt;code&gt;compare and swap&lt;/code&gt; 的缩写，即我们所说的比较交换。&lt;/p&gt;
&lt;p&gt;CAS 是一种基于锁的操作，而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;悲观锁&lt;/strong&gt;认为自己在使用数据的时候一定有别的线程来修改数据，因此在获取数据的时候会先加锁，确保数据不会被别的线程修改。Java 中，synchronized 关键字和 Lock 的实现类都是悲观锁。悲观锁是将资源锁住，等一个之前获得锁的线程释放锁之后，下一个线程才可以访问。&lt;/p&gt;
&lt;p&gt;而&lt;strong&gt;乐观锁&lt;/strong&gt;采取了一种宽泛的态度，乐观锁认为自己在使用数据时不会有别的线程修改数据，所以不会添加锁，只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新，当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新，则根据不同的实现方式执行不同的操作，比如通过给更新记录加 version 来获取数据，性能较悲观锁有很大的提高。&lt;/p&gt;
&lt;p&gt;CAS 操作包含三个操作数 — 内存中的变量值（当前实际值）（V）、预期原值（A）和新值(B)。如果内存地址里面的值和 A 的值是一样的，那么就将内存里面的值更新成 B，如果 V ≠ A，说明已经被其他线程修改过了，则什么都不做，返回 false。&lt;/p&gt;
&lt;p&gt;CAS 是通过无限循环来获取数据的，若果在第一轮循环中，a 线程获取地址里面的值被 b 线程修改了，那么 a 线程需要自旋，到下次循环才有可能机会执行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java.util.concurrent.atomic&lt;/code&gt;包下的类大多是使用 CAS 操作来实现的，比如&lt;code&gt;AtomicInteger,AtomicBoolean,AtomicLong&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;CAS 的问题&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1、CAS 容易造成 ABA 问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ABA 问题指的是这样一个场景：线程 A 读取了一个值 A，然后线程 B 将这个值改为 B 再改回 A，这时线程 A 再次检查值时发现它依然是 A，CAS 操作认为没有变化，但实际上已经发生了改变，这可能导致数据的一致性问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;解决方案: 可以使用版本号标识，每操作一次 version 加 1。在 CAS 操作时，除了检查值本身外，还要检查版本号是否发生变化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 java5 中，已经提供了 &lt;code&gt;AtomicStampedReference&lt;/code&gt; 来解决问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2、不能保证代码块的原子性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CAS 机制所保证的只是一个变量的原子性操作，而不能保证整个代码块的原子性。&lt;/p&gt;
&lt;p&gt;比如需要保证 3 个变量共同进行原子性的更新，就不得不使用 &lt;code&gt;synchronized&lt;/code&gt; 了。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;循环时间长开销大&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;CAS 操作如果长时间不成功，会导致其一直自旋，给 CPU 带来非常大的开销。&lt;/p&gt;
&lt;p&gt;:::tip
版本号解决方案是指：在数据表中增加一个 version 字段，每次更新数据时让 version 加 1。&lt;/p&gt;
&lt;p&gt;更新时使用类似下面的 SQL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE ... SET value = newValue, version = version + 1 
WHERE id = ? AND version = oldVersion
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;volatile 关键字有什么用？它的实现原理是什么？&lt;/h3&gt;
&lt;p&gt;volatile 关键字有两个作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以保证在多线程环境下共享变量的可见性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;指当某一个线程对共享变量的修改，其他线程可以立刻看到修改之后的值。普通变量在多线程环境下可能会被缓存在线程的本地内存中（如 CPU 缓存），导致其他线程无法及时感知变量的变化。而 volatile 变量会强制将修改后的值刷新到主内存，并通知其他线程重新读取主内存中的最新值。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过增加内存屏障防止多个指令之间的重排序。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所谓重排序，就是指令的编写顺序和执行顺序不一致，在多线程环境下导致可见性问题。因为编译器或处理器为了优化性能，可能会对指令进行重排序。volatile 通过插入内存屏障（Memory Barrier）来防止这种重排序。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;sleep 方法和 wait 方法有什么区别?&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;sleep() 方法可以在任何地方使用，而 wait() 方法只能在同步块或同步方法中使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sleep() 方法不会释放锁，即使它在同步块或同步方法中使用。而 wait() 方法会释放锁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sleep()方法中断后可以继续执行，而 wait()方法中断后不会继续执行，除非再次被唤醒。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;wait()方法属于Object 类，	sleep()方法属于Thread 类&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;wait 和 notify 这个为什么要在 synchronized 代码块中？&lt;/h3&gt;
&lt;p&gt;wait() 和 notify() 是基于对象监视器锁（Monitor）操作的，而 synchronized 是获取这个监视器的唯一合法方式。&lt;/p&gt;
&lt;p&gt;如果不在 synchronized 中调用，运行时会直接抛出异常，因为当前线程没有持有该对象的监视器锁；&lt;/p&gt;
&lt;p&gt;调用 wait() 时，当前线程会释放持有的监视器锁，并进入等待队列。notify 方法用于唤醒 等待在该对象监视器上的线程，但只有处于等待状态的线程才能被唤醒。因此，需要将相关代码放在 synchronized 代码块中，以确保调用 wait 和 notify 方法的线程是等待状态的线程。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;造成死锁原因？&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;是多个线程涉及到多个锁，这些锁存在着交叉，所以可能会导致了一个锁依赖的闭环。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;循环等待&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;线程在获得了锁 A 并且没有释放的情况下去申请锁 B，这时，另一个线程已经获得了锁 B，在释放锁 B 之前又要先获得锁 A，因此闭环发生，陷入死锁循环。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h3&gt;避免死锁&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;使用锁超时&lt;/strong&gt;：&lt;strong&gt;使用 &lt;code&gt;tryLock()&lt;/code&gt; 方法&lt;/strong&gt;（来自 &lt;code&gt;java.util.concurrent.locks.Lock&lt;/code&gt; 接口）来尝试获取锁，如果在指定时间内未能获取锁，则放弃尝试，做其它事情，避免无限期等待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;避免嵌套锁&lt;/strong&gt;：如果在一个线程中已经持有一个锁，然后尝试获取第二个锁，那么就有可能发生死锁。因此，应该尽量避免在持有锁的同时请求其他锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;锁顺序&lt;/strong&gt;：如果必须获取多个锁，那么要确保所有的线程都按照相同的顺序来获取锁。这样可以防止&amp;lt;span class=&quot;marker-evy&quot;&amp;gt;循环等待&amp;lt;/span&amp;gt;条件的发生，这是死锁的一个必要条件。&lt;/p&gt;
&lt;h3&gt;线程池核心参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public ThreadPoolExecutor(
    int corePoolSize,           // 核心线程数（即使空闲也会保留）
    int maximumPoolSize,        // 最大线程数（队列满时最多能创建的线程数）
    long keepAliveTime,         // 非核心线程空闲存活时间
    TimeUnit unit,              // 时间单位
    BlockingQueue&amp;lt;Runnable&amp;gt; workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂（可自定义线程名）
    RejectedExecutionHandler handler    // 拒绝策略
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;corePoolSize =&amp;gt; 线程池核心线程数量&lt;/li&gt;
&lt;li&gt;maximumPoolSize =&amp;gt; 线程池最大数量（包含核心线程数量）&lt;/li&gt;
&lt;li&gt;keepAliveTime =&amp;gt; 当前线程池数量超过 corePoolSize 时，多余的空闲线程的存活时间。&lt;/li&gt;
&lt;li&gt;unit =&amp;gt; keepAliveTime 的单位&lt;/li&gt;
&lt;li&gt;workQueue =&amp;gt; 线程池所使用的缓冲队列，被提交但尚未被执行的任务&lt;/li&gt;
&lt;li&gt;threadFactory =&amp;gt; 线程工厂，用于创建线程，一般用默认的即可&lt;/li&gt;
&lt;li&gt;handler =&amp;gt; 拒绝策略，当任务太多来不及处理，如何拒绝任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;如何创建线程池&lt;/h3&gt;
&lt;p&gt;不要使用 &lt;code&gt;Executors&lt;/code&gt; 直接创建线程池，会出现 &lt;code&gt;OOM&lt;/code&gt; 问题，要使用 &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; 构造方法创建&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;FixedThreadPool&lt;/code&gt; 和&lt;code&gt; SingleThreadPool&lt;/code&gt;： 允许的请求队列长度为 &lt;code&gt;Integer.MAX_VALUE&lt;/code&gt;，可能会堆积大量的请求，从而导致 OOM。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CachedThreadPool&lt;/code&gt;： 允许的创建线程数量为 &lt;code&gt;Integer.MAX_VALUE&lt;/code&gt;，可能会创建大量的线程，从而导致 OOM。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;创建线程池方式一：&lt;strong&gt;new ThreadPoolExecutor 方式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.*;

public class ThreadPoolDemo {
    
    public static void main(String[] args) {
        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2,                          // corePoolSize: 核心线程数
            5,                          // maximumPoolSize: 最大线程数
            60,                         // keepAliveTime: 空闲线程存活时间
            TimeUnit.SECONDS,           // unit: 时间单位
            new LinkedBlockingQueue&amp;lt;&amp;gt;(10), // workQueue: 任务队列
            Executors.defaultThreadFactory(), // threadFactory: 线程工厂
            new ThreadPoolExecutor.CallerRunsPolicy() // handler: 拒绝策略
        );
        
        // 提交任务
        for (int i = 0; i &amp;lt; 20; i++) {
            executor.execute(() -&amp;gt; {
                System.out.println(Thread.currentThread().getName() + &quot; 执行任务&quot;);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        // 关闭线程池
        executor.shutdown();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建线程池方式二：&lt;strong&gt;spring 的 ThreadPoolTaskExecutor 方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java Config 配置方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableAsync  // 开启异步支持
public class ThreadPoolConfig {
    
    @Bean(name = &quot;taskExecutor&quot;)
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        
        // 核心线程数
        executor.setCorePoolSize(5);
        
        // 最大线程数
        executor.setMaxPoolSize(10);
        
        // 队列容量
        executor.setQueueCapacity(100);
        
        // 空闲线程存活时间（秒）
        executor.setKeepAliveSeconds(60);
        
        // 线程名前缀
        executor.setThreadNamePrefix(&quot;my-task-&quot;);
        
        // 拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        
        // 等待任务完成再关闭
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        
        // 初始化
        executor.initialize();
        
        return executor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 @Async 注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class AsyncService {
    
    @Async(&quot;taskExecutor&quot;)  // 指定线程池
    public CompletableFuture&amp;lt;String&amp;gt; doTask(String taskId) {
        System.out.println(Thread.currentThread().getName() + &quot; 处理任务: &quot; + taskId);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return CompletableFuture.completedFuture(&quot;完成: &quot; + taskId);
    }
    
    @Async
    public void sendEmail(String email) {
        // 使用默认线程池
        System.out.println(&quot;发送邮件到: &quot; + email);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;线程池工作原理&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://zzyang.oss-cn-hangzhou.aliyuncs.com/img/20260328220426799.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;线程池刚创建时，里面没有一个线程。&lt;/p&gt;
&lt;p&gt;当调用 execute(Runnable task) 方法或submit(Callable task)添加一个任务时，线程池会做如下判断&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果正在运行的线程数量小于 corePoolSize，那么马上创建线程运行这个任务；&lt;/li&gt;
&lt;li&gt;如果正在运行的线程数量大于或等于 corePoolSize，那么将这个任务放入队列；&lt;/li&gt;
&lt;li&gt;如果这时候队列满了，而且正在运行的线程数量小于 maximumPoolSize(最大线程数)，那么还是要创建非核心线程立刻运行这个任务；&lt;/li&gt;
&lt;li&gt;如果队列满了，而且正在运行的线程数量大于或等于 maximumPoolSize，那么线程池会抛出异常 RejectExecutionException。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当一个线程完成任务时，它会从队列中取下一个任务来执行。&lt;/p&gt;
&lt;p&gt;当一个线程无事可做，超过一定的时间（keepAliveTime）时，线程池会判断，如果当前运行的线程数大于 corePoolSize，那么这个线程就被停掉。&lt;/p&gt;
&lt;p&gt;所以线程池的所有任务完成后，它最终会收缩到 corePoolSize 的大小。&lt;/p&gt;
&lt;h3&gt;拒绝策略（RejectedExecutionHandler）&lt;/h3&gt;
&lt;p&gt;当「任务队列满了 + 线程数达到最大线程数」时，新提交的任务会被拒绝，拒绝策略就是定义“如何处理被拒绝的任务”。Java内置4种拒绝策略，也支持自定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AbortPolicy（默认）：直接抛出RejectedExecutionException异常，阻止程序运行；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CallerRunsPolicy：由提交任务的线程（调用者线程）自己执行该任务，避免任务丢失；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DiscardPolicy：默默丢弃被拒绝的任务，不抛异常、不执行；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DiscardOldestPolicy：丢弃任务队列中最老的任务（队列头部的任务），然后将新任务加入队列。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线程池大小如何设定&lt;/h3&gt;
&lt;p&gt;有一个简单并且适用面比较广的公式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是 CPU 密集型的，可以把核心线程数设置为 CPU核心数+1&lt;/li&gt;
&lt;li&gt;如果是 IO 密集型的，可以把核心线程数设置 2*CPU核心数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如何判断是 CPU 密集任务还是 IO 密集任务？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CPU 密集型简单理解就是利用 &lt;strong&gt;CPU 计算能力的任务&lt;/strong&gt;比如你在内存中对大量数据进行排序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但凡涉及到网络请求，文件读取这类都是 IO 密集型，CPU 经常闲。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;jvm&lt;/h2&gt;
&lt;h3&gt;内存溢出、内存泄露&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;内存溢出：指应用程序在申请内存时，无法获得足够的内存空间，造成异常终
止。常见原因是使用了过多的对象并且没有妥善释放。&lt;/li&gt;
&lt;li&gt;内存泄漏：指应用程序中存在无用的对象占用内存，并且这些对象无法被垃圾
回收器回收。随着时间的推移，内存资源逐渐耗尽，最终导致内存溢出&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;JVM 如何调优（调优参数）&lt;/h3&gt;
&lt;p&gt;一般调优，通常优先对堆内存空间进行调整；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-Xms 表示初始堆内存大小（默认为物理内存的 1/64），&lt;/li&gt;
&lt;li&gt;-Xmx 表示最大分配对内存大小（默认为物理内存的 1/4）&lt;/li&gt;
&lt;li&gt;Xmn512m  新生代大小&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;java -Xmx3.2g -Xms1g -jar xxx.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双亲委派机制&lt;/h3&gt;
&lt;p&gt;Java 双亲委派机制是指在类加载过程中，不会立即自己加载，而是先委派给父类加载器去加载，只有父类加载器无法加载时，才自己尝试加载。&lt;/p&gt;
&lt;p&gt;父类加载器在尝试加载类之前，也会先检查自己是否已经加载了该类，若加载了则直接返回；否则继续向上委派给其父类加载器。&lt;/p&gt;
&lt;p&gt;避免了恶意类的加载和类的重复加载。&lt;/p&gt;
</content:encoded></item><item><title>中间件 Interview</title><link>https://zzyang.top/posts/middleware-interview/</link><guid isPermaLink="true">https://zzyang.top/posts/middleware-interview/</guid><pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;RabbitMQ&lt;/h2&gt;
&lt;h3&gt;怎么确保消息幂等性？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方式1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们在生产者发送消息的时候，将生成的唯一token存储到redis中并且设置一个TTL存活时间，通过&lt;code&gt;MessagePostProcessor&lt;/code&gt;对象将token存储在&lt;code&gt;MessageId&lt;/code&gt;中&lt;/p&gt;
&lt;p&gt;在监听队列的消费者端，我们从message对象中获取存在MessageId中的token，在从redis中获取这个token，判断redis中是否有MessageId中的这个token，如果有这个token并且没有被删除，那么我们就正常执行业务逻辑代码，业务逻辑代码执行后，再删除redis中的这个token，并且channel手动确认消息&lt;/p&gt;
&lt;p&gt;需要注意的是：rabbitmq是批量处理消息，有必要加锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式2&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;利用&lt;code&gt;redis&lt;/code&gt;中的&lt;code&gt;setnx&lt;/code&gt;指令，java中&lt;code&gt;setIfAbent()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;让消费者记住消息。结合redis 的setnx指令&lt;/p&gt;
&lt;p&gt;消费前把消息中的唯一属性，放入redis，如果重复消费了，发现redis中若有该唯一属性，就不消费&lt;/p&gt;
&lt;p&gt;如果消息中没有唯一属性？(联合主键：找几个属性加起来时唯一即可)&lt;/p&gt;
&lt;h3&gt;消息成为死信的三种情况&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;​	消费者拒接消费消息，并且不把消息重新放入原目标队列，&lt;code&gt;basicNack/basicReject&lt;/code&gt;,并且不把消息重 新放入原目标队列,&lt;code&gt;requeue=false&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;​	队列中的消息超出队列的长度，淘汰最早的消息&lt;/li&gt;
&lt;li&gt;​	队列中的消息超过设置的过期时间，没有被消费&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;如何实现延迟队列&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方案一：使用死信队列 + TTL（原生支持，无插件）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需要为发送的消息设置 TTL（存活时间），过期后进入死信交换机（DLX），由死信交换机转发到实际消费队列；队列需要绑定“消息过期后转到的死信交换机”、“死信后用的路由键”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方案二：使用延迟插件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在github中找到rabbitmq官方提供的延迟插件，在rabbitmq中进行配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rabbitmq-plugins enable rabbitmq_delayed_message_exchange
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在定义交换机时，需要指定延迟插件类型&lt;code&gt;x-delayed-message&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;使用rabbitmq的好处&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在分布式系统下俱备异步，削峰平谷，解耦等功能、可以使服务之间调用解耦&lt;/li&gt;
&lt;li&gt;​对于高并发场景下，利用消息队列可以对插入到数据库流量限流&lt;/li&gt;
&lt;li&gt;​可以利用死信，实现延迟消费的效果&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;如何确保消息可靠投递&lt;/h3&gt;
&lt;p&gt;异常情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生产者连接MQ失败&lt;/li&gt;
&lt;li&gt;生产者发送消息到达MQ后未找到Exchange&lt;/li&gt;
&lt;li&gt;生产者发送消息到达MQ的Exchange后，未找到合适的Queue，因此无法路由&lt;/li&gt;
&lt;li&gt;消费者接收到消息后突然宕机&lt;/li&gt;
&lt;li&gt;消费者接收到消息后，因处理不当导致异常&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过配置我们可以开启连接失败后的重连机制，当网络不稳定的时候，利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试，也就是说多次重试等待的过程中，当前线程是被阻塞的，会影响业务性能，当然也可以考虑使用异步线程来执行发送消息的代码。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SpringAMQP&lt;/code&gt;提供了&lt;code&gt;Publisher Confirm&lt;/code&gt;和&lt;code&gt;Publisher Return&lt;/code&gt;两种确认机制。开启确机制认后，当发送者发送消息给MQ后，MQ会返回确认结果给发送者。
&lt;code&gt;Publisher Confirm&lt;/code&gt;：消息到达交换机后触发，不管路由是否成功，只要到达交换机就触发；&lt;code&gt;Publisher Return&lt;/code&gt;：消息无法路由到队列时触发；开启生产者确认比较消耗MQ性能，一般不建议开启。路由失败：一般是因为RoutingKey错误导致，往往是编程导致；
交换机名称错误：同样是编程错误导致&lt;/p&gt;
&lt;p&gt;消费者确认机制(Consumer Acknowledgement)是为了确认消费者是否成功处理消息。当消费者处理消息结束后应该向RabbitMQ发送一个回执，告知RabbitMQ自己消息处理状态；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ack:成功处理消息，RabbitMQ从队列中删除该消息&lt;/li&gt;
&lt;li&gt;nack:消息处理失败，RabbitMQ需要再次投递消息&lt;/li&gt;
&lt;li&gt;reject:消息处理失败并拒绝该消息，RabbitMQ从队列中删除该消息&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>MySQL面经</title><link>https://zzyang.top/posts/mysql-mianshi/</link><guid isPermaLink="true">https://zzyang.top/posts/mysql-mianshi/</guid><pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MySql&lt;/h1&gt;
&lt;h2&gt;char 和 varchar 的区别？&lt;/h2&gt;
&lt;p&gt;在数据库中，CHAR（固定长度字符）和 VARCHAR（可变长度字符）是两种用于存储字符数据的数据类型，它们之间的主要区别在于数据存储方式和存储需求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;存储方式：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CHAR： 存储固定长度的字符数据，不管实际数据的长度是多少，都会占用指定长度的存储空间。如果存储的数据长度小于定义的长度，剩余空间将用空格填充。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VARCHAR： 存储可变长度的字符数据，实际存储的空间取决于数据的长度。它只占用实际数据的存储空间加上一些额外的字节来记录数据的长度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;存储需求：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CHAR： 由于它存储固定长度的数据，对于每个数据项都需要分配固定的存储空间，可能会浪费一些存储空间。适用于存储长度固定的数据，如固定长度的代码或状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VARCHAR： 由于它存储实际数据的长度，因此在存储可变长度的数据时更为节省空间。适用于存储长度可变的文本数据，如评论、描述等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;性能：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CHAR： 由于存储固定长度，检索时可能更快，但在存储大量可变长度的数据时可能会浪费空间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VARCHAR： 由于存储实际数据长度，可能在某些情况下对存储效率更友好，尤其是对于可变长度的数据。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;总体来说，选择使用 CHAR 还是 VARCHAR 取决于你的数据特性和存储需求。如果你知道数据项的长度是固定的，而且不太可能变化，那么使用 CHAR 可能更合适。如果数据长度变化较大，或者你想要更节省存储空间，那么 VARCHAR 可能是更好的选择。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;聚合函数有哪些？&lt;/h2&gt;
&lt;p&gt;数据库中的聚合函数是用于对一组值执行计算并返回单一值的函数。常见的数据库聚合函数包括：&lt;/p&gt;
&lt;p&gt;COUNT： 用于计算某列或表中的行数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT COUNT(column_name) FROM table_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SUM： 用于计算某列的数值总和。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT SUM(column_name) FROM table_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AVG： 用于计算某列的数值平均值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT AVG(column_name) FROM table_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MIN： 用于找出某列的最小值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT MIN(column_name) FROM table_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MAX： 用于找出某列的最大值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT MAX(column_name) FROM table_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些聚合函数可以与 GROUP BY 子句一起使用，以便按照一个或多个列对数据进行分组，然后在每个组内&lt;/p&gt;
&lt;p&gt;应用聚合函数。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT column1, COUNT(column2)
FROM table_name
GROUP BY column1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将返回每个不同的 column1 值及其对应的 column2 计数。&lt;/p&gt;
&lt;p&gt;在使用这些聚合函数时，通常需要注意使用合适的 GROUP BY 子句，以确保获得正确的聚合结果&lt;/p&gt;
&lt;h2&gt;除了聚合函数之外还使用过哪些函数？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;字符串函数&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;CONCAT&lt;/code&gt;：连接两个或多个字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUBSTRING&lt;/code&gt; 或 &lt;code&gt;SUBSTR&lt;/code&gt;：提取子字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UPPER&lt;/code&gt;：将字符串转换为大写。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOWER&lt;/code&gt;：将字符串转换为小写。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CHAR_LENGTH&lt;/code&gt; 或 &lt;code&gt;LEN&lt;/code&gt;：返回字符串的长度。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TRIM&lt;/code&gt;：去除字符串首尾的空格。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;日期和时间函数&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;NOW()&lt;/code&gt;、 &lt;code&gt;CURRENT_TIMESTAMP()&lt;/code&gt;: 返回当前的日期和时间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;YEAR&lt;/code&gt;、&lt;code&gt;MONTH&lt;/code&gt;、&lt;code&gt;DAY&lt;/code&gt;：从日期中提取年、月、日。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIME&lt;/code&gt;：从日期时间值中提取时间部分。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;数学函数&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ABS(x)&lt;/code&gt;: 返回 x 的绝对值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ROUND&lt;/code&gt;：将数值四舍五入。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FLOOR&lt;/code&gt;：向下取整。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CEIL&lt;/code&gt; 或 &lt;code&gt;CEILING&lt;/code&gt;：向上取整。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RAND()&lt;/code&gt;: 返回一个随机数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POW(x, y)&lt;/code&gt;, &lt;code&gt;POWER(x, y)&lt;/code&gt;: 返回 x 的 y 次方。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;逻辑函数&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;NULLIF&lt;/code&gt;：如果两个表达式相等，则返回 NULL，否则返回第一个表达式的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TO_DATE&lt;/code&gt; 或 &lt;code&gt;TO_CHAR&lt;/code&gt;：将日期和时间转换为字符串，或字符串转换为日期和时间。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;连接查询有哪些方法？什么情况使用右外连接？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内连接
返回两个表中满足连接条件的行。如果两个表中没有匹配的行，那么就不会出现在结果集中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左外连接（LEFT JOIN 或 LEFT OUTER JOIN）
返回左表中的所有行，以及右表中满足连接条件的行。如果右表中没有匹配的行，将返回 NULL 值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右外连接（RIGHT JOIN 或 RIGHT OUTER JOIN）
返回右表中的所有行，以及左表中满足连接条件的行。如果左表中没有匹配的行，将返回 NULL 值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全外连接（FULL JOIN 或 FULL OUTER JOIN）
MySQL 本身不直接支持 FULL JOIN，但可以通过组合左连接和右连接来模拟全外连接的效果
全外连接返回左右两个表中所有匹配的行，同时也会返回左右表中没有匹配的行，这些没有匹配的行在另一个表中显示为 NULL。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右外连接适用于以下情况：
右外连接是根据右表为基础的，左表的数据是辅助的，即使左表中没有匹配的行，右表中的行也会出现在结果集中，并用 NULL 填充左表的列。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;例如，在一个订单系统中，你可能想列出所有客户，即使他们没有任何订单。在这种情况下，你会以客户表作为右表进行右外连接，以确保所有客户都被列出，无论他们是否下过订单。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;union 和 union all 有什么区别？&lt;/h2&gt;
&lt;p&gt;UNION 和 UNION ALL 都是用于合并两个或多个 SELECT 语句的结果集&lt;/p&gt;
&lt;p&gt;重复行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UNION： 它会合并两个结果集并去除重复的行，即结果集中不会包含重复的行&lt;/li&gt;
&lt;li&gt;UNION ALL： 它会合并两个结果集，但不会去除任何重复的行，即结果集中可能包含重复的行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UNION： 由于要去除重复的行，UNION 操作可能会引起一些额外的性能开销。数据库系统需要进行排序
和去重的操作。&lt;/li&gt;
&lt;li&gt;UNION ALL： 由于不需要去除重复的行，UNION ALL 的性能通常比 UNION 更好。如果确实需要所有的
行，而不仅仅是不重复的行，使用 UNION ALL 可以避免额外的开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UNION： 使用 UNION 时，两个 SELECT 语句的列数和数据类型必须相同，否则会导致错误。&lt;/li&gt;
&lt;li&gt;UNION ALL： 对于 UNION ALL，两个 SELECT 语句的列数和数据类型也应该相同，但它允许更灵活一些。结果集中的顺序相同即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;between and 和 in 有什么区别？&lt;/h2&gt;
&lt;p&gt;BETWEEN 和 IN 是 SQL 中用于筛选数据的两种不同的条件操作符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BETWEEN 用于在给定的范围内筛选数据&lt;/li&gt;
&lt;li&gt;IN 用于指定一个值列表，以便筛选满足条件的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BETWEEN 后面需要两个值，用 AND 连接，表示一个范围。&lt;/li&gt;
&lt;li&gt;IN 后面可以跟一个值列表，用括号括起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;范围 vs 离散值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BETWEEN 用于指定一个范围，适用于连续的值。&lt;/li&gt;
&lt;li&gt;IN 用于指定一个离散的值列表，适用于不连续的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在某些情况下，BETWEEN 可能比 IN 更有效率，尤其是在有序索引的情况下。BETWEEN 的查询条件可以更容易地利用索引，因为它表示一个范围。&lt;/li&gt;
&lt;li&gt;IN 的性能可能在查询的值列表较长时略有下降，因为数据库需要逐一匹配值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;BETWEEN&lt;/code&gt; 更适合描述范围，而 &lt;code&gt;IN&lt;/code&gt; 更适合描述离散的值列表&lt;/p&gt;
&lt;h2&gt;delete 和 truncate table 有什么区别&lt;/h2&gt;
&lt;p&gt;操作类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DELETE 是一种行级别的操作，用于删除表中的特定行。&lt;/li&gt;
&lt;li&gt;TRUNCATE TABLE 是一种表级别的操作，用于删除整个表的所有数据，但保留表结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DELETE 可以在事务中使用，可以根据需要回滚或提交。DELETE 操作是事务安全的，这意味着你可以回滚这个操作。如果在删除过程中发生错误，你可以撤销更改，恢复数据。&lt;/li&gt;
&lt;li&gt;TRUNCATE TABLE 是一个 DDL（数据定义语言）操作，通常不能在事务中使用。一些数据库系统（如 MySQL）在使用 TRUNCATE TABLE 时会自动提交当前事务。
不是事务安全的，一旦执行，无法回滚。这意味着如果你错误地使用了 TRUNCATE TABLE，你将无法恢复数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;速度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DELETE 操作会生成事务日志，并逐行删除数据，因此相对较慢，尤其是在删除大量数据时。因为它需要单独记录每一行的删除&lt;/li&gt;
&lt;li&gt;由于 TRUNCATE TABLE 是一种表级别的操作，通常比 DELETE 更快，尤其是在删除大量数据时。
TRUNCATE TABLE 不记录删除的每一行，也不需要执行回滚段的管理,而是直接释放表的数据页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回滚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以使用 ROLLBACK 语句回滚 DELETE 操作，但不能回滚 TRUNCATE TABLE。&lt;/li&gt;
&lt;li&gt;在数据库系统中，TRUNCATE TABLE 通常不会写入事务日志，因此无法回滚。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;条件过滤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DELETE 可以使用 WHERE 子句来指定删除的条件，可以删除表中满足条件的特定行。&lt;/li&gt;
&lt;li&gt;TRUNCATE TABLE 通常不允许使用 WHERE 子句，它删除整个表的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;触发器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果表上有触发器，DELETE 操作会触发这些触发器。&lt;/li&gt;
&lt;li&gt;TRUNCATE TABLE 通常不会触发表上的触发器。TRUNCATE TABLE 不会触发任何触发器，因为它是作为一个 DDL（数据定义语言）语句来处理的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上所述，DELETE 用于删除表中的特定行，具有更多的灵活性，但相对较慢。
TRUNCATE TABLE 用于删除整个表的数据，速度更快，但不如 DELETE 灵活。选择使用哪个操作取决于具体的需求和性能要求。&lt;/p&gt;
&lt;h2&gt;什么是事务？&lt;/h2&gt;
&lt;p&gt;在数据库管理系统（DBMS）中，事务（Transaction）是指一系列数据库操作组成的逻辑工作单元，这些操作要么全部执行，要么全部不执行，以保证数据库的一致性和完整性。&lt;/p&gt;
&lt;p&gt;在 SQL 中，通过以下关键字来控制事务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BEGIN TRANSACTION： 用于开始一个事务。&lt;/li&gt;
&lt;li&gt;COMMIT： 用于提交事务，使其更改永久生效。&lt;/li&gt;
&lt;li&gt;ROLLBACK： 用于回滚事务，取消未提交的更改，将数据库回滚到事务开始前的状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;事务的四个特性？怎么实现原子性的？&lt;/h2&gt;
&lt;p&gt;事务具有以下四个关键属性，通常被称为 ACID 特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;原子性（Atomicity）： 事务是原子性的，即事务中的所有操作要么全部执行成功，要么全部不执行，不会
出现部分执行的情况。如果其中任何一个操作失败，整个事务将回滚到初始状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一致性（Consistency）： 事务执行后，数据库从一个一致的状态转变为另一个一致的状态。这意味着事务
在执行前后，不能破坏数据库数据的完整性和一致性，数据库应该保持一致性，不破坏数据完整性和业务规则。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隔离性（Isolation）： 多个事务可以同时在数据库中执行，但它们之间应该是相互隔离的，一个事务的执
行不应该影响其他事务的执行。隔离性确保并发事务的执行不会导致数据混乱或不一致。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;持久性（Durability）： 一旦事务提交，其对数据库的修改应该是永久性的，即使系统崩溃，重启后也应该
保留事务的结果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实现原子性的方式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原子性通过使用事务的 BEGIN、COMMIT 和 ROLLBACK 操作来实现。当一组 SQL 语句被包裹在 BEGIN
和 COMMIT 之间时，它们形成一个事务。如果事务中的所有 SQL 语句执行成功，那么可以通过 COMMIT
来提交事务，使得所有的更改永久生效。如果在事务执行过程中发生错误，可以通过 ROLLBACK 操作来回
滚事务，取消未提交的更改，将数据库回滚到事务开始前的状态。&lt;/p&gt;
&lt;p&gt;下面是一个简单的 SQL 示例，演示了如何使用事务和实现原子性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BEGIN TRANSACTION;
-- SQL 语句执行
-- 如果一切正常，提交事务
COMMIT; -- 如果发生错误，回滚事务
ROLLBACK;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在程序中，也可以使用编程语言提供的事务管理机制，如在许多编程语言中使用的数据库事务 API 来实现原 子性。这确保了一组操作的原子执行。&lt;/p&gt;
&lt;h2&gt;事务的隔离级别有哪些？分别会产生什么问题？怎么解决这些问题？&lt;/h2&gt;
&lt;p&gt;SQL 标准定义了四种事务隔离级别，每个级别提供一定程度的隔离，以处理并发事务可能引发的问题。这四个隔离级别由低到高分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;READ UNCOMMITTED（读未提交）：
允许一个事务读取另一个事务未提交的数据。这是最低的隔离级别，不提供任何隔离保护。
可能出现脏读、不可重复读和幻读的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;READ COMMITTED（读已提交）：
允许一个事务只能读取已经提交的其他事务的数据。这是大多数数据库系统的默认隔离级别。
可能出现不可重复读和幻读的问题，但解决了脏读的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;REPEATABLE READ（可重复读）：
保证一个事务在其生命周期内多次读取相同的数据，将返回相同的结果，即使其他事务已经修改了数据。其他事务的插入操作将被阻止。
可能出现幻读的问题，但解决了脏读和不可重复读的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SERIALIZABLE（串行化）：
提供最高的隔离级别，确保事务之间完全隔离。事务按顺序执行，所有事务都像是按照顺序串行执行的，没有并发。避免了脏读、不可重复读和幻读的问题，但会降低并发性能。 虽然能保证数据的一致性，但可能会导致大量的事务等待，降低了系统的吞吐量和性能。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;code&gt;SERIALIZABLE&lt;/code&gt;通常不作为常规使用的隔离级别，但在某些情况下，如非常关键的金融交易，可能需要使用串行化来确保绝对的数据一致性。
不同的隔离级别在性能和隔离性之间存在权衡。通常来说，如果应用程序对数据一致性要求较高，可以选择
较高的隔离级别，但需要注意可能的性能影响。在实际应用中，开发者需要仔细评估业务需求，并选择适当
的隔离级别。在某些情况下，也可以使用一些数据库系统提供的特定功能来解决并发问题，如行级锁、乐观锁等。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;并发事务可能引发一些问题，主要涉及到事务的隔离性。这些问题通常被称为并发控制问题，其中包括以下几个主要问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;脏读（Dirty Read）：
脏读发生在一个事务读取了另一个事务尚未提交的数据。如果这个事务最终回滚，读取到的数据就是无效的。&lt;/p&gt;
&lt;p&gt;解决方法： 设置事务隔离级别，使用更高的隔离级别，如 &lt;code&gt;READ COMMITTED&lt;/code&gt;， &lt;code&gt;REPEATABLE READ&lt;/code&gt;，&lt;code&gt;SERIALIZABLE&lt;/code&gt; 可以避免脏读。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不可重复读（Non-Repeatable Read）：
不可重复读发生在一个事务内的两个相同查询返回了不同的结果，因为在两次查询之间，另一个事务修改了数据。&lt;/p&gt;
&lt;p&gt;解决方法： 设置隔离级别，如：&lt;code&gt;REPEATABLE READ&lt;/code&gt;， &lt;code&gt;Serializable&lt;/code&gt;，或者使用锁定机制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;幻读（Phantom Read）：
幻读发生在一个事务内的两个相同查询返回了不同的结果，因为在两次查询之间，另一个事务插入或删除了数据，导致结果集发生变化。&lt;/p&gt;
&lt;p&gt;解决方法： 设置隔离级别&lt;code&gt;Serializable&lt;/code&gt;，通过强制事务串行化执行来避免幻读，或者使用锁定机制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;丢失更新（Lost Update）：
丢失更新发生在两个事务同时尝试更新相同数据，但只有一个更新生效，导致另一个更新的结果丢失。&lt;/p&gt;
&lt;p&gt;解决方法： 使用锁定机制，如悲观锁或乐观锁，确保同时只有一个事务可以更新数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;死锁（Deadlock）：
死锁是指两个或多个事务互相等待对方释放锁，从而导致它们都无法继续执行。&lt;/p&gt;
&lt;p&gt;解决方法： 使用事务超时机制、死锁检测和回滚机制，或者通过应用程序设计来避免死锁的发生。尽量避免同时执行诸如&lt;code&gt;INSERT&lt;/code&gt;、&lt;code&gt;UPDATE&lt;/code&gt;和&lt;code&gt;DELETE&lt;/code&gt;等数据修改语句。
解决这些问题的方法主要包括合理设置事务的隔离级别，使用锁定机制，以及采用一些并发控制技术，如乐观锁和悲观锁。不同的场景和要求可能需要不同的解决方案，因此在选择解决方法时需要仔细考虑应用程序的需求和性能要求。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;隔离级别&lt;/th&gt;
&lt;th&gt;脏读&lt;/th&gt;
&lt;th&gt;不可重复读&lt;/th&gt;
&lt;th&gt;幻读&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;READ UNCOMMITTED&lt;/td&gt;
&lt;td&gt;可能发生&lt;/td&gt;
&lt;td&gt;可能发生&lt;/td&gt;
&lt;td&gt;可能发生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;不会发生&lt;/td&gt;
&lt;td&gt;可能发生&lt;/td&gt;
&lt;td&gt;可能发生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;不会发生&lt;/td&gt;
&lt;td&gt;不会发生&lt;/td&gt;
&lt;td&gt;可能发生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;不会发生&lt;/td&gt;
&lt;td&gt;不会发生&lt;/td&gt;
&lt;td&gt;不会发生&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;mysql 中常见的引擎有哪些？有什么区别？&lt;/h2&gt;
&lt;p&gt;MyISAM：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;特点： 不支持事务，只支持表级锁定，不支持行级锁。适用于读操作频繁的场景，如查询比较多的静态网站。日志分析、数据分析和报表系统&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点： 查询速度快，占用磁盘空间较少。适合静态查询和插入操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点： 不支持事务、数据一致性较差。不支持外键约束，不提供崩溃恢复机制。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;InnoDB：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;特点： 支持 ACID 事务，行级锁定，适用于读写操作频繁的场景，是 MySQL 的默认存储引擎。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点： 支持事务、外键约束，行级锁定，提供崩溃恢复机制，数据一致性较好。适合高并发场景；如金融、电商等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点： 相对 MyISAM 更复杂，写操作性能可能较低。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
InnoDB 的默认数据结构是聚簇索引，而 MyISAM 是非聚簇索引
:::
MEMORY（或 HEAP）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;特点： 数据存储在内存中，适用于临时表和数据量较小的场景。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点： 读写速度快，适用于一些临时性数据存储。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点： 数据存储在内存中，数据是非持久化的，重启服务器或发生崩溃时数据丢失。内存有限，不适合存储大量数据。只支持表级锁定。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ARCHIVE：ao kai wu&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;特点： 数据以压缩格式存储，占用磁盘空间小。适用于大量归档数据的只读表或日志存储。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点： 压缩存储，适用于大量历史数据的归档。大量的日志数据压缩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点： 不支持索引、事务，只支持 INSERT 和 SELECT 操作。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;索引的分类？索引目的是？&lt;/h2&gt;
&lt;p&gt;MySQL 索引是一种用于提高查询性能的数据结构。索引能够快速定位和访问表中的特定行，从而加速数据
的检索和过滤。MySQL 支持多种类型的索引，主要包括以下几类：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;按功能分类&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单列索引(普通索引)（Single-Column Index）：
对表中的单个列创建的索引。没有唯一性的限制，允许为 NULL 值。
示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE INDEX index_name ON table_name(column_name);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复合索引（Composite Index）：
对表中的多个列创建的索引。复合索引覆盖了多个列，可以支持多列的查询。
示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE INDEX index_name ON table_name(column1, column2);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;唯一索引（Unique Index）：
与单列索引类似，但要求索引列的值在整个表中必须唯一。允许为 NULL 值，一个表允许多个列创建唯一索引
示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE UNIQUE INDEX index_name ON table_name(column_name);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主键索引
主键索引是一种特殊的唯一索引，一个表只能有一个主键，不允许有空值。一般是在建表的时候同时创建主键索引&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全文索引（Full-Text Index）：
用于全文搜索的索引类型，主要用于对文本数据进行模糊匹配。对文本的内容进行分词、搜索，可以提高文本数据的搜索效率。
示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE FULLTEXT INDEX index_name ON table_name(column_name);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间索引（Spatial Index）：
用于空间数据类型（如地理坐标）的索引，支持空间数据的查询。提高空间数据相关查询的效率。
示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE SPATIAL INDEX index_name ON table_name(column_name);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;按物理存储分类&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聚簇索引（Clustered Index）&lt;/li&gt;
&lt;li&gt;非聚簇索引（Secondary Index / Non-Clustered Index）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;索引作用：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;索引的主要目的是提高查询性能，通过减少数据库的扫描行数，加速数据的检索。索引能够加速 WHERE 子
句中的条件过滤、JOIN 操作和排序操作。在大型数据库中，使用合适的索引是优化查询性能的关键之一。
索引的作用包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加速数据检索： 通过索引，数据库可以快速定位到满足查询条件的数据，减少了数据的扫描和读取时间。&lt;/li&gt;
&lt;li&gt;加速排序操作： 当使用 ORDER BY 子句进行排序时，索引可以减少排序所需的时间。提高 GROUP BY 和 ORDER BY 子句的效率。&lt;/li&gt;
&lt;li&gt;加速连接操作： 在连接多个表时，索引可以加速连接的执行效率。&lt;/li&gt;
&lt;li&gt;保持唯一性： 唯一索引可以确保索引列的数值唯一，防止重复数据的插入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;尽管索引能够提高查询性能，但需要注意的是，过多或不合理使用索引可能会导致性能下降，因为每次对表
进行写操作时，都需要更新索引。因此，在设计索引时，需要综合考虑查询和更新操作的需求，选择适当的
索引类型和列。&lt;/p&gt;
&lt;p&gt;:::tip
不适合索引场景:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;小表或低频查询的表不需要索引。&lt;/li&gt;
&lt;li&gt;高频率写入的表，可能因索引维护导致性能问题。
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;索引有哪些优缺点以及具体有哪些索引类型&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;优：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加速数据检索： 通过索引，数据库可以快速定位到满足查询条件的数据，减少了数据的扫描和读取时间。&lt;/li&gt;
&lt;li&gt;加速排序操作： 当使用 ORDER BY 子句进行排序时，索引可以减少排序所需的时间。提高 GROUP BY 和 ORDER BY 子句的效率。&lt;/li&gt;
&lt;li&gt;加速连接操作： 在连接多个表时，索引可以加速连接的执行效率。&lt;/li&gt;
&lt;li&gt;保持唯一性： 唯一索引可以确保索引列的数值唯一，防止重复数据的插入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建索引和维护索引需要很多时间，这种时间随着数据量的增加而增加。&lt;/li&gt;
&lt;li&gt;如果一个数据建立了索引，那么增删改这个数据，相应的索引也要进行动态修改，这将大大降低 sql 的执行效率。&lt;/li&gt;
&lt;li&gt;需要占用物理存储空间：索引需要使用物理文件存储，会耗费一定空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;索引的类型：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;参考上面 ⬆️&lt;/p&gt;
&lt;h2&gt;什么是聚集索引和非聚集索引&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;简单来说，聚集索引就是基于主键创建的索引，除了主键索引以外的其他索引，称为非聚集索引，也叫做二级索引。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于在 InnoDB 引擎里面，一张表的数据对应的物理文件本身就是按照 B+树来组织的一种索引结构，而聚集索引就是按照每张表的主键来构建一颗 B+树，然后叶子节点里面存储了这个表的每一行数据记录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以基于 InnoDB 这样的特性，聚集索引并不仅仅是一种索引类型，还代表着一种数据的存储方式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时也意味着每个表里面必须要有一个主键，如果没有主键，InnoDB 会默认选择或者添加一个隐藏列&lt;code&gt;row_id&lt;/code&gt;作为主键索引来存储这个表的数据行。一般情况是建议使用自增 id 作为主键，这样的话 id 本身具有连续性使得对应的数据也会按照顺序存储在磁盘上，写入性能和检索性能都很高。否则，如果使用 uuid 这种随机 id，那么在频繁插入数据的时候，就会导致随机磁盘 IO，从而导致性能较低。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要注意的是，InnoDB 里面一张表只能存在一个聚集索引，原因很简单，如果存在多个聚集索引，那么意味着这个表里面的数据存在多个副本，造成磁盘空间的浪费，以及数据维护的困难。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于在 InnoDB 里面，主键索引表示的是一种数据存储结构，所以如果是基于非聚集索引来查询一条完整的记录，最终还是需要访问主键索引来检索。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;什么是回表？&lt;/h2&gt;
&lt;p&gt;通过非聚集索引查询时，找到叶子节点后，还需要拿着主键去聚集索引中再查一次完整数据，这个过程叫做回表。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;聚集索引的叶子节点：存储的是该行的全部数据。因此通过聚集索引查询时，找到叶子节点就拿到了完整数据，无需回表。&lt;/p&gt;
&lt;p&gt;非聚集索引的叶子节点：存储的是索引列的值 + 主键的值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;聚集索引和非聚集索引区别&lt;/h2&gt;
&lt;p&gt;区别：&lt;/p&gt;
&lt;p&gt;数据检索&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聚集索引：通过索引键直接定位到数据行，检索速度较快。&lt;/li&gt;
&lt;li&gt;非聚集索引：通过索引键查找到指针，再通过指针访问数据行，检索速度较慢。需要回表获取数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存储空间&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聚集索引：不需要额外的存储空间来存储索引叶节点，因为数据行本身就存储在叶节点中。&lt;/li&gt;
&lt;li&gt;非聚集索引：需要额外的存储空间来存储索引页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;插入和更新&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聚集索引：插入新数据或更新现有数据可能会导致数据页的重新排序，性能开销较大。&lt;/li&gt;
&lt;li&gt;非聚集索引：插入新数据或更新现有数据时性能开销较低，因为不影响数据表的物理存储顺序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;支持的数量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聚集索引：一个表只能有一个聚集索引。&lt;/li&gt;
&lt;li&gt;非聚集索引：一个表可以有多个非聚集索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么 MySQL 索引结构采用 B+树？&lt;/h2&gt;
&lt;p&gt;一般来说，数据库的存储引擎都是采用 B 树或者 B+树来实现索引的存储。首先来看 B 树
B 树是一种多路平衡树，用这种存储结构来存储大量数据，它的整个高度会相比二叉树来说，会矮很多。
而对于数据库而言，所有的数据都将会保存到磁盘上，而磁盘 I/O 的效率又比较低，特别是在随机磁盘 I/O 的情况下效率更低。
所以高度决定了磁盘 I/O 的次数，磁盘 I/O 次数越少，对于性能的提升就越大，这也是为什么采用 B 树作为索引存储结构的原因&lt;/p&gt;
&lt;p&gt;而 MySQL 的 InnoDB 存储引擎，它用了一种增强的 B 树结构，也就是 B+树来作为索引和数据的存储结构。
相比较于 B 树结构来说，B+树做了两个方面的优化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;B+树的所有数据都存储在叶子节点，非叶子节点只存储索引。&lt;/li&gt;
&lt;li&gt;叶子节点中的数据使用双向链表的方式进行关联。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 索引结构采用 B+树，有以下 4 个原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;从磁盘 I/O 效率方面来看：B+树的非叶子节点不存储数据，所以树的每一层就能够存储更多的索引数量，也就是说，B+树在层高相同的情况下，比 B 树的存储数据量更多，间接会减少磁盘 I/O 的次数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从范围查询效率方面来看：在 MySQL 中，范围查询是一个比较常用的操作，而 B+树的所有存储在叶子节点的数据使用了双向链表来关联，所以 B+树在查询的时候只需查两个节点进行遍历就行，而 B 树需要获取所有节点，因此，B+树在范围查询上效率更高。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从全表扫描方面来看：因为，B+树的叶子节点存储所有数据，所以 B+树的全局扫描能力更强一些，因为它只需要扫描叶子节点。而 B 树需要遍历整个树。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从自增 ID 方面来看：基于 B+树的这样一种数据结构，如果采用自增的整型数据作为主键，还能更好的避免增加数据的时候，带来叶子节点分裂导致的大量运算的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip
b 树：叶子节点和非叶子节点都会存储数据，指针和数据共同保存在同一节点中。
:::&lt;/p&gt;
&lt;h2&gt;Mysql 删除重复数据&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;--t_good 三个字段，id、type、name; 删除name重复的数据

delete from t_good where id not in (
	select t.id from (select min(id) as id from t_good group by name)t
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于 MVCC 的理解？&lt;/h2&gt;
&lt;p&gt;MVCC（Multi-Version Concurrency Control，多版本并发控制）&lt;/p&gt;
&lt;p&gt;对于 MVCC 的理解，可以先从数据库的三种并发场景说起：&lt;/p&gt;
&lt;p&gt;第一种：读读&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就是线程 A 与线程 B 同时在进行读操作，这种情况下不会出现任何并发问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二种：读写&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就是线程 A 与线程 B 在同一时刻分别进行读和写操作。这种情况下，可能会对数据库中的数据造成以下问题：
事物隔离性问题，出现脏读，幻读，不可重复读的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第三种：写写&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就是线程 A 与线程 B 同时进行写操作，这种情况下可能会存在数据更新丢失的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而 MVCC 就是为了解决事务操作中并发安全性问题(确保在高并发下，多个事务读取数据时不加锁也可以多次读取相同的值。)的无锁并发控制技术，全称为&lt;code&gt;Multi-Version Concurrency Control&lt;/code&gt;，也就是多版本并发控制。它是通过数据库记录中的隐式字段，undo 日志，Read View 来实现的。&lt;/p&gt;
&lt;p&gt;MVCC 主要解决了三个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一个是：通过 MVCC 可以解决读写并发阻塞问题（读不阻塞写，写不阻塞读）从而提升数据并发处理能力&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二个是：MVCC 采用了乐观锁的方式实现，读取数据时并不需要加锁，降低了死锁的概率&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三个是：解决了一致性读的问题，也就是事务启动时根据某个条件读取到的数据直到事务结束时，再次执行相同条件，还是读到同一份数据，不会发生变化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而我们在使用 MVCC 时一般会根据业务场景来选择组合搭配乐观锁或悲观锁。
这两个组合中，MVCC 用来解决读写冲突，乐观锁或者悲观锁解决写写冲突从而最大程度的提高数据库并发性能。&lt;/p&gt;
&lt;p&gt;:::tip
Read View&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ReadView 其实就是一个保存事务 ID 的 list 列表。记录的是本事务执行时，MySQL 还有哪些事务在执行，且还没有提交。(当前系统中还有哪些活跃的读写事务);&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Undo Log&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Undo Log 来保存数据的历史版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;MVCC 过程中会加锁吗？&lt;/h2&gt;
&lt;p&gt;MVCC 机制，全称(Multi-Version Concurrency Control)&lt;strong&gt;&lt;em&gt;多版本并发控制&lt;/em&gt;&lt;/strong&gt;，是确保在高并发下，多个事务读取数据时不加锁也可以多次读取相同的值。&lt;/p&gt;
&lt;p&gt;MVCC 在读已提交(READ COMMITTED)、可重复读(REPEATABLE READ 简称 RR)模式下才生效。&lt;/p&gt;
&lt;p&gt;MVCC 在可重复读的事物隔离级别下，可以解决脏读、脏写、不可重复读等问题。MVCC 是基于乐观锁的实现，所以很自然的想到 MVCC 是不是不会加锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题答案&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 MVCC 中，通常不需要加锁来控制并发访问。&lt;/p&gt;
&lt;p&gt;相反，每个事务都可以读取已提交的快照，而不需要获得共享锁或排它锁。&lt;/p&gt;
&lt;p&gt;在写操作的时候，MVCC 会使用一种叫为&lt;strong&gt;写时复制&lt;/strong&gt;(Copy-On-Write)的技术，也就是在修改数据之前先将数据复制一份，从而创建一个新的快照。&lt;/p&gt;
&lt;p&gt;当一个事务需要修改数据时，MVCC 会首先检查修改数据的快照版本号。&lt;/p&gt;
&lt;p&gt;是否与该事务的快照版本一致，如果一致则表示可以修改这条数据，否则该事务需要等待其他事务完成对该数据的修改。&lt;/p&gt;
&lt;p&gt;另外，这个事务在新快照之上修改的结果，事务对数据的修改不会直接覆盖原始数据，而是基于一个新的快照，其他事务可以继续读取原始数据的快照，原始数据的旧版本仍然保留在 Undo Log 中，供其他事务读取。从而解决了脏读、不可重复读问题。&lt;/p&gt;
&lt;p&gt;所以，正是有了 MVCC 机制，让多个事务对同一条数据进行读写时，不需要加锁也不会出现读写冲突。&lt;/p&gt;
&lt;h2&gt;索引失效场景&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在 where 后使用 or，如果 OR 条件中有一个字段没有索引，导致索引失效（尽量少用 or）&lt;/li&gt;
&lt;li&gt;使用 like ，like 查询是以%开头&lt;/li&gt;
&lt;li&gt;复合索引遵守&lt;strong&gt;最左前缀&lt;/strong&gt;原则，即在查询条件中使用了复合索引的第一个字段，索引才会被使用&lt;/li&gt;
&lt;li&gt;如果列类型是字符串，那一定要在条件中将数据使用引号引用起来,否则不使用索引&lt;/li&gt;
&lt;li&gt;正则表达式不使用索引。&lt;/li&gt;
&lt;li&gt;DATE_FORMAT()格式化时间，格式化后的时间再去比较，可能会导致索引失效。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;in&lt;/code&gt; 导致索引失效，使用 &lt;code&gt;NOT&lt;/code&gt; 或 &lt;code&gt;!=&lt;/code&gt;导致失效&lt;/li&gt;
&lt;li&gt;对于 order by、group by 、 union、 distinc 中的字段出现在 where 条件中时，才会利用索引&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;复合索引失效示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE INDEX idx_name_age ON users(name, age);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，name 和 age 列组成了一个复合索引。假设有一个复合索引 (name, age)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 索引会被使用
SELECT * FROM users WHERE name = &apos;Alice&apos;;

-- 索引会被使用
SELECT * FROM users WHERE name = &apos;Alice&apos; AND age = 25;

-- 索引不会被使用
SELECT * FROM users WHERE age = 25;

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一条查询使用了 name，符合最左前缀原则，索引会被使用。&lt;/li&gt;
&lt;li&gt;第二条查询同时使用了 name 和 age，也符合最左前缀原则，索引会被使用。&lt;/li&gt;
&lt;li&gt;第三条查询只使用了 age，没有包含索引的最左列 name，索引不会被使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;如果列类型是字符串&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当列的数据类型是字符串（如 VARCHAR、CHAR 等），但在查询条件中未使用引号时，MySQL 会尝试进行隐式类型转换。这种隐式类型转换可能导致索引失效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MySQL 在比较两个值时，要求它们的类型一致。如果查询条件中的值与列的类型不匹配，MySQL 会进行隐式类型转换。例如：&lt;/p&gt;
&lt;p&gt;如果列 name 是字符串类型（VARCHAR），但查询条件中写成 WHERE name = 123，MySQL 会将 name 列的所有值隐式转换为数字类型。
这种隐式转换会导致索引失效，因为索引是基于原始数据类型的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设 name 列是字符串类型，并且有索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 索引会被使用
SELECT * FROM users WHERE name = &apos;Alice&apos;;

-- 索引不会被使用
SELECT * FROM users WHERE name = 123;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;sql 优化&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;方式 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;针对慢 SQL，我们可以使用关键字 explain 来查看当前 sql 的执行计划.可以重点关注 type key rows filterd 等字段，从而定位该 SQL 执行慢的根本原因&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;type&lt;/strong&gt;：range 表示使用了索引范围扫描。(type 表示访问类型，用于说明 MySQL 决定如何查找行)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;key&lt;/strong&gt;：表示实际使用的索引。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rows&lt;/strong&gt;：表示 MySQL 预计需要检查的行数。大概需要扫描多少行？（rows）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;filterd&lt;/strong&gt;表示 MySQL 估计经过 WHERE 子句过滤后，只有大约 xxxx% 的行会被保留下来。按条件过滤后的百分比，越大越好&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extra&lt;/strong&gt; 是否有回表、排序、临时表等额外开销？&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT ... FROM ... WHERE ...;

-- 推荐写法（更详细）
EXPLAIN FORMAT=JSON SELECT ... ;     -- JSON 格式（更易读）
EXPLAIN ANALYZE SELECT ... ;         -- MySQL 8.0.18+，实际执行并显示耗时（最推荐）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
type字段示例值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;system&lt;/strong&gt;：表只有一行记录，这是最好的情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;const&lt;/strong&gt;：通过主键或唯一索引匹配单行记录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;eq_ref&lt;/strong&gt;：对于多表连接，通过主键或唯一索引匹配单行记录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ref&lt;/strong&gt;：使用普通索引或唯一索引。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fulltext&lt;/strong&gt;：全文索引搜索。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;index_merge&lt;/strong&gt;：使用了多个索引。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unique_subquery&lt;/strong&gt;：使用了唯一索引的子查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;index_subquery&lt;/strong&gt;：使用了索引的子查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;range&lt;/strong&gt;：使用索引范围扫描。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;index&lt;/strong&gt;：全索引扫描。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;all&lt;/strong&gt;：全表扫描。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;方式 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Show Profile&lt;/code&gt;是 MySQL 提供的可以用来分析当前会话中，SQL 语句资源消耗情况的工具，可用于 SQL 调优的测量。在当前会话中，默认情况下处于 show profile 是关闭状态， 打开之后保存最近 15 次的运行结果。&lt;/p&gt;
&lt;p&gt;针对运行慢的 SQL，通过 profile 工具进行详细分析，可以得到 SQL 执行过程中所有的资源开销情况，&lt;strong&gt;如 IO 开销,CPU 开销,内存开销等。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;开启性能监控 &lt;code&gt;SET profiling = ON;&lt;/code&gt; 打开之后保存最近 15 次的运行结果。&lt;/p&gt;
&lt;p&gt;关闭 &lt;code&gt;SET profiling = OFF;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一旦开启了性能监控，你可以使用 &lt;code&gt;SHOW PROFILES&lt;/code&gt; 命令来查看可用的性能数据，然后再使用 &lt;code&gt;SHOW PROFILE&lt;/code&gt; 来查看具体的性能统计信息。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化方式：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查询有效的列信息即可，少用*代替列信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;联合索引中的列从左往右，命中越多越好&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;where 字句中 like%号,尽量放置在右边&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;避免索引列上使用函数或者运算，这样会导致索引失效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;避免使用 &lt;code&gt;SELECT DISTINCT&lt;/code&gt;，除非绝对必要，否则尽量避免使用 &lt;code&gt;DISTINCT&lt;/code&gt;，因为它会导致额外的排序操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;LIMIT&lt;/code&gt;分页，在分页查询中使用 LIMIT 和 OFFSET 来限制结果集的大小。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;公式：&lt;code&gt;LIMIT (n-1)*m , m&lt;/code&gt;，n 代表第几页，m 代表显示内容的数量&lt;/p&gt;
&lt;p&gt;:::tip
啥是慢 sql&lt;/p&gt;
&lt;p&gt;慢 SQL 是指执行时间过长的 SQL 查询。这类查询会消耗较多的数据库资源（如 CPU、内存、I/O），可能导致数据库性能下降，甚至影响整个系统的响应速度。
:::&lt;/p&gt;
&lt;p&gt;:::info
多慢算慢？&lt;/p&gt;
&lt;p&gt;如果一条 SQL 查询的执行时间超过 1 秒 ，就会被认为是慢查询。
:::&lt;/p&gt;
&lt;p&gt;这个阈值可以通过配置参数 &lt;code&gt;long_query_time&lt;/code&gt; 来调整。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;long_query_time&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认值通常是 10 秒（具体取决于 MySQL 配置）。&lt;/p&gt;
&lt;p&gt;可以通过以下命令将慢查询的阈值设置为更小的值（例如 0.5 秒）：所有执行时间超过 0.5 秒 的 SQL 查询都会被记录到 慢查询日志 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET GLOBAL long_query_time = 0.5;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 启用慢查询日志
SET GLOBAL slow_query_log = &apos;ON&apos;;

--查看慢查询日志文件路径
SHOW VARIABLES LIKE &apos;slow_query_log_file&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例日志内容：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Time: 2023-10-01T12:00:00.123456Z
# User@Host: root[root] @ localhost [127.0.0.1]
# Query_time: 0.612345  Lock_time: 0.000123 Rows_sent: 1  Rows_examined: 10000
SELECT * FROM users WHERE age &amp;gt; 30;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;慢查询日志的内容通常包括以下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询的执行时间（Query_time）。&lt;/li&gt;
&lt;li&gt;锁等待时间（Lock_time）。&lt;/li&gt;
&lt;li&gt;返回的行数（Rows_sent）。&lt;/li&gt;
&lt;li&gt;扫描的行数（Rows_examined）。&lt;/li&gt;
&lt;li&gt;具体的 SQL 语句。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MySQL优化方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;服务器优化（增加CPU、内存、网络、更换高性能磁盘）&lt;/li&gt;
&lt;li&gt;表设计优化（字段长度控制、添加必要的索引）&lt;/li&gt;
&lt;li&gt;SQL优化（避免SQL命中不到索引的情况）&lt;/li&gt;
&lt;li&gt;架构部署优化（一主多从集群部署）&lt;/li&gt;
&lt;li&gt;分库分表（垂直分库、水平分表）&lt;/li&gt;
&lt;li&gt;编码优化实现读写分离&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MySQL索引底层原理&lt;/h2&gt;
&lt;p&gt;MySQL的索引底层结构是B+树。&lt;/p&gt;
&lt;p&gt;B+树是一种平衡多路搜索树，具有以下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所有关键字保存在叶子节点，并且叶子节点之间通过链表连接，形成一个有序的叶子节点序列。&lt;/li&gt;
&lt;li&gt;非叶子节点只存储索引字段的值和子节点的指针，不保存实际的数据。这样可以使得一个节点可以存储更多的关键字，减少了树的高度，加快搜索速度。&lt;/li&gt;
&lt;li&gt;叶子节点包含所有索引字段的值和指向对应数据的指针。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在B+树索引中，每个节点的大小是固定的，与磁盘页的大小相当。节点的大小通常是数据库页的大小，例如16KB或32KB。每个节点可以存储多个关键字和指针。叶子节点的关键字是有序的，且通过链表连接在一起。&lt;/p&gt;
&lt;p&gt;索引查询快的原因有以下几点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;路径长度短：B+树具有平衡性，所有叶子节点的深度相同，因此在查询过程中只需要沿着树的高度进行几次磁盘I/O操作，所以查询速度较快。&lt;/li&gt;
&lt;li&gt;顺序访问优势：B+树的叶子节点之间使用链表连接，并且叶子节点的关键字是有序的，因此对于范围查询操作，可以通过顺序扫描叶子节点来获取有序的数据结果，提高查询速度。&lt;/li&gt;
&lt;li&gt;最小化磁盘I/O操作：B+树具有较高的填充因子，每个磁盘页上存储的关键字数量较多，能够减少磁盘I/O操作的次数，提高查询效率。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综上所述，B+树的平衡性、有序的叶子节点、顺序访问以及最小化的磁盘I/O操作是使得索引查询快速的关键因素。通过B+树的数据结构和索引的建立，可以大幅度减少磁盘访问次数，提高数据库查询的效率。&lt;/p&gt;
</content:encoded></item><item><title>运维等 Interview</title><link>https://zzyang.top/posts/ops-interview/</link><guid isPermaLink="true">https://zzyang.top/posts/ops-interview/</guid><pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Docker&lt;/h2&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h2&gt;Nginx&lt;/h2&gt;
&lt;h3&gt;nginx是什么？优势？&lt;/h3&gt;
&lt;p&gt;一个高性能的Web服务器和反向代理服务器&lt;/p&gt;
&lt;p&gt;可以容易的做负载均衡，更好的面对高并发的场景。&lt;/p&gt;
&lt;h3&gt;Nginx应用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Http服务器 可以做网页静态服务器，托管 HTML、CSS、JavaScript、图片、视频等静态资源。&lt;/li&gt;
&lt;li&gt;反向代理/负载均衡，当网站的访问量达到一定程度后，单台服务器不能满足用户的请求时，需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载，不会因为某台服务器负载高宕机而某台服务器闲置的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;负载均衡策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;轮询&lt;/p&gt;
&lt;p&gt;每个请求按时间顺序逐一分配到不同的后端服务器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加权轮询&lt;/p&gt;
&lt;p&gt;指定轮询几率，用于后端服务器性能不均的情况。(权重/加权),权重越高分配的请求越多，权重越低，请求越少。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ip_hash&lt;/p&gt;
&lt;p&gt;ip_hash 方式的负载均衡，可以使来自同一个 IP 的客户端固定访问一台 Web 服务器，从而就解决了 Session 共享问题。(己经淘汰)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;正向代理？反向代理？&lt;/h3&gt;
&lt;p&gt;反向：是代理服务器端，隐藏真实服务器&lt;/p&gt;
&lt;p&gt;正向：代理的是客户端(常见的vpn)&lt;/p&gt;
&lt;h2&gt;Linux&lt;/h2&gt;
&lt;h3&gt;如何查看进程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看所有进程
ps -ef

# 查找特定进程
ps -ef | grep java

ps aux

ps aux | grep java

# 实时监控 动态显示系统中各进程的资源占用状况
top

kill 2868  # 杀掉2868编号的进程
kill -9 2868  # 强制杀死进程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ps aux&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列出所有进程的详细信息。&lt;code&gt;a&lt;/code&gt; 表示显示所有用户的进程，&lt;code&gt;u&lt;/code&gt; 表示以用户友好的格式显示，&lt;code&gt;x&lt;/code&gt; 表示显示没有控制终端的进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;端口占用排查&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;netstat -tulnp&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;显示监听端口的详细信息。&lt;code&gt;t&lt;/code&gt; 表示 TCP，&lt;code&gt;u&lt;/code&gt; 表示 UDP，&lt;code&gt;l&lt;/code&gt; 表示监听状态，&lt;code&gt;n&lt;/code&gt; 表示数字地址，&lt;code&gt;p&lt;/code&gt; 表示显示进程信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lsof -i :port&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;查看指定端口的使用情况。例如，&lt;code&gt;lsof -i :8080&lt;/code&gt; 查看端口 8080 的使用情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ss -tulnp&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;显示监听端口的详细信息。&lt;code&gt;ss&lt;/code&gt; 命令类似于 &lt;code&gt;netstat&lt;/code&gt;，但通常更快。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;文件查看命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;cat - 查看整个文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 基本用法
cat filename.txt

# 显示行号
cat -n filename.txt

# 显示不可见字符（如换行、制表符）
cat -A filename.txt

# 合并多个文件
cat file1.txt file2.txt &amp;gt; merged.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;less - 分页查看&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 查看文件
less filename.txt

# 查看时常用命令：
# 空格      - 下一页
# b         - 上一页
# 回车      - 下一行
# /关键词   - 搜索
# n         - 下一个搜索结果
# N         - 上一个搜索结果
# g         - 跳到文件开头
# G         - 跳到文件末尾
# q         - 退出

# 打开时跳到指定行
less +100 filename.txt

# 打开时搜索
less +/error filename.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;more - 简单分页（较老）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;more filename.txt
# 空格翻页，回车下一行，q退出
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;head - 查看文件开头&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 默认显示前10行
head filename.txt

# 显示前20行
head -20 filename.txt

# 显示前100个字节
head -c 100 filename.txt

# 查看多个文件
head -n 5 file1.txt file2.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;tail - 查看文件结尾（最常用）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 默认显示最后10行
tail filename.txt

# 显示最后20行
tail -20 filename.txt

# 实时跟踪文件更新（查看日志神器）
tail -f access.log

tail -n 20 -f 2.txt   # 动态查看后20行内容

# 多文件实时监控
tail -f log1.log log2.log
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解压与压缩&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 打包：
tar –cvf  xxx.tar 要打包的件
# 打包并且压缩：
tar –czvf xxx.tar.gz 要压缩的文件
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 解压到user/aaa目录中
tar -xzvf xxx.tar.gz -C /usr/aaa
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;字母	含义	英文	说明
c	创建	create	 创建一个新的归档文件
z	gzip	zip	     通过 gzip 压缩/解压
v	详细	verbose	 显示处理的文件列表
f	文件	file	   指定归档文件名

# 可以理解为：
c = Create（创建）
z = gZip（压缩）
v = Verbose（啰嗦模式，显示详情）
f = File（文件名）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;如何分配权限？&lt;/h3&gt;
&lt;p&gt;Linux 权限检查遵循 “所有者 &amp;gt; 所属组 &amp;gt; 其他人” 的严格优先级&lt;/p&gt;
&lt;p&gt;Linux 中每个文件/目录都有权限身份：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;身份&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;u&lt;/strong&gt; (user)&lt;/td&gt;
&lt;td&gt;文件所有者&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;g&lt;/strong&gt; (group)&lt;/td&gt;
&lt;td&gt;文件所属组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;o&lt;/strong&gt; (other)&lt;/td&gt;
&lt;td&gt;其他人&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;a&lt;/strong&gt; (all)&lt;/td&gt;
&lt;td&gt;所有身份&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;每种身份有三种权限：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;权限&lt;/th&gt;
&lt;th&gt;字符&lt;/th&gt;
&lt;th&gt;数字&lt;/th&gt;
&lt;th&gt;对文件的意义&lt;/th&gt;
&lt;th&gt;对目录的意义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;读&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;r&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;查看文件内容&lt;/td&gt;
&lt;td&gt;列出目录内容（ls）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;写&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;w&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;修改文件内容&lt;/td&gt;
&lt;td&gt;在目录中创建/删除文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;执行&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;x&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;运行文件（脚本/程序）&lt;/td&gt;
&lt;td&gt;进入目录（cd）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;数字法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;数字表示法使用三位数字来表示权限，每一位数字代表一组权限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4&lt;/strong&gt;：读取权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2&lt;/strong&gt;：写入权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1&lt;/strong&gt;：执行权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;code&gt;chmod [所有者权限][组权限][其他人权限] 文件名&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 给 script.sh 设置权限：所有者(rwx=7), 组(rx=5), 其他(rx=5)
chmod 755 script.sh

# 给 config.ini 设置权限：所有者(rw=6), 组(r=4), 其他(r=4)
chmod 644 config.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;符号法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;符号表示法使用符号来表示要更改的权限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;u&lt;/strong&gt;：所有者。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;g&lt;/strong&gt;：所属组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;o&lt;/strong&gt;：其他用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;a&lt;/strong&gt;：所有用户（等同于 ugo）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;+&lt;/strong&gt;：添加权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-&lt;/strong&gt;：移除权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;=&lt;/strong&gt;：设置权限（清除现有权限并设置新权限）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 给所有者添加执行权限
chmod u+x script.sh

# 移除其他人的写权限
chmod o-w file.txt

# 让所有人都能读
chmod a+r file.txt

# 让其他人 (others) 只有 读 (r) 权限
chmod o=r test.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;环境变量配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看所有环境变量
env
printenv

# 查看单个变量
echo $变量名
echo $HOME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;临时设置（当前会话有效）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 设置变量（当前终端有效）
export MY_VAR=&quot;hello&quot;

# 取消变量
unset MY_VAR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;永久配置（用户级别）&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;修改 ~/.bashrc（推荐，仅当前用户）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 编辑配置文件
vim ~/.bashrc

# 在文件末尾添加
export MY_APP_HOME=/home/user/myapp

# 添加自定义别名
alias ll=&apos;ls -la&apos;
alias gs=&apos;git status&apos;

# 生效配置
source ~/.bashrc
# 或
. ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;系统级配置（所有用户）&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;修改 /etc/profile（影响所有用户）&lt;/li&gt;
&lt;li&gt;修改 /etc/environment（纯变量定义）&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Redis Interview</title><link>https://zzyang.top/posts/redis-notes/</link><guid isPermaLink="true">https://zzyang.top/posts/redis-notes/</guid><pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;谈谈你对Redis的理解&lt;/h3&gt;
&lt;p&gt;Redis是一个高性能的基于Key-Value结构存储的NoSQL开源数据库。大部分公司
采用Redis来实现分布式缓存，用来提高数据查询效率。&lt;/p&gt;
&lt;h3&gt;Redis为什么这么快？&lt;/h3&gt;
&lt;p&gt;决定Redis请求效率的因素主要是三个方面：分别是网络、CPU、内存。&lt;/p&gt;
&lt;p&gt;1.运行在内存中：绝大部分请求是纯粹的内存操作，非常快速；  cpu&amp;gt;内存&amp;gt;磁盘&lt;/p&gt;
&lt;p&gt;2.redis是运行线程是单线程。采用单线程，避免了不必要的上下文切换和竞争条件，也不存在多进程或者多线程导致的切换而消耗 CPU，不用去考虑各种锁的问题，不存在加锁释放锁操作，没有因为可能出现死锁而导致的性能消耗；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么采用单线程？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;redis运行中性能瓶颈不是cpu数（也就是多线程作用不大，是内存大小），单线程的好处可以避免多线程时的上下文切换。&lt;/p&gt;
&lt;p&gt;如果采用多线程，对于Redis中的数据操作，都需要通过同步的方式来保证线程安全性，这反而会影响到redis的性能&lt;/p&gt;
&lt;p&gt;3.采用多路复用I/O模式&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里“多路”指的是多个网络连接，“复用”指的是复用同一个线程&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Redis 可以用来做什么&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;缓存&lt;/li&gt;
&lt;li&gt;排行榜&lt;/li&gt;
&lt;li&gt;分布式计数器&lt;/li&gt;
&lt;li&gt;分布式锁&lt;/li&gt;
&lt;li&gt;消息队列&lt;/li&gt;
&lt;li&gt;分布式token&lt;/li&gt;
&lt;li&gt;限流&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;RDB和AOF的实现原理以及优缺点?&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;两种持久化机制的特性：
RDB和AOF都是Redis里面提供的持久化机制，RDB是通过快照方式实现持久化、 AOF是通过命令追加的方式实现持久化。

这两种机制的工作原理:
RDB持久化机制会根据快照触发条件，把内存里面的数据快照写入到磁盘，以二进制的压缩文件进行存储。 
RDB快照的触发方式有很多，比如： 
1.执行bgsave命令触发异步快照，执行save命令触发同步快照，同步快照会阻塞客户端的执行指令。 
2.根据redis.conf文件里面的配置，自动触发bgsave
3.主从复制的时候触发

AOF持久化机制是近乎实时的方式来完成持久化的，就是客户端执行一个数据变更的操作，Redis Server就会把这个命令追加到aof缓冲区的末尾，然后再把缓冲区的数据写入到磁盘的AOF文件里面，至于最终什么时候真正持久化到磁盘，是根据刷盘的策略来决定的。
为了避免追加的方式导致AOF文件过大的问题，Redis提供了AOF重写机制，也就是说当AOF文件的大小达到某个阈值的时候，就会把这个文件里面相同的指令进行压缩。

AOF和RDB的优缺点分析
RDB和AOF的优缺点有两个： 
RDB是每隔一段时间触发持久化，因此数据安全性低，AOF可以做到实时持久化，数据安全性较高;
RDB文件默认采用压缩的方式持久化，AOF存储的是执行指令，所以RDB在数据恢复的时候性能比AOF要好
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;redis五种基本类型，及其应用场景?&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;String
String最常规的 set/get 操作，Value 可以是 String 也可以是数字。可以设置TTL过期时间与自动续期，一般做一些复杂的计数功能的缓存。
String类型的key，是有最小内存大小的，512mb，如果有很多个key但没有512MB就有些浪费内存。如果key超过512位就会扩容

- 分布式锁：利用 SETNX（Set if Not eXists）实现基础的分布式锁功能。
- 计数器：利用 INCR 指令实现点赞数、访问量统计。

Hash
缓存的数据具有相同的大key
适用于存储对象、缓存， 存储要秒杀的商品
这里 Value 存放的是结构化的对象，比较方便的就是操作其中的某个字段。

list
有序、可重复的字符串集合，Redis中的list数据结构是队列，底层实现是一个双向链表，(特点是先进先出)
​	应用场景
​	有限资源抢购(优惠券，满减券)
​	按照顺序执行的场景(已经淘汰，有更好的技术替代了，rabbitmq)	
 适用于消息队列和发布/订阅系统：利用 LPUSH 和 BRPOP 实现简单的异步消息处理。


set		无序、不可重复的字符串集合
​	应用场景
适用于标签系统和好友关系等，常用命令：
SADD 用于向集合添加成员，SMEMBERS 用于获取集合所有成员

​	点赞，收藏，访问次数，需要去重等功能		
去重统计：统计 IP 访问量（UV）。		

set中
添加是add
移除是remove
访问查看是members


Zset
​	有序的字符串集合，可以对数据排序，根据数据中的权重来排序
​	应用场景： 适用于排行榜和按分数范围获取成员
​	排行榜，新闻排行

添加
​		添加是add方法
获取
​	range方法
​	根据数据下标取，权重从小到大
​	reverseRange方法
​		根据数据下标取，权重从大到小
​	rangeByScore
​		根据权重的范围取，权重从小到大
​	reverseRangeByScore
​		根据权重的范围取，权重从大到小
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;高级数据类型

Bitmaps (位图)
- 极度节省空间。一个 bit 只有 0 和 1 两种状态。
- 布隆过滤器 (Bloom Filter) 的底层实现。


GEO (地理位置)
- 它的底层实现其实是 Sorted Set (ZSet)，通过 GeoHash 算法将二维经纬度转换为一维的分数。
- 附近的人/店铺：如美团找附近美食、微信附近的人。

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;请说一下你对分布式锁的理解，以及分布式锁的实现&lt;/h3&gt;
&lt;p&gt;分布式锁和线程锁本质上是一样的，线程锁的生命周期是单进程多线程，分布式锁的声明周期是多进程多机器节点。
在本质上，他们都需要满足锁的几个重要特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;排他性，也就是说，同一时刻只能有一个节点去访问共享资源。&lt;/li&gt;
&lt;li&gt;可重入性，允许一个已经获得锁的进程，在没有释放锁之前再次重新获得锁。&lt;/li&gt;
&lt;li&gt;锁的获取、释放的方法&lt;/li&gt;
&lt;li&gt;锁的失效机制，避免死锁的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;关系型数据库，可以使用唯一约束来实现锁的排他性，如果要针对某个方法加锁，就可以创建一个表包含方法名称字段，并且把方法名设置成唯一的约束。
那抢占锁的逻辑就是：往表里面插入一条数据，如果已经有其他的线程获得了某个方法的锁，那这个时候插入数据会失败，从而保证了互斥性。
这种方式虽然简单啊，但是要实现比较完整的分布式锁，还需要考虑重入性、锁超时（防死锁）、没抢占到锁的线程要实现阻塞等，就会比较麻烦。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Redis，它里面提供了&lt;code&gt;SETNX&lt;/code&gt;命令可以实现锁的排他性，当key不存在就返回1，存在就返回0。然后还可以用&lt;code&gt;expire命令&lt;/code&gt;设置锁的失效时间，从而避免死锁问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然有可能存在锁过期了，但是业务逻辑还没执行完的情况。所以这种情况，可以写一个定时任务对指定的key进行续期。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如，SET lock_key value PX 10000 NX 会将名为 lock_key 的锁设置为在 10 秒后过期。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Redisson这个开源组件，就提供了分布式锁的封装实现，并且也内置了一个&lt;code&gt;Watch Dog机制&lt;/code&gt;来对key做续期(默认锁的时间是30s，还会开启一个子线程去检测主线程是否执行完，会自动续期)。
释放锁(内部用lua脚本实现的原子性删除)
Redis里面这种分布式锁设计已经能够解决99%的问题了，当然如果在Redis搭建了高可用集群的情况下出现主从切换导致key失效，这个问题也有可能造成，多个线程抢占到同一个锁资源的情况，所以Redis官方也提供了一个RedLock的解决办法，但是实现会相对复杂一些，或者使用Redisson中的MultiLock；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip
为了提高Redis的可用性，我们会搭建集群或者主从，以主从为例，主节点负责增删改，从节点负责读，主机会将数据同步给从机，但在主从同步完成之前，如果主节点宕机，Redis的哨兵机制会选择一个新的从节点作为主节点。然而，这个新的主节点上并没有之前的锁信息，导致锁失效。这样，当新的线程发来请求时，又可以获取到锁，从而出现两个线程并发访问安全问题。
:::&lt;/p&gt;
&lt;h3&gt;说说缓存雪崩和缓存穿透的理解，以及如何避免？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;缓存穿透&lt;/strong&gt;指客户端请求的数据在缓存中和数据库中都不存在，这样缓存永远不会生效，这些请求都会打到数据库。&lt;/p&gt;
&lt;p&gt;常见解决方案有两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;缓存空对象
当我们发现请求的数据即不存在于缓存，也不存于与数据库时，将空值缓存到Redis，并设置过期时间，避免频繁查询数据库。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优点：
实现简单，维护⽅便&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：
额外的内存消耗；可能发生不一致问题（在TTL内真的有对应数据存入数据库中）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;假如用户刚好请求了一个id，但是这个id的数据不存在，我们给缓存了个null，就在此时我们真的给这个id插入了一条数据，但是缓存中缓存的是null，出现了数据不一致的问题。&lt;/p&gt;
&lt;p&gt;我们可以在新增数据的时候，我们主动把redis中的数据进行覆盖掉，也可以解决这个问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;布隆过滤  内存占用少,空间利用率高，因为每个位置只需要占用一个bit位，不需要存实际数据，但实现复杂，并且因为hash冲突存在，可能有误判&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;优点：内存占用较少，没有多余key&lt;/li&gt;
&lt;li&gt;缺点：
实现复杂
存在误判可能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现原理：是用redis 中bitmap 数据类型来实现的。 bitmap: 很长的二进制数组&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;会把需要做缓存的数据，会根据查询条件(把该条件字段的值获取出来)在用hash算法算这个值，比如hash(20)得到一个二进制数组的下标，在把该下标值改成1&lt;/li&gt;
&lt;li&gt;在接口查询之前(过滤器/拦截器) 得到条件的值比如id=20,对值进行hash(20)运行，此时也会得到数组下标，判断下标对应的值是0 还是1，如果是0那么数据库一定没有该值，可以拦截。那么等于1数据可能有这个值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为有hash冲突的存在，两个不同的值，经过hash运算得到的值是一样&lt;code&gt;hash(100) = 2&lt;/code&gt; ，&lt;code&gt;hash(999) = 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;总结:因为hash冲突的存在，布隆过滤器判断没有，数据库一定没有，判断有的，数据库不一定有，但是这个机率可以控制0.000001(数组越小误判率就越大，数组越大误判率就越小，但是同时带来了更多的内存消耗。)&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;缓存雪崩&lt;/strong&gt;是指在同⼀时段 &lt;strong&gt;⼤量的缓存key同时失效&lt;/strong&gt; 或者 &lt;strong&gt;Redis服务宕机&lt;/strong&gt;，导致⼤量请求到达数据库，带来巨⼤压⼒。&lt;/p&gt;
&lt;p&gt;与缓存击穿的区别：雪崩是很多key，击穿是某一个key缓存。&lt;/p&gt;
&lt;p&gt;常见的解决方案有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于设置缓存时采用了相同的过期时间，导致缓存在某一时刻同时失效。因此给不同的Key在原本TTL的基础上添加随机值，这样KEY的过期时间不同，不会大量KEY同时过期&lt;/li&gt;
&lt;li&gt;利用Redis集群提高服务的可用性，避免缓存服务宕机&lt;/li&gt;
&lt;li&gt;给缓存业务添加降级限流策略（服务降级、快速失败等）&lt;/li&gt;
&lt;li&gt;给业务添加多级缓存，比如先查询本地缓存，本地缓存未命中再查询Redis，Redis未命中再查询数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;缓存击穿&lt;/strong&gt;问题也叫热点Key问题，就是⼀个被 &lt;strong&gt;⾼并发访问&lt;/strong&gt; 并且 &lt;strong&gt;缓存重建业务较复杂的key&lt;/strong&gt;突然失效了，⽆数的请求访问会在瞬间给数据库带来巨⼤的冲击。&lt;/p&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥锁：给重建缓存逻辑加锁，避免多线程同时进行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当线程1发现缓存过期并尝试重建缓存时，首先获取互斥锁，再查询数据库并写入缓存，之后释放锁。在重建过程中，有其他线程也发现缓存过期并尝试重建时，会获取互斥锁失败，休眠一会再尝试查询缓存和获取锁的操作，直到查询到新的缓存数据时直接返回。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;逻辑过期：热点key不要设置过期时间，通过逻辑过期字段标识是否过期。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当一个线程发现缓存已经过期时，获取互斥锁进行缓存重建，与前一种方案不同的是，缓存重建时会创建新的线程去完成，重建完成后释放互斥锁，自己直接返回过期数据。在重建缓存过程中，有新线程发现缓存过期并尝试重建时，会获取锁失败，此时直接返回过期数据。&lt;/p&gt;
&lt;h3&gt;缓存更新策略&lt;/h3&gt;
&lt;p&gt;内存淘汰&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不用自己维护，利用Redis的内存淘汰机制，当内存不足时自动淘汰部分数据。下次查询时更新缓存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;超时剔除&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给缓存数据添加TTL时间，到期后自动删除缓存。下次查询时更新缓存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;主动更新&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;编写业务逻辑，在修改数据库的同时，更新缓存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Cache Aside(缓存旁路模式)：由缓存调用者，在更新数据库的同时更新缓存。（代码复杂，但可人为控制）&lt;/li&gt;
&lt;li&gt;Read/Write Through(读写穿透模式)：缓存与数据库整合为一个服务，由服务来维护一致性。调用者调用该服务，无需关心缓存一致性问题。（维护服务复杂，无现成服务）&lt;/li&gt;
&lt;li&gt;Write Behind Caching(写回模式)：调用者只操作缓存，由其它线程异步的将缓存数据持久化到数据库，保证最终一致。（维护异步任务复杂，在异步进程修改数据库前，难以保证一致性，若服务器宕机，内存中的Redis数据将丢失 ）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Redis和MySQL如何保证数据一致性&lt;/h3&gt;
&lt;p&gt;延迟双删&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void write(id, newValue) {
    // 1. 第一次删除缓存
    cache.delete(id);
    
    // 2. 更新数据库
    db.update(id, newValue);
    
    // 3. 休眠一小段时间（如500ms）
    Thread.sleep(500);
    
    // 4. 第二次删除缓存
    cache.delete(id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cache Aside（缓存旁路模式）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;读操作：
[应用] → ①读缓存 → 有数据 → 返回
             ↓ 无数据
           ②读数据库
             ↓
           ③写回缓存
             ↓
           ④返回数据

写操作：
[应用] → ①更新数据库
             ↓
           ②删除缓存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么写操作是“删除缓存”而不是“更新缓存”？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免复杂计算&lt;/li&gt;
&lt;li&gt;避免并发写导致脏数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 场景：两个并发写请求
// 线程A：写操作1 → 更新缓存为value1
// 线程B：写操作2 → 更新缓存为value2
// 线程B的更新可能晚于线程A，但网络原因先完成
// 最终缓存 = value2，数据库 = value1 → 不一致！

// 如果用的是删除缓存：
// 无论顺序如何，缓存都被删除
// 下次读请求一定会从数据库拉取最新值
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Java 服务框架</title><link>https://zzyang.top/posts/spring-mj/</link><guid isPermaLink="true">https://zzyang.top/posts/spring-mj/</guid><pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;谈谈你对 Spring 的理解，springmvc，springboot 之间关系？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Sping&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Spring 框架的基本概念：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring 是一个开源的 Java 企业应用开发框架，旨在简化 Java 开发，提高代码的可维护性和可扩展性。
它提供了控制反转（IoC）和面向切面编程（AOP）等特性，帮助解决了传统 Java 应用中的一些设计问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;控制反转（IoC）和依赖注入（DI）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IoC 是 Spring 的核心概念，控制反转 Inversion of Control：由 spring 框架创建对象，对象不由我 们创建。所有 bean(对象)交给 spring 容器管理，由 spring 创建、销毁、统一管理(不使用 new 创建)，解决了对象间的耦合(解耦)&lt;/p&gt;
&lt;p&gt;DI 是 IoC 的一种具体实现，通过依赖注入，给对象定义的属性赋初值，的过程称为依赖注入&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring AOP： 面向切面编程，通过 AOP 可以将横切关注点（如日志、事务）从主要的业务逻辑中分离出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SpringMVC&lt;/strong&gt;： MVC 是 Spring 框架的一个模块，专门用于构建 Web 应用程序。Spring MVC 遵循 Model-View-Controller（MVC）设计模式；我们都是使用 spring 来实现基于 Java 的 Web 应用程序的 MVC 模式，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Spring Boot&lt;/strong&gt; 是 Spring 的子项目，用于简化 Spring 应用的开发和部署，它提供了自动化配置，可以减少开发者的配置工作，同时集成了嵌入式 Web 服务器，简化了应用的部署。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;@Transactional 失效场景&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;一、错误的传播机制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Spring 支持了 7 种传播机制，分别为：(默认的事务传播行为是 &lt;code&gt;PROPAGATION_REQUIRED&lt;/code&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PROPAGATION_REQUIRED&lt;/code&gt;：
定义：如果当前存在事务，则加入该事务；如果当前没有事务，则创建一个新的事务。
用途：这是最常见的传播行为，用于大部分需要事务的方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROPAGATION_SUPPORTS&lt;/code&gt;：
定义：如果当前存在事务，则加入该事务；如果当前没有事务，则以非事务方式执行。
用途：用于不需要强制事务的方法，但如果存在事务则加入。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROPAGATION_MANDATORY&lt;/code&gt;： man de te rui 强制性的
定义：如果当前存在事务，则加入该事务；如果当前没有事务，则抛出异常。
用途：用于必须在事务上下文中执行的方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROPAGATION_REQUIRES_NEW&lt;/code&gt;：
定义：创建一个新的事务，如果当前存在事务，则将当前事务挂起。
用途：用于需要在新事务中执行的操作，而不受现有事务的影响。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROPAGATION_NOT_SUPPORTED&lt;/code&gt;：
定义：以非事务方式执行操作，如果当前存在事务，则将当前事务挂起。
用途：用于不需要事务的操作，但可能存在于事务方法调用中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROPAGATION_NEVER&lt;/code&gt;：
定义：以非事务方式执行，如果当前存在事务，则抛出异常。
用途：用于严格禁止在事务上下文中执行的操作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROPAGATION_NESTED&lt;/code&gt;： nai si tei de 嵌套的
定义：如果当前存在事务，则创建一个嵌套事务（如果支持）；如果当前没有事务，则创建一个新的事务。
用途：用于需要嵌套事务支持的场景，允许在事务中开启新的事务，可以利用 savepoint 来实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面不支持事务的传播机制为：&lt;code&gt;PROPAGATION_SUPPORTS&lt;/code&gt;，&lt;code&gt;PROPAGATION_NOT_SUPPORTED&lt;/code&gt;，&lt;code&gt;PROPAGATION_NEVER&lt;/code&gt;。如果配置了这三种传播方式的话，在发生异常的时候，事务是不会回滚的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;rollbackFor 属性设置错误&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;默认情况下事务仅回滚运行时异常和 Error，不回滚受检异常（例如 IOException）。
因此如果方法中抛出了 IO 异常，默认情况下事务也会回滚失败。我们可以通过指定&lt;code&gt;@Transactional(rollbackFor = Exception.class)&lt;/code&gt;的方式进行全异常捕获。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;异常被内部 catch&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当异常被内部捕获，如果不显式地设置事务回滚，事务管理器不会自动检测到异常，事务将不会回滚，并且最终会提交。&lt;/p&gt;
&lt;p&gt;解决：
显式设置回滚：&lt;code&gt;TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;或者重新抛出异常&lt;/p&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内部调用（自调用）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.method()&lt;/code&gt;调用是直接的对象方法调用，不经过Spring代理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Spring AOP&lt;/code&gt;只能拦截外部调用，内部调用绕过了代理机制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;注入自己（获取代理对象）&lt;/li&gt;
&lt;li&gt;方案2：使用&lt;code&gt;AopContext.currentProxy()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;方案3：将事务方法抽取到另一个Service&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;嵌套事务&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果一个被&lt;code&gt;@Transactional&lt;/code&gt; 标注的方法直接调用另一个被&lt;code&gt;@Transactional&lt;/code&gt; 标注的方法，事务不会嵌套。&lt;/p&gt;
&lt;p&gt;要解决这个问题，可以使用&lt;code&gt;@Transactional(propagation = Propagation.REQUIRED)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二、代理不生效&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将注解标注在接口方法上&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt; 是支持标注在方法与类上的。一旦标注在接口上，对应接口实现类的代理方式如果是 &lt;code&gt;CGLIB&lt;/code&gt;，将通过生成子类的方式生成目标类的代理，将无法解析到&lt;code&gt;@Transactional&lt;/code&gt;，从而事务失效。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;被 &lt;code&gt;final、static&lt;/code&gt; 关键字修饰的类或方法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;CGLIB &lt;/code&gt;是通过生成目标类子类的方式生成代理类的，被 &lt;code&gt;final&lt;/code&gt;、&lt;code&gt;static&lt;/code&gt; 修饰后，无法继承父类与父类的方法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;类方法内部调用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;事务的管理是通过代理执行的方式生效的，如果是方法内部调用，将不会走代理逻辑，也就调用不到了。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当前类没有被 Spring 管理&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个没什么好说的，都没有被 &lt;code&gt;Spring&lt;/code&gt; 管理成为 &lt;code&gt;IOC&lt;/code&gt; 容器中的一个 &lt;code&gt;bean&lt;/code&gt;，更别说被事务切面代理到了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三、框架或底层不支持的功能&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;非 public 修饰的方法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不支持非 public 修饰的方法进行事务管理。因为Spring事务基于AOP代理实现，只有通过代理对象调用的public方法才能被拦截&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;多线程调用,异步方法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;异步方法在不同的线程中执行，
事务是基于&lt;code&gt;ThreadLocal&lt;/code&gt;的，不同线程间无法共享事务上下文&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据库本身不支持事务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;比如 &lt;code&gt;Mysql&lt;/code&gt; 的 &lt;code&gt;Myisam&lt;/code&gt; 存储引擎是不支持事务的，只有 &lt;code&gt;innodb&lt;/code&gt; 存储引擎才支持。
这个问题出现的概率极其小，因为 &lt;code&gt;Mysql5&lt;/code&gt; 之后默认情况下是使用 &lt;code&gt;innodb&lt;/code&gt; 存储引擎了。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;未开启事务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个也是一个比较麻瓜的问题，在 &lt;code&gt;Springboot&lt;/code&gt; 项目中已经不存在了，已经有 &lt;code&gt;DataSourceTransactionManagerAutoConfiguration&lt;/code&gt; 默认开启了事务管理。
但是在 MVC 项目中还需要在 applicationContext.xml 文件中，手动配置事务相关参数。如果忘了配置，事务肯定是不会生效的。&lt;/p&gt;
&lt;h2&gt;SpringIOC注入有哪几种方式？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;构造器注入 (Constructor Injection)  通过构造函数传入依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserService {
    private final UserRepository userRepository;

    // 显式定义构造函数
    @Autowired // Spring 4.3+ 如果只有一个构造函数，此注解可省略
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@RequiredArgsConstructor&lt;/code&gt; 是 Lombok 库提供的一个注解，它属于 构造器注入 (Constructor Injection) 的一种自动化实现方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@RequiredArgsConstructor // Lombok 会自动生成上面的构造函数
public class UserService {
    private final UserRepository userRepository;      // 1. 是 final -&amp;gt; 会被包含
    private final EmailService emailService;          // 2. 是 final -&amp;gt; 会被包含
    private String config;                            // 3. 既不是 final 也不是 @NonNull -&amp;gt; 被忽略
    @NonNull private Logger logger;                   // 4. 有 @NonNull -&amp;gt; 会被包含
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Setter 注入 (Setter Injection) 通过 setter 方法传入依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserService {
    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;字段注入 (Field Injection)  直接注入类的字段(@Autowired, @Resource)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserService {
    @Autowired // 直接打在字段上
    private UserRepository userRepository;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;方法注入 (@Lookup)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Spring 为了解决 单例 Bean 需要每次获取新的原型 Bean 这一特殊生命周期冲突而提供的&lt;/p&gt;
&lt;p&gt;:::tip
场景描述&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bean A：默认是 单例 (Singleton)（Spring 默认作用域），整个应用生命周期只有一个实例。&lt;/li&gt;
&lt;li&gt;Bean B：设置为 原型 (Prototype)，每次请求都需要一个新的实例。&lt;/li&gt;
&lt;li&gt;需求：A 需要依赖 B。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SingletonBean 内部持有的 PrototypeBean 永远都是同一个实例，失去了“原型”的意义（不再是每次获取都新建）。
:::&lt;/p&gt;
&lt;h2&gt;spring常用注解有哪些？&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;1、自定义类(自己写的类，要放到 spring 容器中)
@Controller controller 层
@Service service 层
@Repository 数据访问层
@Component 其他组件

2、自定义操作
@Scope 单例或多例
@PostConstruct. 标注在方法上，初始化对象后调用 同init-method属性
@PreDestroy 标注在方法上，对象销毁时调用  同destroy-method属性

3、注入相关的注解
@Autowired 注入一个对象
@Qualifier 注入类型，有多个对象符合标准，就用此注解区分

@Value 给属性注入值

4、代替 xml 文件
@Configuration---代替 xml 的
@ComponentScan----扫描包
@Bean----把第三方的类，可以在方法中生成，放到 spring 容器中
@PropertySource(classpath:xxx.properties)，读取 properties 文件
@Import，导入其他配置文件
@Conditional 注解的作用是为 Bean 的装载提供了一个条件判断。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;springmvc中的常用注解有哪些？&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping

@RestController

@RequestParam

@RequestBody

@ResponseBody

@PathVariable

@RequestHeader

@ControllerAdvice
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;@Resource和@Autowired的区别&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;@Autowired和@Qualifier是spring提供的注解，@Resource是jdk提供的注解&lt;/li&gt;
&lt;li&gt;@Autowired默认按类型注入，从IOC容器中找与之属性类型匹配的对象注入。特殊情况下一个接口下有多个实现类，会注入失败，可以根据 &lt;code&gt;@Qualifier&lt;/code&gt; 注解来指定具体的 Bean。
@Resource先按名称注入,名称不存在时,再按类型注入, 指定了名称注入，找不到抛出异常&lt;/li&gt;
&lt;li&gt;@Autowired只有一个属性required，默认值为true，为true时，找不到就抛异常，为false时，找不到就赋值为null&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;@Resource有两个常用属性name、type，注入时分4种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指定name和type：通过name找到唯一的bean，找不到抛出异常；如果type和字段类型不一致，也会抛出异常；&lt;/li&gt;
&lt;li&gt;指定name：通过name找到唯一的bean，找不到抛出异常；&lt;/li&gt;
&lt;li&gt;指定type：通过tpye找到唯一的bean，如果不唯一，则抛出异常；&lt;/li&gt;
&lt;li&gt;都不指定：通过&lt;a href=&quot;https://zhida.zhihu.com/search?q=%E5%AD%97%E6%AE%B5%E5%90%8D&quot;&gt;字段名&lt;/a&gt;作为key去查找，找到则赋值；找不到则再通过字段类型去查找，如果不唯一，则抛出异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解释 Spring 支持的几种 bean 的作用域？&lt;/h2&gt;
&lt;p&gt;singleton单例模式 &lt;code&gt;@Scope(&quot;singleton&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;spring容器中仅有一份对象，默认，Spring IoC 容器中只会存在一个共享的 Bean 实例，无论有多少个Bean 引用它，始终指向同一对象。该模式在多线程下是不安全的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;prototype原型模式 &lt;code&gt;@Scope(&quot;prototype&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次通过容器获得该对象时创建新对象，每个 Bean 实例都有自己的属性和状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;request &lt;code&gt;@Scope(&quot;request&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次请求创建一份新对象，对不同的 Http 请求则会产生新的 Bean，而且该 bean 仅在当前 Http Request 内有效,当前 Http 请求结束，该 bean实例也将会被销毁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;session &lt;code&gt;@Scope(&quot;session&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次会话创建一次新对象，对不同的 Session 请求则会创建新的实例，该 bean 实例仅在当前 Session 内有效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;globalsession&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;web环境下与session相同，小程序开发才会有区别&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;@PostConstruct使用注意事项&lt;/h2&gt;
&lt;p&gt;发生在依赖注入之后，可以在 &lt;code&gt;@PostConstruct&lt;/code&gt; 方法中安全地使用所有已注入的 &lt;code&gt;@Autowired&lt;/code&gt; 成员变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@PostConstruct&lt;/code&gt; 注解的方法必须是 public 的，并且不能有参数。&lt;/li&gt;
&lt;li&gt;如果一个类中定义了多个 &lt;code&gt;@PostConstruct&lt;/code&gt; 注解的方法，这些方法将按照它们在类中定义的顺序执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@PostConstruct&lt;/code&gt; 注解的方法可以抛出异常，如果抛出异常，容器可能会将这个 Bean 标记为销毁状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::info
场景 A：加载全局缓存，从数据库读取字典表、配置项加载到内存（Map/Redis），避免每次请求都查库。&lt;/p&gt;
&lt;p&gt;场景 B：校验关键配置，检查某些必须的配置文件或环境变量是否存在
:::&lt;/p&gt;
&lt;h2&gt;Spring bean 的生命周期？&lt;/h2&gt;
&lt;p&gt;spring bean的生命周期指的是从创建到销毁的整个过程。这个过程可以分为以下几个步骤，&lt;/p&gt;
&lt;p&gt;1、实例化。Spring容器根据配置文件或注解创建一个bean定义，这个定义描述了bean的类依赖关系等，然后容器使用java反射机制创建一个bean的实例。&lt;/p&gt;
&lt;p&gt;2、属性赋值。Spring容器将在bean实例化后通过调用set方法等方式来将属性赋值给bean，这个过程可以通过XML配置或注解来实现。&lt;/p&gt;
&lt;p&gt;3、初始化。在bean的属性赋值完成后，Spring容器会调用bean的初始化方法，这个方法可以是自定义的，需要在bean的配置文件或注解中进行定义。如果使用了&lt;code&gt;@PostConstruct&lt;/code&gt;或配置了&lt;code&gt;init-method&lt;/code&gt;属性，Spring IoC 容器会寻找 Bean 上的所有 &lt;code&gt;@PostConstruct&lt;/code&gt; 方法并调用它们。这些方法通常用来进行必要的初始化工作。&lt;/p&gt;
&lt;p&gt;4、使用。bean初始化完成后就可以使用了，这个时候bean已经被完全构建并准备好被其他组件使用。&lt;/p&gt;
&lt;p&gt;5、销毁。当bean不再需要使用时，spring容器会调用bean的销毁方法，如果使用了&lt;code&gt;@PreDestroy&lt;/code&gt;，或者配置了&lt;code&gt;destory-method&lt;/code&gt;属性，&lt;/p&gt;
&lt;p&gt;会在这个阶段被调用。&lt;/p&gt;
&lt;p&gt;总的来说，spring的生命周期包括实例化、属性赋值、初始化、使用和销毁等阶段。在这些阶段中，开发人员可以通过配置文件或注解来对bean的行为进行控制和定制，使得病的使用更加灵活和高效。同时，Spring框架提供了很多扩展点，可以让开发人员在病生命周期的不同阶段进行自定义处理，从而实现更加复杂的功能。&lt;/p&gt;
&lt;h2&gt;过滤器和拦截器有什么区别？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;运行顺序不同：过滤器是在Servlet容器接收到请求之后，但在Servlet被调用之前运行的；&lt;/p&gt;
&lt;p&gt;而拦截器则是在Servlet被调用之后，但在响应被发送到客户端之前运行的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置方式不同：&lt;/p&gt;
&lt;p&gt;过滤器是在web.xml中进行配置；web.xml 文件中配置 &lt;code&gt;&amp;lt;filter&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;filter-mapping&amp;gt;&lt;/code&gt;,&lt;code&gt;@WebFilter&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而拦截器的配置则是在Spring的配置文件中进行配置，或者使用注解进行配置。实现接口HandlerInterceptor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Filter依赖于Servlet容器，而Interceptor不依赖于Servlet容器，是springmvc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Filter在过滤是只能对request和response进行操作，&lt;/p&gt;
&lt;p&gt;而interceptor可以对request、response、handler、modelAndView、exception进行操作。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;SpringBoot自动装配原理&lt;/h2&gt;
&lt;p&gt;Spring Boot的自动装配实际上是从&lt;code&gt;META-INF/spring.factories&lt;/code&gt;文件中获取到对应的需要进行自动装配的类，并生成相应的Bean对象，然后将它们交给Spring容器进行管理&lt;/p&gt;
&lt;p&gt;在Spring Boot项目中有一个注解&lt;code&gt;@SpringBootApplication&lt;/code&gt;，这个注解是对三个注解进行了封装：&lt;code&gt;@SpringBootConfiguration&lt;/code&gt;、&lt;code&gt;@EnableAutoConfiguration&lt;/code&gt;、&lt;code&gt;@ComponentScan&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;@EnableAutoConfiguration&lt;/code&gt;是实现自动化配置的核心注解。&lt;/p&gt;
&lt;p&gt;该注解通过&lt;code&gt;@Import&lt;/code&gt;注解导入&lt;code&gt;AutoConfigurationImportSelector&lt;/code&gt;，这个类实现了一个导入器接口&lt;code&gt;ImportSelector&lt;/code&gt;。在该接口中存在一个方法&lt;code&gt;selectImports&lt;/code&gt;，&lt;/p&gt;
&lt;p&gt;该方法的返回值是一个数组，数组中存储的就是要被导入到spring容器中的类的全类名。在&lt;code&gt;AutoConfigurationImportSelector&lt;/code&gt;类中重写了这个方法,
该方法内部就是读取了项目的&lt;code&gt;classpath&lt;/code&gt;路径下&lt;code&gt;META-INF/spring.factories&lt;/code&gt;文件中的所配置的类的全类名。
在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。&lt;/p&gt;
&lt;h2&gt;SpringMVC执行流程&lt;/h2&gt;
&lt;p&gt;1.用户发起请求，请求先被 Servlet 拦截转发给 Spring MVC 框架&lt;/p&gt;
&lt;p&gt;2.Spring MVC 里面的 DispatcherSerlvet 核心控制器，会接收到请求并转发给HandlerMapping&lt;/p&gt;
&lt;p&gt;3.HandlerMapping 负责解析请求，根据请求信息和配置信息找到匹配的 Controller类，不过这里如果有配置拦截器，就会按照顺序执行拦                      截器里面的 preHandle方法&lt;/p&gt;
&lt;p&gt;4.找到匹配的 Controller 以后，把请求参数传递给 Controller 里面的方法&lt;/p&gt;
&lt;p&gt;5.Controller 中的方法执行完以后，会返回一个 ModeAndView，这里面会包括视图名称和需要传递给视图的模型数据&lt;/p&gt;
&lt;p&gt;6.视图解析器根据名称找到视图，然后把数据模型填充到视图里面再渲染成 Html 内容返回给客户端&lt;/p&gt;
&lt;h2&gt;Spring用到了哪些设计模式？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;创建型模式&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单例模式(Singleton)。Spring容器默认创建的bean都是单例的，保证系统中每个 bean 只有一个实例，减少系统开销。&lt;/li&gt;
&lt;li&gt;工厂模式(Factory)。Spring 使用工厂模式来创建对象,如 BeanFactory和ApplicationContext(&lt;code&gt;ClassPathXmlApplication&lt;/code&gt;)都是工厂模式的实现。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结构型模式&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;适配器模式(Adapter)。SpringMVC中的HandlerAdapter就是一个适配器模式的实现，将不同类型的处理器适配到相同的处理流程中。&lt;/li&gt;
&lt;li&gt;代理模式(Proxy)。SpringAOP就是基于代理模式实现的，通过动态代理的方式在运行时为目标对象增加额外的功能。&lt;/li&gt;
&lt;li&gt;装饰器模式(Decorator)。Spring的AOP也可以通过装饰器模式来实现，可以在运行时动态地为目标对象增加额外的功能。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;行为型模式&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;模板方法模式(Template Method)。Spring使用模板方法模式来实现一些固定流程的操作，如JdbcTemplate就是一个典型的模板方法模式的实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;观察者模式(Observer)。Spring事件机制使用观察者模式，将事件发布者和订阅者解耦，让系统更加灵活和可扩展。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;策略模式(Strategy)。Spring的HandlerMapping就是基于策略模式实现的，根据请求的URL来选择不同的处理策略。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SpringAOP&lt;/h2&gt;
&lt;p&gt;Spring AOP 核心概念&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;切面 (Aspect)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：在连接点上做的一系列行为，是&lt;strong&gt;通知 (Advice)&lt;/strong&gt; 与 &lt;strong&gt;切入点 (Pointcut)&lt;/strong&gt; 的整合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本质&lt;/strong&gt;：AOP 的整体封装单元，代表了横切关注点（如日志、事务、安全等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;切入点 (Pointcut)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：由多个&lt;strong&gt;连接点 (Joinpoint)&lt;/strong&gt; 组成，使用&lt;strong&gt;切点表达式&lt;/strong&gt;进行表达。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：筛选符合某种规则的连接点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象化&lt;/strong&gt;：将多个连接点用 &lt;code&gt;Pointcut&lt;/code&gt; 对象表达出来（例如：指定某一层的某个类，或某一层的全部类）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;连接点 (Joinpoint)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：程序执行过程中的特定位置，在 Spring AOP 中特指&lt;strong&gt;目标方法&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重点&lt;/strong&gt;：明确具体是&lt;strong&gt;哪个方法&lt;/strong&gt;被拦截。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;通知 (Advice)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：需要加入的公共程序逻辑（即要添加的具体代码）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通知类型&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;前置通知&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Before&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在目标方法执行&lt;strong&gt;之前&lt;/strong&gt;执行。&amp;lt;br&amp;gt;例：&lt;code&gt;@Before(&quot;execution(* com.woniu.controller.*.*(..))&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;后置通知&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@After&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在目标方法执行&lt;strong&gt;之后&lt;/strong&gt;执行（无论是否异常），类似 &lt;code&gt;finally&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;返回通知&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@AfterReturning&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在目标方法&lt;strong&gt;成功执行并返回结果后&lt;/strong&gt;执行。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;环绕通知&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Around&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;包围目标方法，可控制方法是否执行、何时执行及修改返回值。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;异常通知&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@AfterThrowing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在目标方法&lt;strong&gt;抛出异常后&lt;/strong&gt;执行。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;关键接口与参数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;org.aspectj.lang.JoinPoint&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;：封装了目标方法对象的信息（如方法签名、参数、代理对象等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用范围&lt;/strong&gt;：可用于 &lt;strong&gt;前置通知&lt;/strong&gt;、&lt;strong&gt;后置通知&lt;/strong&gt;、&lt;strong&gt;返回通知&lt;/strong&gt; 和 &lt;strong&gt;异常通知&lt;/strong&gt; 的方法参数中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;限制&lt;/strong&gt;：&lt;strong&gt;不能&lt;/strong&gt;用于环绕通知（环绕通知需使用其子接口）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;org.aspectj.lang.ProceedingJoinPoint&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;：&lt;code&gt;JoinPoint&lt;/code&gt; 的子接口，&lt;strong&gt;专用于 &lt;code&gt;@Around&lt;/code&gt; (环绕通知)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心方法&lt;/strong&gt;：&lt;code&gt;proceed()&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：继续执行被通知的目标方法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重要性&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;如果没有调用 &lt;code&gt;proceed()&lt;/code&gt;，目标方法将&lt;strong&gt;不会被执行&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;该方法&lt;strong&gt;必须返回对象&lt;/strong&gt;（通常是目标方法的返回值）。&lt;/li&gt;
&lt;li&gt;如果忽略返回值，后续对返回值的赋值或处理操作将不会生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;环绕通知示例代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Around(&quot;execution(* com.woniu.service.*.*(..))&quot;)
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    // 1. 前置逻辑
    System.out.println(&quot;环绕通知：方法执行前&quot;);

    // 2. 执行目标方法 (必须调用 proceed() 并接收返回值)
    Object result = pjp.proceed();

    // 3. 后置逻辑
    System.out.println(&quot;环绕通知：方法执行后&quot;);

    // 4. 返回结果 (必须返回，否则调用方获取不到值)
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;如何理解Spring Boot中的Starter？&lt;/h2&gt;
&lt;p&gt;Spring Boot的自动配置功能会根据项目中加入的Starter自动配置相关的Bean&lt;/p&gt;
&lt;p&gt;Starter 组件会把对应功能的所有jar包依赖全部导入进来，避免了开发者自己去引入依赖带来的麻烦。&lt;/p&gt;
&lt;p&gt;维护对应的jar包的版本依赖，使得开发者可以不需要去关心这些版本冲突这种容易出错的细节。&lt;/p&gt;
&lt;p&gt;Starter组件几乎完美的体现了Spring Boot里面约定优于配置的理念。&lt;/p&gt;
&lt;h2&gt;SpringCloud和SpringBoot的区别和关系&lt;/h2&gt;
&lt;p&gt;SpringBoot是用于简化Spring应用的开发和部署的框架，而Spring Cloud是构建分布式系统的解决方案，它基于SpringBoot并提供了分布式系统开发所需的组件与工具。简而言之，SpringBoot是单体应用的生产力工具，Spring Cloud是构建分布式系统的工具集。&lt;/p&gt;
&lt;h2&gt;SpringCloud五大组件是哪几个&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;服务注册与发现（Eureka）：Eureka是一个用于实现服务注册与发现的组件，提供了服务注册中心来管理服务实例的注册和发现，使得服务之间可以方便地进行通信和调用。&lt;/li&gt;
&lt;li&gt;客户端负载均衡（Ribbon）：Ribbon是一个用于在客户端实现负载均衡的组件，它可以根据一定的策略选择合适的服务实例进行负载均衡，提高系统的可用性和性能。&lt;/li&gt;
&lt;li&gt;服务调用（Feign）：Feign是一个声明式的服务调用组件，它基于注解和动态代理，可以让开发者使用简单的接口定义服务调用，而无需关注底层的具体实现。&lt;/li&gt;
&lt;li&gt;熔断器（Hystrix）：Hystrix是一个用于实现服务容错和熔断的组件，它可以保护系统免受服务故障的影响，通过实现服务降级、熔断和隔离等机制，提高系统的稳定性和可靠性。&lt;/li&gt;
&lt;li&gt;网关（Gateway）：Zuul或Gateway是用于构建统一的API网关的组件，它可以实现请求的路由、过滤和转发等功能，提供了对外的统一的接入点，并可以对请求进行安全验证、限流和监控等。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Nacos作为注册中心的工作原理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;注册过程：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;服务提供者启动后，将自己的元数据（如IP、端口、健康状态等）和服务信息，注册到Nacos注册中心。&lt;/li&gt;
&lt;li&gt;注册中心将该服务实例的元数据存储起来，并根据服务名进行索引。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;发现过程：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;服务调用者通过向Nacos注册中心发送服务发现请求，并提供服务名。&lt;/li&gt;
&lt;li&gt;注册中心根据服务名从存储的服务实例信息中找到对应的实例列表，并返回给服务调用者。&lt;/li&gt;
&lt;li&gt;服务调用者可以通过负载均衡算法选择其中一个实例进行服务调用。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;心跳检查和实例剔除机制：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;注册中心会周期性地进行心跳检查，检查服务实例是否发送了续约的心跳信号。&lt;strong&gt;默认情况下，Nacos的配置为每隔5秒进行一次心跳检查。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果发现某个实例在一定时间内没有发送心跳，注册中心会认为该实例失去连接，将其从注册表中剔除。&lt;strong&gt;默认情况下，Nacos在30秒内未收到心跳信号时将实例剔除。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;分布式事务框架使用&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;XA模式&lt;/li&gt;
&lt;li&gt;AT模式&lt;/li&gt;
&lt;li&gt;TCC&lt;/li&gt;
&lt;li&gt;Saga&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;XA模式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XA模式的优点是什么?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;事务的强一致性，满足ACID原则。&lt;/li&gt;
&lt;li&gt;常用数据库都支持，实现简单，并且没有代码侵入&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;XA模式的缺点是什么?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;因为一阶段需要锁定数据库资源，等待二阶段结束才释放，性能较差&lt;/li&gt;
&lt;li&gt;依赖关系型数据库实现事务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;AT模式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Seata主推的是AT模式，弥补了XA模型中资源锁定周期过长的缺陷。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简述AT模式与XA模式最大的区别是什么？&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;XA模式一阶段不提交事务，锁定资源；AT模式一阶段直接提交，不锁定资源。&lt;/li&gt;
&lt;li&gt;XA模式依赖数据库机制实现回滚；AT模式利用数据快照实现数据回滚。&lt;/li&gt;
&lt;li&gt;XA模式强一致；AT模式最终一致&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Saga 模式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将长事务拆分为多个本地事务，每个事务都有对应的补偿事务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;性能高，吞吐量强。&lt;/li&gt;
&lt;li&gt;支持长事务&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;缺点&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;最终一致性，不是强一致。&lt;/li&gt;
&lt;li&gt;需要设计大量补偿业务，代码侵入性高。&lt;/li&gt;
&lt;li&gt;缺乏隔离性，可能出现脏读。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;TCC（Try-Confirm-Cancel）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TCC 是一种补偿型分布式事务方案&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;性能较高（无全局锁），最终一致性&lt;/li&gt;
&lt;li&gt;强隔离性&lt;/li&gt;
&lt;li&gt;支持自定义粒度&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;缺点&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;代码侵入性极高（每个服务都要写 Try、Confirm、Cancel 三个方法）&lt;/li&gt;
&lt;li&gt;开发量大，容易出错&lt;/li&gt;
&lt;li&gt;需要大量业务补偿逻辑&lt;/li&gt;
&lt;li&gt;需要处理空回滚、悬挂、幂等&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;解决方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;空回滚&lt;/td&gt;
&lt;td&gt;Cancel 时 Try 未执行&lt;/td&gt;
&lt;td&gt;事务表判断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;幂等&lt;/td&gt;
&lt;td&gt;请求重复执行&lt;/td&gt;
&lt;td&gt;幂等控制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;悬挂&lt;/td&gt;
&lt;td&gt;Cancel 先于 Try&lt;/td&gt;
&lt;td&gt;Try 前检查&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;空回滚（Empty Rollback）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Try 阶段因网络超时实际未执行（或执行失败），但事务协调器认为超时需要回滚，触发了 Cancel 操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;增加事务控制表，记录每个分支事务的执行状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 事务日志表
transaction_record {
    xid,           -- 全局事务ID
    branch_id,     -- 分支事务ID
    state,         -- INIT / TRIED / CONFIRMED / CANCELLED
    create_time
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cancel 执行前检查：若状态为 INIT（Try未成功），则标记为 CANCELLED 并直接返回成功，不执行业务回滚逻辑。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;幂等性（Idempotency）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Confirm 或 Cancel 操作因网络超时触发重试机制，导致同一阶段被执行多次。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用数据库唯一键防重&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;悬挂（Suspension）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Try 阶段超时，Cancel 先执行，随后迟到的 Try 请求到达并执行，导致资源被永久预留但无法释放。&lt;/p&gt;
&lt;p&gt;时序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T1: Try 请求发送（网络延迟）
T2: 超时触发 Cancel（成功执行空回滚）
T3: Try 请求到达（执行成功，资源被预留）
T4: 事务结束，但预留资源永远无法释放（没有对应的 Cancel/Confirm）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cancel 标记机制：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Try 执行前检查：若发现该事务已存在 Cancel 记录，拒绝执行 Try，直接返回失败&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;一致性&lt;/th&gt;
&lt;th&gt;侵入性&lt;/th&gt;
&lt;th&gt;性能&lt;/th&gt;
&lt;th&gt;实现复杂度&lt;/th&gt;
&lt;th&gt;典型使用场景&lt;/th&gt;
&lt;th&gt;推荐指数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;最终一致性&lt;/td&gt;
&lt;td&gt;无侵入&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;最低&lt;/td&gt;
&lt;td&gt;绝大多数普通业务&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TCC&lt;/td&gt;
&lt;td&gt;最终一致性&lt;/td&gt;
&lt;td&gt;高侵入&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;高并发核心业务（如订单、支付）&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saga&lt;/td&gt;
&lt;td&gt;最终一致性&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;长流程业务（工单、审批、复杂流程）&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐☆&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XA&lt;/td&gt;
&lt;td&gt;强一致&lt;/td&gt;
&lt;td&gt;无侵入&lt;/td&gt;
&lt;td&gt;最低&lt;/td&gt;
&lt;td&gt;最低&lt;/td&gt;
&lt;td&gt;对强一致性要求极高、金融核心&lt;/td&gt;
&lt;td&gt;⭐⭐☆☆☆&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>XXL-JOB</title><link>https://zzyang.top/posts/xxl-job/</link><guid isPermaLink="true">https://zzyang.top/posts/xxl-job/</guid><pubDate>Sat, 15 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;XXL-JOB&lt;/h1&gt;
&lt;h2&gt;什么是任务调度？&lt;/h2&gt;
&lt;p&gt;我们可以思考一下下面业务场景的解决方案:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;某电商平台需要每天上午10点，下午3点，晚上8点发放一批优惠券&lt;/li&gt;
&lt;li&gt;某银行系统需要在信用卡到期还款日的前三天进行短信提醒&lt;/li&gt;
&lt;li&gt;某财务系统需要在每天凌晨0:10分结算前一天的财务数据，统计汇总&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上场景就是任务调度所需要解决的问题&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;任务调度是为了自动完成特定任务，在约定的特定时刻去执行任务的过程&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么需要分布式调度？&lt;/h2&gt;
&lt;p&gt;使用Spring中提供的注解@Scheduled，也能实现调度的功能&lt;/p&gt;
&lt;p&gt;在业务类中方法中贴上这个注解,然后在启动类上贴上&lt;code&gt;@EnableScheduling&lt;/code&gt;注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Scheduled(cron = &quot;0/20 * * * * ? &quot;)
 public void doWork(){
 	//doSomething
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;感觉Spring给我们提供的这个注解可以完成任务调度的功能，好像已经完美解决问题了，为什么还需要分布式呢?&lt;/p&gt;
&lt;p&gt;主要有如下这几点原因:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;高可用：单机版的定式任务调度只能在一台机器上运行，如果程序或者系统出现异常就会导致功能不可用。&lt;/li&gt;
&lt;li&gt;防止重复执行: 在单机模式下，定时任务是没什么问题的。但当我们部署了多台服务，同时又每台服务又有定时任务时，若不进行合理的控制在同一时间，只有一个定时任务启动执行，这时，定时执行的结果就可能存在混乱和错误了&lt;/li&gt;
&lt;li&gt;单机处理极限：原本1分钟内需要处理1万个订单，但是现在需要1分钟内处理10万个订单；原来一个统计需要1小时，现在业务方需要10分钟就统计出来。你也许会说，你也可以多线程、单机多进程处理。的确，多线程并行处理可以提高单位时间的处理效率，但是单机能力毕竟有限（主要是CPU、内存和磁盘），始终会有单机处理不过来的情况。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;XXL-JOB介绍&lt;/h2&gt;
&lt;p&gt;XXL-Job：是大众点评的分布式任务调度平台，是一个轻量级分布式任务调度平台, 其核心设计目标是开发迅速、学习简单、轻量级、易扩展&lt;/p&gt;
&lt;p&gt;大众点评目前已接入XXL-JOB，该系统在内部已调度约100万次，表现优异。&lt;/p&gt;
&lt;p&gt;目前已有多家公司接入xxl-job，包括比较知名的大众点评，京东，优信二手车，360金融 (360)，联想集团 (联想)，易信 (网易)等等&lt;/p&gt;
&lt;p&gt;官网地址 https://www.xuxueli.com/xxl-job/&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;系统架构图&lt;/strong&gt;
&lt;img src=&quot;../../assets/img/1894380034907439104.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设计思想&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将调度行为抽象形成&lt;strong&gt;调度中心&lt;/strong&gt;公共平台，而平台自身并不承担业务逻辑，“调度中心”负责发起调度请求。&lt;/p&gt;
&lt;p&gt;将任务抽象成分散的JobHandler，交由“执行器”统一管理，“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。&lt;/p&gt;
&lt;p&gt;因此，“调度”和“任务”两部分可以相互解耦，提高系统整体稳定性和扩展性；&lt;/p&gt;
&lt;h2&gt;调度中心部署&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;源码下载地址:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/xuxueli/xxl-job&quot;&gt;github地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitee.com/xuxueli0323/xxl-job&quot;&gt;gitee地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;执行该路径下的sql文件：&amp;lt;span class=&quot;marker-evy&quot;&amp;gt;xxl-job\doc\db\tables_xxl_job.sql&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;p&gt;需要修该项目配置文件下的数据源（&lt;strong&gt;application.properties文件&lt;/strong&gt;）
&lt;img src=&quot;../../assets/img/1895097847301275650.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里可以配置，如果任务执行失败，会给予通知
&lt;img src=&quot;../../assets/img/1895098390098739203.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;执行该启动文件&lt;code&gt;XxlJobAdminApplication&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;拉下来的xxl-job项目，配置文件内容&lt;code&gt;application.properties&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;### web
server.port=8080
server.servlet.context-path=/xxl-job-admin

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

### resources
spring.mvc.servlet.load-on-startup=0
spring.mvc.static-path-pattern=/static/**
spring.web.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.##########
spring.freemarker.settings.new_builtin_class_resolver=safer

### mybatis
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml

### 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, datasource
spring.datasource.url=jdbc:mysql://数据库ip:端口号/xxl_job?useUnicode=true&amp;amp;characterEncoding=UTF-8&amp;amp;autoReconnect=true&amp;amp;serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=数据库密码
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

### 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=xxx
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=default_token

### xxl-job, access token
xxl.job.timeout=3

### xxl-job, i18n (default is zh_CN, and you can choose &quot;zh_CN&quot;, &quot;zh_TC&quot; and &quot;en&quot;)
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

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调度中心访问地址: &lt;code&gt;http://localhost:8080/xxl-job-admin&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;默认登录账号 “admin/123456”&lt;/p&gt;
&lt;h2&gt;执行器部署&lt;/h2&gt;
&lt;p&gt;也就是使用我们现有项目，或新建一个demo项目&lt;/p&gt;
&lt;p&gt;添加依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.xuxueli&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;xxl-job-core&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.3.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置&lt;strong&gt;resources/application.properties或yaml&lt;/strong&gt;文件&lt;/p&gt;
&lt;p&gt;::: code-group&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;### 调度中心部署根地址 [选填]：如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行&quot;执行器心跳注册&quot;和&quot;任务结果回调&quot;；为空则关闭自动注册；
xxl:
  job:
    admin:
      addresses: http://127.0.0.1:8080/xxl-job-admin
    accessToken: default_token
    executor:
      appname: xxl-job-executor-sample
      address:
      ip: 127.0.0.1
      port: 9999
      logpath: /data/applogs/xxl-job/jobhandler
      logretentiondays: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;### 调度中心部署根地址 [选填]：如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行&quot;执行器心跳注册&quot;和&quot;任务结果回调&quot;；为空则关闭自动注册；
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器通讯TOKEN [选填]：非空时启用；
xxl.job.accessToken=default_token
### 执行器AppName [选填]：执行器心跳注册分组依据；为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
### 执行器注册 [选填]：优先使用该配置作为注册地址，为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]：默认为空表示自动获取IP，多网卡时可手动设置指定IP，该IP不会绑定Host仅作为通讯实用；地址信息用于 &quot;执行器注册&quot; 和 &quot;调度中心请求并触发任务&quot;；
xxl.job.executor.ip=127.0.0.1
### 执行器端口号 [选填]：小于等于0则自动获取；默认端口为9999，单机部署多个执行器时，注意要配置不同执行器端口；
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] ：需要对该路径拥有读写权限；为空则使用默认路径；
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] ： 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能；
xxl.job.executor.logretentiondays=30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::warning
xxl.job.accessToken=default_token 调度中心与执行器中的token必须一致
:::&lt;/p&gt;
&lt;h3&gt;添加执行器配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class XxlJobConfig {
    @Value(&quot;${xxl.job.admin.addresses}&quot;)
    private String adminAddresses;
    @Value(&quot;${xxl.job.accessToken}&quot;)
    private String accessToken;
    @Value(&quot;${xxl.job.executor.appname}&quot;)
    private String appname;
    @Value(&quot;${xxl.job.executor.address}&quot;)
    private String address;
    @Value(&quot;${xxl.job.executor.ip}&quot;)
    private String ip;
    @Value(&quot;${xxl.job.executor.port}&quot;)
    private int port;
    @Value(&quot;${xxl.job.executor.logpath}&quot;)
    private String logPath;
    @Value(&quot;${xxl.job.executor.logretentiondays}&quot;)
    private int logRetentionDays;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
        return xxlJobSpringExecutor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加一个简单的任务处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class SimpleXxlJob {
    @XxlJob(&quot;demoJobHandler&quot;)
    public void demoJobHandler() throws Exception {
        System.out.println(&quot;执行定时任务,执行时间:&quot; + new Date());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行程序后，简单配置一个demo
&lt;img src=&quot;../../assets/img/1895120092404908033.png&quot; alt=&quot;&quot; /&gt;
调度中心中点击启动，执行结果如下
&lt;img src=&quot;https://7up.pics/images/2025/03/03/Snipaste_2025-03-03_19-46-07.png&quot; alt=&quot;Snipaste 2025 03 03 19 46 07&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;GLUE模式(Java)&lt;/h2&gt;
&lt;p&gt;任务以源码方式维护在调度中心，支持通过Web IDE在线更新，实时编译和生效，因此不需要指定JobHandler。&lt;/p&gt;
&lt;p&gt;可以不在程序中进行编写即可执行&lt;/p&gt;
&lt;p&gt;假设程序中有该service业务方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class HelloService {
    public void methodA() {
        System.out.println(&quot;执行MethodA的方法&quot;);
    }

    public void methodB() {
        System.out.println(&quot;执行MethodB的方法&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建GLUE模式任务
&lt;img src=&quot;https://7up.pics/images/2025/03/03/Snipaste_2025-03-03_20-32-52.png&quot; alt=&quot;Snipaste 2025 03 03 20 32 52&quot; /&gt;
编辑GLUD IDE&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.xxl.job.service.handler;

import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
import org.springframework.beans.factory.annotation.Autowired;
import com.zzy.xxl.service.HelloService;

public class DemoGlueJobHandler extends IJobHandler {

  	@Autowired
    private HelloService helloService;

	@Override
	public void execute() throws Exception {
        helloService.methodA();
	}

}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注入要执行的业务层，调用执行的方法&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;导包路径一定要写正确&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果报了这个错&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Exception in thread &quot;xxl-job, EmbedServer bizThreadPool-1827643357&quot; java.lang.NoClassDefFoundError: javax/annotation/Resource
at com.xxl.job.core.glue.impl.SpringGlueFactory.injectService(SpringGlueFactory.java:45)
at com.xxl.job.core.glue.GlueFactory.loadNewInstance(GlueFactory.java:53)
at com.xxl.job.core.biz.impl.ExecutorBizImpl.run(ExecutorBizImpl.java:93)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加 javax.annotation-api 依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;javax.annotation&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;javax.annotation-api&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.3.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;启动后
&lt;img src=&quot;https://7up.pics/images/2025/03/03/Snipaste_2025-03-03_20-51-23.png&quot; alt=&quot;Snipaste 2025 03 03 20 51 23&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;执行器集群-负载均衡&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://7up.pics/images/2025/03/03/Snipaste_2025-03-03_20-57-43.png&quot; alt=&quot;Snipaste 2025 03 03 20 57 43&quot; /&gt;
在后端服务集群的时候&lt;/p&gt;
&lt;p&gt;:::warning
如果报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java.net.BindException: Address already in use: bind
	at java.base/sun.nio.ch.Net.bind0(Native Method) ~[na:na]
	at java.base/sun.nio.ch.Net.bind(Net.java:555) ~[na:na]
	at java.base/sun.nio.ch.ServerSocketChannelImpl.netBind(ServerSocketChannelImpl.java:337) ~[na:na]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意修改该配置
&lt;img src=&quot;https://7up.pics/images/2025/03/03/Snipaste_2025-03-03_21-00-51.png&quot; alt=&quot;Snipaste 2025 03 03 21 00 51&quot; /&gt;
:::&lt;/p&gt;
&lt;p&gt;在这里可以看到集群
&lt;img src=&quot;../../assets/img/1896547140906254336.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在编辑中修改策略
&lt;img src=&quot;../../assets/img/1896548283585658880.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;调度路由算法讲解&lt;/h3&gt;
&lt;p&gt;当执行器集群部署时，提供丰富的路由策略，包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FIRST（第一个）：固定选择第一个机器&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LAST（最后一个）：固定选择最后一个机器；&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ROUND（轮询）：依次的选择在线的机器发起调度&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RANDOM（随机）：随机选择在线的机器；&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CONSISTENT_HASH（一致性HASH）：&lt;/code&gt;
&lt;code&gt;每个任务按照Hash算法固定选择某一台机器，且所有任务均匀散列在不同机器上。&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LEAST_FREQUENTLY_USED（最不经常使用）：使用频率最低的机器优先被选举；&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LEAST_RECENTLY_USED（最近最久未使用）：最久未使用的机器优先被选举；&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FAILOVER（故障转移）：按照顺序依次进行心跳检测，第一个心跳检测成功的机器选定为目标执行器并发起调度；&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;BUSYOVER（忙碌转移）：按照顺序依次进行空闲检测，第一个空闲检测成功的机器选定为目标执行器并发起调度；&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SHARDING_BROADCAST(分片广播)：&lt;/code&gt;
&lt;code&gt;广播触发对应集群中所有机器执行一次任务，同时系统自动传递分片参数；可根据分片参数开发分片任务；&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;分片案例准备&lt;/h2&gt;
&lt;p&gt;引入pom依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;druid&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.2.20&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.18.30&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mybatis-plus-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.5.3.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;8.0.33&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.datasource.url=jdbc:mysql://IP地址:3306/xxl_job_demo?serverTimezone=GMT%2B8&amp;amp;useUnicode=true&amp;amp;characterEncoding=UTF-8
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=密码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接口与任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void sendMsgHandler();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    @XxlJob(&quot;sendMsgHandler&quot;)
    public void sendMsgHandler() {
        List&amp;lt;UserMobilePlan&amp;gt; userMobilePlans = userMobilePlanMapper.selectList(new LambdaQueryWrapper&amp;lt;UserMobilePlan&amp;gt;()
                .orderByDesc(UserMobilePlan::getId));
        System.out.println(&quot;任务开始时间:&quot; + new Date() + &quot;,处理任务数量:&quot; + userMobilePlans.size());
        Long startTime = System.currentTimeMillis();
        userMobilePlans.forEach(item -&amp;gt; {
            try {
                //模拟发送短信动作
                TimeUnit.MILLISECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(&quot;任务结束时间:&quot; + new Date());
        System.out.println(&quot;任务耗时:&quot; + (System.currentTimeMillis() - startTime) + &quot;毫秒&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info
注意这个
&lt;code&gt;@XxlJob(&quot;sendMsgHandler&quot;)&lt;/code&gt;
:::&lt;/p&gt;
&lt;p&gt;在任务调度中心，添加新任务
&lt;img src=&quot;../../assets/img/1896557327775956992.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;启动之后就会看到
&lt;img src=&quot;../../assets/img/1896557730890514432.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;分片案例改造&lt;/h2&gt;
&lt;p&gt;比如我们的案例中有2000+条数据，如果不采取分片形式的话，任务只会在一台机器上执行，这样的话需要20+秒才能执行完任务.&lt;/p&gt;
&lt;p&gt;如果采取分片广播的形式的话，一次任务调度将会广播触发对应集群中所有执行器执行一次任务，同时系统自动传递分片参数；可根据分片参数开发分片任务；&lt;/p&gt;
&lt;p&gt;获取分片参数方式:&lt;/p&gt;
&lt;p&gt;可以理解为一台执行器就是一个分片，索引则是从0开始&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这两个参数，我们可以通过求模取余的方式，分别查询，分别执行，这样的话就可以提高处理的速度；&lt;/p&gt;
&lt;p&gt;之前2000+条数据只在一台机器上执行需要20+秒才能完成任务，分片后，有两台机器可以共同完成2000+条数据，每台机器处理1000+条数据，这样的话只需要10+秒就能完成任务；&lt;/p&gt;
&lt;p&gt;改造之前的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
    @XxlJob(&quot;sendMsgHandler&quot;)
    public void sendMsgHandler() {
        System.out.println(&quot;任务开始时间:&quot; + new Date());
        int shardTotal = XxlJobHelper.getShardTotal();
        int shardIndex = XxlJobHelper.getShardIndex();
        List&amp;lt;UserMobilePlan&amp;gt; userMobilePlans;
        if (shardTotal == 1) {
            //如果没有分片就直接查询所有数据
            userMobilePlans = userMobilePlanMapper.selectList(new LambdaQueryWrapper&amp;lt;UserMobilePlan&amp;gt;()
                    .orderByDesc(UserMobilePlan::getId));
        } else {
            userMobilePlans = userMobilePlanMapper.selectByMod(shardIndex, shardTotal);
        }
        System.out.println(&quot;处理任务数量:&quot; + userMobilePlans.size());
        Long startTime = System.currentTimeMillis();
        userMobilePlans.forEach(item -&amp;gt; {
            try {
                TimeUnit.MILLISECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(&quot;任务结束时间:&quot; + new Date());
        System.out.println(&quot;任务耗时:&quot; + (System.currentTimeMillis() - startTime) + &quot;毫秒&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    List&amp;lt;UserMobilePlan&amp;gt; selectByMod(@Param(&quot;shardingIndex&quot;) Integer shardingIndex, @Param(&quot;shardingTotal&quot;) Integer shardingTotal);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;select id=&quot;selectByMod&quot; resultMap=&quot;BaseResultMap&quot;&amp;gt;
    select * from t_user_mobile_plan where mod(id,#{shardingTotal})=#{shardingIndex}
  &amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改调度中心
&lt;img src=&quot;../../assets/img/1896562793742598144.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发现每个执行器执行任务的速度大大缩短
&lt;img src=&quot;../../assets/img/1896563311047081984.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::info
&lt;code&gt;shardTotal&lt;/code&gt;：表示任务的总分片数（即任务被拆分成多少份）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shardIndex&lt;/code&gt;：表示当前任务实例的分片索引（从 0 开始）。&lt;/p&gt;
&lt;p&gt;分片机制允许将一个大任务拆分成多个小任务，分配到不同的机器或线程中并行执行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;XxlJobHelper&lt;/code&gt; 是 XXL-JOB 提供的工具类，用于获取任务运行时的相关信息。
:::&lt;/p&gt;
&lt;p&gt;分片适用场景：适用于需要处理大量数据的任务，尤其是需要分布式部署的场景。
通过分片机制，可以有效提升任务的并发处理能力。&lt;/p&gt;
&lt;h3&gt;sql分析&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;#{shardingTotal}&lt;/code&gt;：分片总数，表示任务被拆分成多少份。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#{shardingIndex}&lt;/code&gt;：当前分片索引，表示当前任务实例负责处理哪一部分数据。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mod(id, #{shardingTotal})&lt;/code&gt; ：
使用 mod 函数对 id 取模运算，结果是 &lt;code&gt;id % shardingTotal&lt;/code&gt;。
例如，如果 &lt;code&gt;shardingTotal = 4&lt;/code&gt;，则 id 的取模结果可能是 0, 1, 2, 3。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;= #{shardingIndex}&lt;/code&gt; ：
将取模结果与当前分片索引 &lt;code&gt;shardingIndex&lt;/code&gt; 比较，筛选出符合条件的数据。
例如，如果 &lt;code&gt;shardingIndex = 2&lt;/code&gt;，则只选择 &lt;code&gt;id % shardingTotal = 2&lt;/code&gt; 的记录。&lt;/p&gt;
&lt;p&gt;假设表中有以下数据（id 列）：
id: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10&lt;/p&gt;
&lt;p&gt;如果 shardingTotal = 4，shardingIndex = 2，则计算过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 % 4 = 1 → 不符合
2 % 4 = 2 → 符合
3 % 4 = 3 → 不符合
4 % 4 = 0 → 不符合
5 % 4 = 1 → 不符合
6 % 4 = 2 → 符合
7 % 4 = 3 → 不符合
8 % 4 = 0 → 不符合
9 % 4 = 1 → 不符合
10 % 4 = 2 → 符合
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终查询结果为：
id: 2, 6, 10&lt;/p&gt;
&lt;h2&gt;项目集成xxl-job&lt;/h2&gt;
&lt;p&gt;引入依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.xuxueli&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;xxl-job-core&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.3.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Nacos中或yaml文件中配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 调度中心部署根地址 [选填]：如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行&quot;执行器心跳注册&quot;和&quot;任务结果回调&quot;；为空则关闭自动注册。
xxl:
  job:
    admin:
      addresses: http://127.0.0.1:8080/xxl-job-admin

# 执行器通讯TOKEN [选填]：非空时启用。
    accessToken: default_token

# 执行器AppName [选填]：执行器心跳注册分组依据；为空则关闭自动注册。
    executor:
      appname: xxl-job-executor-sample 执行器的AppName

# 执行器注册 [选填]：优先使用该配置作为注册地址，为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活地支持容器类型执行器动态IP和动态映射端口问题。
      address:

# 执行器IP [选填]：默认为空表示自动获取IP，多网卡时可手动设置指定IP，该IP不会绑定Host仅作为通讯实用；地址信息用于 &quot;执行器注册&quot; 和 &quot;调度中心请求并触发任务&quot;。
      ip: 127.0.0.1

# 执行器端口号 [选填]：小于等于0则自动获取；默认端口为9999，单机部署多个执行器时，注意要配置不同执行器端口。
      port: 9999

# 执行器运行日志文件存储磁盘路径 [选填] ：需要对该路径拥有读写权限；为空则使用默认路径。
      logpath: /data/applogs/xxl-job/jobhandler

# 执行器日志文件保存天数 [选填] ： 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能。
      logretentiondays: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加配置类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class XxlJobConfig {
    @Value(&quot;${xxl.job.admin.addresses}&quot;)
    private String adminAddresses;
    @Value(&quot;${xxl.job.accessToken}&quot;)
    private String accessToken;
    @Value(&quot;${xxl.job.executor.appname}&quot;)
    private String appname;
    // @Value(&quot;${xxl.job.executor.address}&quot;)
    // private String address;
    @Value(&quot;${xxl.job.executor.ip}&quot;)
    private String ip;
    @Value(&quot;${xxl.job.executor.port}&quot;)
    private int port;
    @Value(&quot;${xxl.job.executor.logpath}&quot;)
    private String logPath;
    @Value(&quot;${xxl.job.executor.logretentiondays}&quot;)
    private int logRetentionDays;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        // xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
        return xxlJobSpringExecutor;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加执行器
&lt;img src=&quot;../../assets/img/Snipaste_2025-03-04_21-40-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;OnLine 机器地址&lt;/code&gt;一栏中便可查看，服务注册的机器&lt;/p&gt;
&lt;p&gt;代码中添加job任务
&lt;img src=&quot;../../assets/img/Snipaste_2025-03-04_21-50-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;调度中心新增任务
&lt;img src=&quot;../../assets/img/Snipaste_2025-03-04_21-47-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Markdown Extended Features</title><link>https://zzyang.top/posts/markdown-extended/</link><guid isPermaLink="true">https://zzyang.top/posts/markdown-extended/</guid><description>Read more about Markdown features in Fuwari</description><pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;GitHub Repository Cards&lt;/h2&gt;
&lt;p&gt;You can add dynamic cards that link to GitHub repositories, on page load, the repository information is pulled from the GitHub API.&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;Fabrizz/MMM-OnSpotify&quot;}&lt;/p&gt;
&lt;p&gt;Create a GitHub repository card with the code &lt;code&gt;::github{repo=&quot;&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;&quot;}&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::github{repo=&quot;saicaca/fuwari&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Admonitions&lt;/h2&gt;
&lt;p&gt;Following types of admonitions are supported: &lt;code&gt;note&lt;/code&gt; &lt;code&gt;tip&lt;/code&gt; &lt;code&gt;important&lt;/code&gt; &lt;code&gt;warning&lt;/code&gt; &lt;code&gt;caution&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;:::note
Highlights information that users should take into account, even when skimming.
:::&lt;/p&gt;
&lt;p&gt;:::tip
Optional information to help a user be more successful.
:::&lt;/p&gt;
&lt;p&gt;:::important
Crucial information necessary for users to succeed.
:::&lt;/p&gt;
&lt;p&gt;:::warning
Critical content demanding immediate user attention due to potential risks.
:::&lt;/p&gt;
&lt;p&gt;:::caution
Negative potential consequences of an action.
:::&lt;/p&gt;
&lt;h3&gt;Basic Syntax&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;:::note
Highlights information that users should take into account, even when skimming.
:::

:::tip
Optional information to help a user be more successful.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom Titles&lt;/h3&gt;
&lt;p&gt;The title of the admonition can be customized.&lt;/p&gt;
&lt;p&gt;:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GitHub Syntax&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;a href=&quot;https://github.com/orgs/community/discussions/16925&quot;&gt;The GitHub syntax&lt;/a&gt; is also supported.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; The GitHub syntax is also supported.

&amp;gt; [!TIP]
&amp;gt; The GitHub syntax is also supported.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Spoiler&lt;/h3&gt;
&lt;p&gt;You can add spoilers to your text. The text also supports &lt;strong&gt;Markdown&lt;/strong&gt; syntax.&lt;/p&gt;
&lt;p&gt;The content :spoiler[is hidden &lt;strong&gt;ayyy&lt;/strong&gt;]!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The content :spoiler[is hidden **ayyy**]!

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Expressive Code Example</title><link>https://zzyang.top/posts/expressive-code/</link><guid isPermaLink="true">https://zzyang.top/posts/expressive-code/</guid><description>How code blocks look in Markdown using Expressive Code.</description><pubDate>Wed, 10 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Here, we&apos;ll explore how code blocks look using &lt;a href=&quot;https://expressive-code.com/&quot;&gt;Expressive Code&lt;/a&gt;. The provided examples are based on the official documentation, which you can refer to for further details.&lt;/p&gt;
&lt;h2&gt;Expressive Code&lt;/h2&gt;
&lt;h3&gt;Syntax Highlighting&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/syntax-highlighting/&quot;&gt;Syntax Highlighting&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Regular syntax highlighting&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;This code is syntax highlighted!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Rendering ANSI escape sequences&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;ANSI colors:
- Regular: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
- Bold:    [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
- Dimmed:  [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m

256 colors (showing colors 160-177):
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m

Full RGB colors:
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m

Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Editor &amp;amp; Terminal Frames&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/frames/&quot;&gt;Editor &amp;amp; Terminal Frames&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Code editor frames&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Title attribute example&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- src/content/index.html --&amp;gt;
&amp;lt;div&amp;gt;File name comment example&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Terminal frames&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;This terminal frame has no title&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;Write-Output &quot;This one has a title!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Overriding frame types&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Look ma, no frame!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;# Without overriding, this would be a terminal frame
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
New-Alias tail Watch-Tail
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Text &amp;amp; Line Markers&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/text-markers/&quot;&gt;Text &amp;amp; Line Markers&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Marking full lines &amp;amp; line ranges&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Line 1 - targeted by line number
// Line 2
// Line 3
// Line 4 - targeted by line number
// Line 5
// Line 6
// Line 7 - targeted by range &quot;7-8&quot;
// Line 8 - targeted by range &quot;7-8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Selecting line marker types (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;this line is marked as deleted&apos;)
  // This line and the next one are marked as inserted
  console.log(&apos;this is the second inserted line&apos;)

  return &apos;this line uses the neutral default marker type&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Adding labels to line markers&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}
  value={value}
  className={buttonClassName}
  disabled={disabled}
  active={active}
&amp;gt;
  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Adding long labels on their own lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}

  value={value}
  className={buttonClassName}

  disabled={disabled}
  active={active}
&amp;gt;

  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Using diff-like syntax&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;+this line will be marked as inserted
-this line will be marked as deleted
this is a regular line
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+this is an actual diff file
-all contents will remain unmodified
 no whitespace will be removed either
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Combining syntax highlighting with diff-like syntax&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;  function thisIsJavaScript() {
    // This entire block gets highlighted as JavaScript,
    // and we can still add diff markers to it!
-   console.log(&apos;Old code to be removed&apos;)
+   console.log(&apos;New and shiny code!&apos;)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Marking individual text inside lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  // Mark any given text inside lines
  return &apos;Multiple matches of the given text are supported&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Regular expressions&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;The words yes and yep will be marked.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Escaping forward slashes&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Test&quot; &amp;gt; /home/test.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Selecting inline marker types (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;These are inserted and deleted marker types&apos;);
  // The return statement uses the default marker type
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Word Wrap&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/word-wrap/&quot;&gt;Word Wrap&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Configuring word wrap per block&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Example with wrap
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Example with wrap=false
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Configuring indentation of wrapped lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Example with preserveIndent (enabled by default)
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Example with preserveIndent=false
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Collapsible Sections&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/collapsible-sections/&quot;&gt;Collapsible Sections&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// All this boilerplate setup code will be collapsed
import { someBoilerplateEngine } from &apos;@example/some-boilerplate&apos;
import { evenMoreBoilerplate } from &apos;@example/even-more-boilerplate&apos;

const engine = someBoilerplateEngine(evenMoreBoilerplate())

// This part of the code will be visible by default
engine.doSomething(1, 2, 3, calcFn)

function calcFn() {
  // You can have multiple collapsed sections
  const a = 1
  const b = 2
  const c = a + b

  // This will remain visible
  console.log(`Calculation result: ${a} + ${b} = ${c}`)
  return c
}

// All this code until the end of the block will be collapsed again
engine.closeConnection()
engine.freeMemory()
engine.shutdown({ reason: &apos;End of example boilerplate code&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Line Numbers&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/line-numbers/&quot;&gt;Line Numbers&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Displaying line numbers per block&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// This code block will show line numbers
console.log(&apos;Greetings from line 2!&apos;)
console.log(&apos;I am on line 3&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Line numbers are disabled for this block
console.log(&apos;Hello?&apos;)
console.log(&apos;Sorry, do you know what line I am on?&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Changing the starting line number&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Greetings from line 5!&apos;)
console.log(&apos;I am on line 6&apos;)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Simple Guides for Fuwari</title><link>https://zzyang.top/posts/guide/</link><guid isPermaLink="true">https://zzyang.top/posts/guide/</guid><description>How to use this blog template.</description><pubDate>Mon, 01 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This blog template is built with &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;. For the things that are not mentioned in this guide, you may find the answers in the &lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Front-matter of Posts&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The title of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The date the post was published.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A short description of the post. Displayed on index page.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The cover image path of the post.&amp;lt;br/&amp;gt;1. Start with &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt;: Use web image&amp;lt;br/&amp;gt;2. Start with &lt;code&gt;/&lt;/code&gt;: For image in &lt;code&gt;public&lt;/code&gt; dir&amp;lt;br/&amp;gt;3. With none of the prefixes: Relative to the markdown file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tags of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The category of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If this post is still a draft, which won&apos;t be displayed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Where to Place the Post Files&lt;/h2&gt;
&lt;p&gt;Your post files should be placed in &lt;code&gt;src/content/posts/&lt;/code&gt; directory. You can also create sub-directories to better organize your posts and assets.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/
├── post-1.md
└── post-2/
    ├── cover.png
    └── index.md
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Markdown Example</title><link>https://zzyang.top/posts/markdown/</link><guid isPermaLink="true">https://zzyang.top/posts/markdown/</guid><description>A simple example of a Markdown blog post.</description><pubDate>Sun, 01 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;An h1 header&lt;/h1&gt;
&lt;p&gt;Paragraphs are separated by a blank line.&lt;/p&gt;
&lt;p&gt;2nd paragraph. &lt;em&gt;Italic&lt;/em&gt;, &lt;strong&gt;bold&lt;/strong&gt;, and &lt;code&gt;monospace&lt;/code&gt;. Itemized lists
look like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this one&lt;/li&gt;
&lt;li&gt;that one&lt;/li&gt;
&lt;li&gt;the other one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Block quotes are
written like so.&lt;/p&gt;
&lt;p&gt;They can span multiple paragraphs,
if you like.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., &quot;it&apos;s all
in chapters 12--14&quot;). Three dots ... will be converted to an ellipsis.
Unicode is supported. ☺&lt;/p&gt;
&lt;h2&gt;代码块带名称&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 你的代码
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 你的代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;An h2 header&lt;/h2&gt;
&lt;p&gt;Here&apos;s a numbered list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;first item&lt;/li&gt;
&lt;li&gt;second item&lt;/li&gt;
&lt;li&gt;third item&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here&apos;s a code sample:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define foobar() {
    print &quot;Welcome to flavor country!&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(which makes copying &amp;amp; pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
# Quick, count to ten!
for i in range(10):
    # (but not *too* quick)
    time.sleep(0.5)
    print i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;An h3 header&lt;/h3&gt;
&lt;p&gt;Now a nested list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;First, get these ingredients:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;carrots&lt;/li&gt;
&lt;li&gt;celery&lt;/li&gt;
&lt;li&gt;lentils&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Boil some water.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dump everything in the pot and follow
this algorithm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; find wooden spoon
 uncover pot
 stir
 cover pot
 balance wooden spoon precariously on pot handle
 wait 10 minutes
 goto first step (or shut off burner when done)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not bump wooden spoon or it will fall.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).&lt;/p&gt;
&lt;p&gt;Here&apos;s a link to &lt;a href=&quot;http://foo.bar&quot;&gt;a website&lt;/a&gt;, to a &lt;a href=&quot;local-doc.html&quot;&gt;local
doc&lt;/a&gt;, and to a &lt;a href=&quot;#an-h2-header&quot;&gt;section heading in the current
doc&lt;/a&gt;. Here&apos;s a footnote [^1].&lt;/p&gt;
&lt;p&gt;[^1]: Footnote text goes here.&lt;/p&gt;
&lt;p&gt;Tables can look like this:&lt;/p&gt;
&lt;p&gt;size material color&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;9 leather brown
10 hemp canvas natural
11 glass transparent&lt;/p&gt;
&lt;p&gt;Table: Shoes, their sizes, and what they&apos;re made of&lt;/p&gt;
&lt;p&gt;(The above is the caption for the table.) Pandoc also supports
multi-line tables:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;keyword text&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;red Sunsets, apples, and
other red or reddish
things.&lt;/p&gt;
&lt;p&gt;green Leaves, grass, frogs
and other things it&apos;s
not easy being.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A horizontal rule follows.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Here&apos;s a definition list:&lt;/p&gt;
&lt;p&gt;apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There&apos;s no &quot;e&quot; in tomatoe.&lt;/p&gt;
&lt;p&gt;Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)&lt;/p&gt;
&lt;p&gt;Here&apos;s a &quot;line block&quot;:&lt;/p&gt;
&lt;p&gt;| Line one
| Line too
| Line tree&lt;/p&gt;
&lt;p&gt;and images can be specified like so:&lt;/p&gt;
&lt;p&gt;Inline math equations go in like so: $\omega = d\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:&lt;/p&gt;
&lt;p&gt;$$I = \int \rho R^{2} dV$$&lt;/p&gt;
&lt;p&gt;$$
\begin{equation*}
\pi
=3.1415926535
;8979323846;2643383279;5028841971;6939937510;5820974944
;5923078164;0628620899;8628034825;3421170679;\ldots
\end{equation*}
$$&lt;/p&gt;
&lt;p&gt;And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: `foo`, *bar*, etc.&lt;/p&gt;
</content:encoded></item><item><title>Include Video in the Posts</title><link>https://zzyang.top/posts/video/</link><guid isPermaLink="true">https://zzyang.top/posts/video/</guid><description>This post demonstrates how to include embedded video in a blog post.</description><pubDate>Tue, 01 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Just copy the embed code from YouTube or other platforms, and paste it in the markdown file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Include Video in the Post
published: 2023-10-19
// ...
---

&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;YouTube&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;Bilibili&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?bvid=BV1fK4y1s7Qf&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item></channel></rss>